diff --git a/core/modules/system/src/Tests/Update/DedicatedFrontControllerTest.php b/core/modules/system/src/Tests/Update/DedicatedFrontControllerTest.php new file mode 100644 index 0000000..79051fc --- /dev/null +++ b/core/modules/system/src/Tests/Update/DedicatedFrontControllerTest.php @@ -0,0 +1,142 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../tests/fixtures/update/drupal-8.update_early.php', + ]; + parent::setUp(); + } + + protected function setupBrokenContainer() { + \Drupal::state()->set('error_service_test_break_authentication', TRUE); + $this->kernel->rebuildContainer(); + } + + protected function setupNonBrokenContainer() { + \Drupal::state()->set('error_service_test_break_authentication', FALSE); + $this->kernel->rebuildContainer(); + } + + protected function setupFreeAccess() { + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + $filename = $this->siteDirectory . '/settings.php'; + chmod($filename, 0666); + + $settings['settings']['update_free_access'] = (object) [ + 'value' => TRUE, + 'required' => TRUE, + ]; + drupal_rewrite_settings($settings); + } + + protected function setupNoFreeAccess() { + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + $filename = $this->siteDirectory . '/settings.php'; + chmod($filename, 0666); + + $settings['settings']['update_free_access'] = (object) [ + 'value' => FALSE, + 'required' => TRUE, + ]; + drupal_rewrite_settings($settings); + } + + /** + * Tests the various scenarios of access to /core/update.php. + */ + public function testUpdatePhpAccess() { + $this->setupBrokenContainer(); + $this->dotestAccessDeniedWithBrokenBootstrapAndWithoutFreeAccess(); + $this->dotestAccessAllowedWithBrokenBootstrapAndFreeAccess(); + + $this->setupNonBrokenContainer(); + $this->dotestAccessDeniedAsUserWithoutPermission(); + } + + /** + * Tests a broken authentication without $update_free_access. + */ + public function dotestAccessDeniedWithBrokenBootstrapAndWithoutFreeAccess() { + $this->drupalGet('core/update.php'); + $this->assertResponse(403); + } + + /** + * Tests a broken authentication with $update_free_access. + */ + public function dotestAccessAllowedWithBrokenBootstrapAndFreeAccess() { + $this->setupFreeAccess(); + + $this->drupalGet('core/update.php'); + $this->assertResponse(200); + + $this->setupNoFreeAccess(); + } + + /** + * Tests a non broken authentication without access. + */ + protected function dotestAccessDeniedAsUserWithoutPermission() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->setupNoFreeAccess(); + + $this->drupalGet('core/update.php'); + $this->assertResponse(403); + } + + /** + * Tests a non broken authentication without access but $update_free_access. + */ + protected function dotestAccessDeniedAsUserWithoutPermissionAndFreeAccess() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->setupFreeAccess(); + + $this->drupalGet('core/update.php'); + + $this->assertResponse(200); + } + + public function testUpdatePhpRepair() { + $this->setupFreeAccess(); + $this->setupBrokenContainer(); + + $this->drupalGet('core/update.php'); + + // Ensure that the user got redirected. + $this->assertUrl('admin/update'); + + // Ensure that the update_early_N ran. + $this->assertFalse(\Drupal::state()->get('error_service_test_break_authentication')); + // Ensure that the function was logged. + + $log_filename = PublicStream::basePath() . '/.ht.update_early'; + $this->assertEqual("error_service_test_update_early_8000\n", file_get_contents($log_filename)); + } + +} diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update_early.php b/core/modules/system/tests/fixtures/update/drupal-8.update_early.php new file mode 100644 index 0000000..575eac2 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.update_early.php @@ -0,0 +1,17 @@ +query("SELECT data FROM {config} WHERE name = 'core.extension'")->fetchField()); +$extensions['module']['error_service_test'] = 0; +$database->update('config') + ->fields(['data' => serialize($extensions)]) + ->condition('name', 'core.extension') + ->execute(); + +$database->insert('key_value') + ->fields(['collection' => 'system.schema', 'name' => 'error_service_test', 'value' => serialize(8000)]) + ->execute(); + diff --git a/core/modules/system/tests/modules/error_service_test/error_service_test.update_early.inc b/core/modules/system/tests/modules/error_service_test/error_service_test.update_early.inc new file mode 100644 index 0000000..f5624e0 --- /dev/null +++ b/core/modules/system/tests/modules/error_service_test/error_service_test.update_early.inc @@ -0,0 +1,14 @@ +update('key_value') + ->condition('collection', 'state') + ->condition('name', 'error_service_test_break_authentication') + ->fields(['value' => serialize(FALSE)]) + ->execute(); +} diff --git a/core/modules/system/tests/modules/error_service_test/src/BrokenAuthenticationManager.php b/core/modules/system/tests/modules/error_service_test/src/BrokenAuthenticationManager.php new file mode 100644 index 0000000..fa798a5 --- /dev/null +++ b/core/modules/system/tests/modules/error_service_test/src/BrokenAuthenticationManager.php @@ -0,0 +1,45 @@ +get('error_service_test_break_authentication')) { + $container->register('authentication_manager', 'Drupal\error_service_test\BrokenAuthenticationManager'); + } } + } diff --git a/core/update.php b/core/update.php index 02ca5cc..bcf5334 100644 --- a/core/update.php +++ b/core/update.php @@ -6,11 +6,17 @@ * possible dependencies, so that it can change for example some of the really * low level tables. * + * In order to have access to this front controller you either needed to be logged + * in before (this might or might not work), or have $update_free_access set in + * your + * + * * @see \Drupal\system\Controller\DbUpdateController */ use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\DrupalKernel; +use Drupal\Core\DrupalKernelInterface; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Site\Settings; @@ -47,18 +53,50 @@ function _update_get_available_update_early_functions() { // If this function is a module update function, add it to the list of // module updates. if (preg_match($regexp, $function, $matches)) { - $updates[$matches['module']][] = $matches['version']; + $updates[] = $matches['module'] . '_update_early_' . $matches['version']; } } - // Ensure that updates are applied in numerical order. - foreach ($updates as &$module_updates) { - sort($module_updates, SORT_NUMERIC); - } + // Ensure that updates are applied in right order. + // @todo Does that mean we need to take into account module weights? + sort($updates); return $updates; } +function _update_get_missing_update_early_functions(DrupalKernelInterface $kernel) { + // We need a) the list of active modules (we get that from the config + // bootstrap factory) and b) the path to the modules, we use the extension + // discovery for that. + + // Scan the module list. + // We don't support install profiles at that point? + $extension_discovery = new ExtensionDiscovery($kernel->getAppRoot(), FALSE, []); + $module_extensions = $extension_discovery->scan('module'); + + $config = BootstrapConfigStorageFactory::get(); + + // Load all the update_early.inc files. + foreach (array_keys($config->read('core.extension')['module']) as $module) { + if (isset($module_extensions[$module])) { + _update_load_update_early_file($module_extensions[$module]); + } + } + + // First figure out which hook_update_early got executed already. + $filename = PublicStream::basePath() . '/.ht.update_early'; + $existing_update_early_N = []; + if (file_exists($filename)) { + $existing_update_early_N = file_get_contents($filename); + $existing_update_early_N = explode("\n", $existing_update_early_N); + } + + $available_update_early_N = _update_get_available_update_early_functions(); + $not_executed_update_early_N = array_diff($available_update_early_N, $existing_update_early_N); + + return $not_executed_update_early_N; +} + // Change the directory to the Drupal root. chdir('..'); @@ -72,53 +110,46 @@ function _update_get_available_update_early_functions() { try { Settings::initialize(dirname(__DIR__), DrupalKernel::findSitePath($request), $autoloader); - - if (!Settings::get('update_free_access', FALSE)) { - throw new AccessDeniedHttpException(); - } - else { - $kernel = new DrupalKernel('prod', $autoloader); - $kernel->setSitePath(DrupalKernel::findSitePath($request)); - - // We need a) the list of active modules (we get that from the config - // bootstrap factory) and b) the path to the modules, we use the extension - // discovery for that. - - // Scan the module list. - // We don't support install profiles at that point? - $extension_discovery = new ExtensionDiscovery($kernel->getAppRoot(), FALSE, []); - $module_extensions = $extension_discovery->scan('module'); - - $config = BootstrapConfigStorageFactory::get(); - - // Load all the update_early.inc files. - foreach (array_keys($config->read('core.extension')['module']) as $module) { - if (isset($module_extensions[$module])) { - _update_load_update_early_file($module_extensions[$module]); - } + $kernel = new DrupalKernel('prod', $autoloader); + $kernel->setSitePath(DrupalKernel::findSitePath($request)); + + // Try to boot up Drupal and authenicate the user. In case it works, we know + // that the user has access. Otherwise we check for $update_free_access, if + // this also doesn't work we show at least some help message. + try { + $kernel->boot(); + $container = $kernel->getContainer(); + /** @var \Drupal\Core\Authentication\AuthenticationManager $authentication_manager */ + $authentication_manager = $container->get('authentication'); + $account = $authentication_manager->authenticate($request); + + // Ensure that the user is allowed to access update.php. + if (!$account || !$container->get('access_check.db_update')->access($account)) { + throw new AccessDeniedHttpException(); } - - // First figure out which hook_update_early got executed already. - $filename = PublicStream::basePath() . '/.ht.update_early'; - $existing_update_early_N = []; - if (file_exists($filename)) { - $existing_update_early_N = file_get_contents($filename); - $existing_update_early_N = explode("\n", $existing_update_early_N); + } + catch (\Exception $e) { + if (!Settings::get('update_free_access')) { + throw new AccessDeniedHttpException('In order to run update.php you need to either be logged in as admin or have set $update_free_access in your settings.php.'); } + } - $available_update_early_N = _update_get_available_update_early_functions(); - $not_executed_update_early_N = array_diff($available_update_early_N, $existing_update_early_N); + $response = new Response(); + $log_filename = PublicStream::basePath() . '/.ht.update_early'; - // Execute all of the remainign ones. - foreach ($available_update_early_N as $function) { - $output = $function(); - $response->setContent($output)->prepare($request)->send(); - } + // Execute all of the remaining ones. + $missing_update_early_N = _update_get_missing_update_early_functions($kernel); + foreach ($missing_update_early_N as $function) { + $output = $function(); + $response->setContent($output)->prepare($request)->send(); - // Redirect to the actual update controller. - $response = new RedirectResponse(str_replace('core/update.php', '', $request->getUriForPath('/admin/update'))); - $response->prepare($request)->send(); + file_put_contents($log_filename, $function . "\n", FILE_APPEND); } + + // Redirect to the actual update controller which can deal with a bootstrapped + // Drupal. + $response = new RedirectResponse(str_replace('core/update.php', '', $request->getUriForPath('/admin/update'))); + $response->prepare($request)->send(); } catch (HttpExceptionInterface $e) { $response = new Response('', $e->getStatusCode());