diff --git a/core/core.update_early_services.yml b/core/core.update_early_services.yml new file mode 100644 index 0000000..0d2399d --- /dev/null +++ b/core/core.update_early_services.yml @@ -0,0 +1,17 @@ +parameters: + session.storage.options: {} +services: + database: + class: Drupal\Core\Database\Connection + factory: Drupal\Core\Database\Database::getConnection + arguments: [default] + session_configuration: + class: Drupal\Core\Session\SessionConfiguration + arguments: ['%session.storage.options%'] + authentication: + class: Drupal\Core\Authentication\AuthenticationManager + arguments: ['@authentication_collector'] + authentication_collector: + class: Drupal\Core\Authentication\AuthenticationCollector + tags: + - { name: service_collector, tag: authentication_provider, call: addProvider } diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 3238ce6..0b04fde 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -91,14 +91,23 @@ class ExtensionDiscovery { protected $fileCache; /** + * The site path. + * + * @var string + */ + protected $sitePath; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root * The app root. */ - public function __construct($root) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) { $this->root = $root; - $this->fileCache = FileCacheFactory::get('extension_discovery'); + $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; + $this->profileDirectories = $profile_directories; + $this->sitePath = $site_path; } /** @@ -172,7 +181,7 @@ public function scan($type, $include_tests = NULL) { $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path'); } else { - $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals()); + $searchdirs[static::ORIGIN_SITE] = isset($this->sitePath) ? $this->sitePath : DrupalKernel::findSitePath(Request::createFromGlobals()); } // Unless an explicit value has been passed, manually check whether we are @@ -184,6 +193,7 @@ public function scan($type, $include_tests = NULL) { } $files = array(); + print_r($searchdirs); foreach ($searchdirs as $dir) { // Discover all extensions in the directory, unless we did already. if (!isset(static::$files[$dir][$include_tests])) { @@ -392,6 +402,8 @@ protected function scanDirectory($dir, $include_tests) { $dir_prefix = ($dir == '' ? '' : "$dir/"); $absolute_dir = ($dir == '' ? $this->root : $this->root . "/$dir"); + print_r('hello'); + print_r($absolute_dir); if (!is_dir($absolute_dir)) { return $files; } @@ -423,11 +435,13 @@ protected function scanDirectory($dir, $include_tests) { foreach ($iterator as $key => $fileinfo) { // All extension names in Drupal have to be valid PHP function names due // to the module hook architecture. + print_r($fileinfo); if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) { continue; } - if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) { + print_r(123); + if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) { $files[$cached_extension->getType()][$key] = $cached_extension; continue; } @@ -467,7 +481,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/lib/Drupal/Core/Update/UpdateEarlyKernel.php b/core/lib/Drupal/Core/Update/UpdateEarlyKernel.php new file mode 100644 index 0000000..482cc61 --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateEarlyKernel.php @@ -0,0 +1,69 @@ +serviceYamls = [ + 'app' => [], + 'site' => [], + ]; + $this->serviceProviderClasses = [ + 'app' => [], + 'site' => [], + ]; + $this->serviceYamls['app']['core'] = 'core/core.update_early_services.yml'; + + // Retrieve enabled modules and register their namespaces. + if (!isset($this->moduleList)) { + $extensions = $this->getConfigStorage()->read('core.extension'); + $this->moduleList = isset($extensions['module']) ? $extensions['module'] : array(); + } + $module_filenames = $this->getModuleFileNames(); + $this->classLoaderAddMultiplePsr4($this->getModuleNamespacesPsr4($module_filenames)); + + // Load each module's .update_early_services files. + foreach ($module_filenames as $module => $filename) { + $filename = dirname($filename) . "/$module.update_early_services.yml"; + if (file_exists($filename)) { + $this->serviceYamls['app'][$module] = $filename; + } + } + } + + /** + * {@inheritdoc} + */ + protected function initializeContainer() { + // Always force a container rebuild. + $this->containerNeedsRebuild = TRUE; + $container = parent::initializeContainer(); + return $container; + } + +} diff --git a/core/lib/Drupal/Core/Update/UpdateEarlyRegistry.php b/core/lib/Drupal/Core/Update/UpdateEarlyRegistry.php new file mode 100644 index 0000000..57263fb --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateEarlyRegistry.php @@ -0,0 +1,120 @@ +root = $root; + $this->sitePath = $site_path; + $this->enabledModules = $enabled_modules; + $this->logFilename = $log_filename; + } + + /** + * Gets all available update_early functions. + * + * @return callable[] + */ + protected function getAvailableEarlyUpdateFunctions() { + $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; + } + + /** + * Gets the non already executed update_early functions. + * + * @return callable[] + */ + public function getMissingUpdateEarlyFunctions() { + // 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($this->root, FALSE, [], $this->sitePath); + $module_extensions = $extension_discovery->scan('module', FALSE); + + $this->loadUpdateEarlyFiles($module_extensions); + + // First figure out which hook_update_early got executed already. + $existing_update_early_N = []; + if (file_exists($this->logFilename)) { + $existing_update_early_N = file_get_contents($this->logFilename); + $existing_update_early_N = explode("\n", $existing_update_early_N); + } + + $available_update_early_N = $this->getAvailableEarlyUpdateFunctions(); + $not_executed_update_early_N = array_diff($available_update_early_N, $existing_update_early_N); + + return $not_executed_update_early_N; + } + + protected function loadUpdateEarlyFiles(array $module_extensions) { + // Load all the update_early.inc files. + foreach ($this->enabledModules as $module) { + if (isset($module_extensions[$module])) { + $this->loadUpdateEarlyFile($module_extensions[$module]); + } + } + } + + /** + * Loads the update_early.inc file for a given extension. + * + * @param \Drupal\Core\Extension\Extension $module + */ + protected function loadUpdateEarlyFile(Extension $module) { + $filename = $module->getPath() . '/' . $module->getName() . '.update_early.inc'; + if (file_exists($filename)) { + include_once $filename; + } + } + + + +} 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/modules/user/user.update_early_services.yml b/core/modules/user/user.update_early_services.yml new file mode 100644 index 0000000..4fe3282 --- /dev/null +++ b/core/modules/user/user.update_early_services.yml @@ -0,0 +1,6 @@ +services: + user.authentication.cookie: + class: Drupal\user\Authentication\Provider\Cookie + arguments: ['@session_configuration', '@database'] + tags: + - { name: authentication_provider, provider_id: 'cookie', priority: 0, global: TRUE } diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateEarlyRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateEarlyRegistryTest.php new file mode 100644 index 0000000..d56a083 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Update/UpdateEarlyRegistryTest.php @@ -0,0 +1,64 @@ + ['module_a.update_early.inc' => $module_a, 'module_a.info.yml' => $module_a]], + ['sites/default/modules/module_b' => ['module_b.update_early.inc' => $module_b, 'module_b.info.yml' => $module_b]] + ]); + + $update_registry = new UpdateEarlyRegistry('drupal', 'sites/default', ['module_a', 'module_b'], 'sites/default/files/.ht.update_early'); + + print_r($update_registry->getMissingUpdateEarlyFunctions()); + } + +} diff --git a/core/update.php b/core/update.php new file mode 100644 index 0000000..8211dd2 --- /dev/null +++ b/core/update.php @@ -0,0 +1,95 @@ +setSitePath(UpdateEarlyKernel::findSitePath($request)); + + $log_filename = $kernel->getSitePath() . '/.ht.update_early'; + $update_early_registry = new UpdateEarlyRegistry($kernel->getAppRoot(), $log_filename); + + // 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. + if (!Settings::get('update_free_access')) { + 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 (AccessDeniedHttpException $e) { + 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.'); + } + catch (\Exception $e) { + throw new AccessDeniedHttpException('Authentication fataled for running update.php, so you need to set $update_free_access in settings.php.'); + } + } + + $response = new Response(); + + // Execute all of the remaining ones. + $missing_update_early_N = $update_early_registry->getMissingUpdateEarlyFunctions($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; +}