Problem/Motivation

Let's assume the following scenario:

Detailed report:

  • Go to update.php already triggers:
    Uncaught ReferenceError: drupalSettings is not defined
  • Later Uncaught DrupalBehaviorError: attach ; batch: Cannot read property 'batch' of undefined is triggered.

In addition, the logic around maintenance mode in DbUpdateController is broken and seems to be trying to use config that's never set. We need to fix that also if we want to rely on it.

As a bonus, defined('MAINTENANCE_MODE') seems to only be true during the running of install.php now.

Proposed resolution

Disable the JS aggregation on update.php by disabling it whenever in maintenance mode. Fix the logic in the update controller so we actually go into in maintenance mode while update.php is running.

Fix the Cache ID so that JS aggregation uses the correct cache value in AssetResolver::getJsAssets()

Remaining tasks

review

User interface changes

none

API changes

none

Data model changes

none

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because JS can be broken during updates, and JS asset caching does not work correctly
Issue priority Major because it blocks a security enhancement, and can break running update.php in the UI
Prioritized changes The main goal of this issue is security becuase it enables removal of inline JS settings which enables CSP
Disruption Not disruptive for core/contributed and custom modules/themes

Comments

wim leers’s picture

Priority: Normal » Major

Seems major at least? :)

dawehner’s picture

Issue summary: View changes
wim leers’s picture

The actual problem here is that update.php should NEVER use aggregated CSS & JS. It's only by accident that we haven't encountered this problem in the past.

#2510104: Convert drupalSettings from JavaScript to JSON, to allow for CSP in the future just happens to be the first one to trigger this problem.

wim leers’s picture

Issue tags: +JavaScript
dawehner’s picture

Issue summary: View changes

.

wim leers’s picture

I think we should make BareHtmlPageRendererInterface-rendered HTML responses never aggregate CSS & JS. The installer and updater don't need better front-end performance (which is what aggregation is for), they need to be reliable.

dawehner’s picture

I'm curious whether care about fast maintenance pages if you are offline

wim leers’s picture

This is the code retrieving aggregated CSS/JS:

    if (isset($placeholders['styles'])) {
      // Optimize CSS if necessary, but only during normal site operation.
      $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
      $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css));
    }

    // Print scripts - if any are present.
    if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) {
      // Optimize JS if necessary, but only during normal site operation.
      $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess');
      list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
      $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
      $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
    }

(In \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::processAssetLibraries().)

So, the easy fix is to have BareHtmlPageRenderer do define('MAINTENANCE_MODE', TRUE). But we kind of want to get rid of that global, so…

wim leers’s picture

I'm curious whether care about fast maintenance pages if you are offline

Meaning we should have aggregation on the maintenance page?

I was thinking that too, but… your maintenance page should be super lightweight in the first place (i.e. very little CSS, and probably no JS); so I don't think it really makes sense to want to enable CSS/JS aggregation on the maintenance page.

dawehner’s picture

Besides the discussion that defining the constant might do it, I would strongly argue that this is just a workaround. There should be a proper API for that.

wim leers’s picture

Actually, install.php does set MAINTENANCE_MODE. Looks like the migration of update.php to a route + controller (DbUpdateController) lost that?

And, in fact, looks like this breaks existing code:

