diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 3238ce6..e90cd0c 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -96,9 +96,10 @@ class ExtensionDiscovery { * @param string $root * The app root. */ - public function __construct($root) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL) { $this->root = $root; - $this->fileCache = FileCacheFactory::get('extension_discovery'); + $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; + $this->profileDirectories = $profile_directories; } /** @@ -427,7 +428,7 @@ protected function scanDirectory($dir, $include_tests) { continue; } - if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) { + if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) { $files[$cached_extension->getType()][$key] = $cached_extension; continue; } @@ -467,7 +468,10 @@ protected function scanDirectory($dir, $include_tests) { $extension->origin = $dir; $files[$type][$key] = $extension; - $this->fileCache->set($fileinfo->getPathName(), $extension); + + if ($this->fileCache) { + $this->fileCache->set($fileinfo->getPathName(), $extension); + } } return $files; } 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/system.routing.yml b/core/modules/system/system.routing.yml index a656ab3..3ec57c9 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -449,7 +449,7 @@ system.batch_page.json: _admin_route: TRUE system.db_update: - path: '/update.php/{op}' + path: '/admin/update/{op}' defaults: _title: 'Drupal database update' _controller: '\Drupal\system\Controller\DbUpdateController::handle' 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 new file mode 100644 index 0000000..bcf5334 --- /dev/null +++ b/core/update.php @@ -0,0 +1,158 @@ +getPath() . '/' . $module->getName() . '.update_early.inc'; + if (file_exists($filename)) { + include_once $filename; + } +} + +/** + * Gets all available update_early functions. + * + * @return string[] + */ +function _update_get_available_update_early_functions() { + $regexp = '/^(?.+)_update_early_(?\d+)$/'; + $functions = get_defined_functions(); + + $updates = []; + foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { + // 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'] . '_update_early_' . $matches['version']; + } + } + + // 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('..'); + +$autoloader = require_once __DIR__ . '/vendor/autoload.php'; +require_once __DIR__ . '/includes/utility.inc'; + +$request = Request::createFromGlobals(); +// Manually resemble early bootstrap of DrupalKernel::boot(). +require_once __DIR__ . '/includes/bootstrap.inc'; +DrupalKernel::bootEnvironment(); + +try { + Settings::initialize(dirname(__DIR__), DrupalKernel::findSitePath($request), $autoloader); + $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(); + } + } + 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.'); + } + } + + $response = new Response(); + $log_filename = PublicStream::basePath() . '/.ht.update_early'; + + // 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(); + + 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()); + $response->prepare($request)->send(); + exit; +}