function hook_cache_flush() {
  if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
    _update_cache_clear();
  }
}
function user_role_permissions(array $roles) {
  if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
    return _user_role_permissions_update($roles);
  }
  // Install and update pages are treated differently to prevent theming overrides.
  if (defined('MAINTENANCE_MODE') && (MAINTENANCE_MODE == 'install' || MAINTENANCE_MODE == 'update')) {

Those things no longer run because DbUpdateController doesn't set const MAINTENANCE_MODE = 'update';. Only authorize.php still does.
Looks like DbUpdateController should too?

wim leers’s picture

Besides the discussion that defining the constant might do it, I would strongly argue that this is just a workaround. There should be a proper API for that.

Agreed in principle, but attachments, including assets, are only handled AFTER we have a HtmlResponse object. So whatever non-global-state API we provide, would need to make its way into HtmlResponse too; i.e. HtmlResponse needs to carry the configuration.

(Not saying that's bad, just pointing it out.)

wim leers’s picture

StatusFileSize
new1.76 KB

So DbUpdateController sets a value in state rather than the constant. That's why #11 points out broken code. Here's a patch with the quick'n'dirty fix.

This could be a solution that's equally hacky as the current mess. But agreed with #10 in principle that we want a proper API. Just not sure that's worth it, because the API simply is… the configuration. The only places where you want to disable aggregation are places where the site's deployed state is being modified.

Actually, perhaps this could be implemented using a config override?

catch’s picture

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new3.56 KB

Alternatively, here's a config override-based approach. Untested.

dawehner’s picture

MHH, this would add an overhead on every page, which is sad.

\Drupal\Core\Config\Config::setSettingsOverride is public, so it could be called from the controller, couldn't it?

wim leers’s picture

Title: update.php doesn't run due to cached JS assets » update.php doesn't run due to aggregated JS assets
pwolanin’s picture

#13 looks reasonable if it resolves the bug. What's wrong with that apporach?

dawehner’s picture

StatusFileSize
new0 bytes
new814 bytes

#13 looks reasonable if it resolves the bug. What's wrong with that apporach?

Right, but its simply adding an overhead on every request. Let's not start to do bad things.

pwolanin’s picture

Looks like that need to merge with Wims patch?

pwolanin’s picture

Are the 2 extra method calls that much of a worry?

dawehner’s picture

Looks like that need to merge with Wims patch?

Mh, no?

Are the 2 extra method calls that much of a worry?

Are you really sure we talk about just 2? For example cacheable metadata needs to be created, then merged.
I bet you easily come to 50, and well, the approach from wim is more difficult.

fabianx’s picture

Issue tags: +Needs tests

We will need some tests - but #19 looks fine on its own.

dawehner’s picture

Mh, that is too late to be actually able to change it, mh...

dawehner’s picture

I would vote for adding a flag on the route to be able to opt out.

pwolanin’s picture

Can we even just disable JS and not CSS here? Especially with the plan to break CSS into many more small files that may break worse on some browsers, and generally the CSS isn't functional for update.php

dawehner’s picture

yeah I don't care about CSS here.

wim leers’s picture

I prefer consistency and prudence, but don't have a strong opinion.

Peter, can you link to the issue where we are splitting up CSS in many small files?

serg2’s picture

pwolanin’s picture

Yes, that's the one davidhernandez was working on recently.

pwolanin’s picture

Status: Needs review » Needs work

Testing this locally, I don't see that the JS files are non-aggregated with this patch applied:

--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -155,6 +155,10 @@ public function handle($op, Request $request) {
       $_SESSION['update_ignore_warnings'] = TRUE;
     }
 
+    // Disable JS aggregation so as to never serve old aggregated code that
+    // might break the update batch process.
+    $this->config('system.performance')->setSettingsOverride(['js.preprocess' => FALSE]);
+
     $regions = array();
     $requirements = update_check_requirements();
     $severity = drupal_requirements_severity($requirements)

Am I missing something about how this should work?

dawehner’s picture

Yeah this is why we need maybe a flag on the route itself. It is kinda great that you cannot just change global state as you like it to be.

wim leers’s picture

Right.

What do you propose, @dawehner?

pwolanin’s picture

Title: update.php doesn't run due to aggregated JS assets » DbUpdateController boken mainteneance mode logic, and update.php doesn't run due to aggregated JS assets
Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new2.92 KB

Wow, the logic in DbUpdateController seems to be 2x broken.

This is a new patch.

wim leers’s picture

Title: DbUpdateController boken mainteneance mode logic, and update.php doesn't run due to aggregated JS assets » DbUpdateController has broken mainteneance mode logic, and update.php doesn't run due to aggregated JS assets
Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -164,7 +164,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
+      $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess') && !\Drupal::state()->get('system.maintenance_mode');

If we go this way after all, can we then at least keep the two "is maintenance mode?"-checks together?

All we need now, is test coverage.

pwolanin’s picture

StatusFileSize
new5.99 KB
new4.25 KB

Trying to write the test, I found a serious bug in \Drupal\Core\Asset\AssetResolver::getJsAssets() in that is was not including the $optimize flag in the cache ID, so was serving incorrect cached assets.

So, this includes that fix plus a small test change to verify that the JS aggregation is off when maintenance mode is enabled.

pwolanin’s picture

Status: Needs work » Needs review
dawehner’s picture

Will try to manually reproduce my previous failures.

+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -544,13 +544,13 @@ protected function updateTasksList($active = NULL) {
   protected function triggerBatch(Request $request) {
-    // During the update, bring the site offline so that schema changes do not
-    // affect visiting users.
-    $maintenance_mode = $this->config('system.maintenance')->get('enabled');
-    if (isset($maintenance_mode)) {
-      $_SESSION['maintenance_mode'] = $maintenance_mode;
-    }
-    if (empty($_SESSION['maintenance_mode'])) {
+    $maintenance_mode = $this->state->get('system.maintenance_mode', FALSE);
+    // Store the prior value in the session so it can be restored at the end
+    // of the batch.
+    $_SESSION['maintenance_mode'] = $maintenance_mode;
+    // During the update, always bring the site offline so that schema changes
+    // do not affect visiting users.
+    if (empty($maintenance_mode)) {
       $this->state->set('system.maintenance_mode', TRUE);
     }
 
@@ -630,11 +630,11 @@ public static function batchFinished($success, $results, $operations) {

@@ -630,11 +630,11 @@ public static function batchFinished($success, $results, $operations) {
     $_SESSION['updates_remaining'] = $operations;
 
     // Now that the update is done, we can put the site back online if it was
-    // previously in maintenance mode.
-    if (isset($_SESSION['maintenance_mode'])) {
+    // previously not in maintenance mode.
+    if (empty($_SESSION['maintenance_mode'])) {
       \Drupal::state()->set('system.maintenance_mode', FALSE);
-      unset($_SESSION['maintenance_mode']);
     }
+    unset($_SESSION['maintenance_mode']);
   }

Do we need this fix here? #2435135: Maintenance Mode after Database Update seems to tackle the same problem but also takes care of providing a test

dawehner’s picture

Peter asked me to manually check out the patch:

pwolanin’s picture

@dawehner, the other issue doesn't seem to have a real fix?

pwolanin’s picture

StatusFileSize
new7.18 KB
new1.19 KB

Applying patch from https://www.drupal.org/node/2435135#comment-10165198

credit to wuinfo for that

pwolanin’s picture

Issue summary: View changes
Issue tags: -Needs tests
StatusFileSize
new11.35 KB
new2.11 KB
new5.36 KB
new5.69 KB

Expanding the test so we check running both running updates starting from out of or in maintenance mode and making sure that's preserved.

Also, 2 test only patches to show the fails during update.php and the fails around aggregation.

wim leers’s picture

This looks great! Pretty much only nits. RTBC once they are fixed. Especially considering #39.

  1. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -221,7 +221,7 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
         // Add the theme name to the cache key since themes may implement
         // hook_js_alter(). Additionally add the current language to support
         // translation of JavaScript files.
    

    We should update this code comment too, to remain in sync.

  2. +++ b/core/modules/system/src/Controller/DbUpdateController.php
    @@ -544,13 +544,13 @@ protected function updateTasksList($active = NULL) {
    +    // Store the prior value in the session so it can be restored at the end
    +    // of the batch.
    

    Nit: 80 cols.

  3. +++ b/core/modules/system/src/Controller/DbUpdateController.php
    @@ -544,13 +544,13 @@ protected function updateTasksList($active = NULL) {
    +    // During the update, always bring the site offline so that schema changes
    

    Nit: "bring the site offline" sounds a bit strange. Why not just "put in maintenance mode"?

  4. +++ b/core/modules/system/src/Tests/System/SiteMaintenanceTest.php
    @@ -42,6 +44,10 @@ protected function setUp() {
    +    // JS Should be aggregated, so drupal.js is not in the page source.
    
    @@ -52,7 +58,10 @@ protected function testSiteMaintenance() {
    +    // JS Should not be aggregated, so drupal.js is in the page source.
    

    s/Should/should/

  5. +++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php
    @@ -173,6 +173,54 @@ function testNoUpdateFunctionality() {
    +  }
    +  /**
    

    Missing \n.

pwolanin’s picture

StatusFileSize
new11.41 KB
new3.13 KB

@Wim Leers -

43.1: the CSS code above doesn't mention the $optimize flag, even though it includes it already. I don't think that needs a comment, since it's obvious why it's needed.

  public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
    $theme_info = $this->themeManager->getActiveTheme();
    // Add the theme name to the cache key since themes may implement
    // hook_css_alter().
    $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;

Fixed other comment issues.

The last submitted patch, 42: 2538274-42-SiteMaintenanceTest-only.patch, failed testing.

The last submitted patch, 42: 2538274-42-UpdateScriptTest-only.patch, failed testing.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Looks alright for me now

wim leers’s picture

+1

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed fbde5c4 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation to the issue summary.

  1. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -221,7 +221,7 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
    -    $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' .  Crypt::hashBase64(serialize($assets));
    +    $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' .  Crypt::hashBase64(serialize($assets)) . (int) $optimize;
    

    Not a huge fan of just tacking a 0 or 1 on to the end of the key but this is exactly the same as the CSS version so okay.

  2. +++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php
    @@ -173,6 +173,55 @@ function testNoUpdateFunctionality() {
    +  function testMaintenanceModelUpdateFunctionality() {
    

    Fixed Model -> Mode on commit.

  • alexpott committed fbde5c4 on 8.0.x
    Issue #2538274 by pwolanin, Wim Leers, dawehner: DbUpdateController has...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.