diff --git a/core/lib/Drupal/Core/Update/UpdateKernel.php b/core/lib/Drupal/Core/Update/UpdateKernel.php new file mode 100644 index 0000000..20a7275 --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateKernel.php @@ -0,0 +1,164 @@ +initializeSettings($request); + $this->boot(); + $container = $this->getContainer(); + /** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */ + $request_stack = $container->get('request_stack'); + $request_stack->push($request); + $this->preHandle($request); + + // Handle the actual request. In order to have batch API working we need + // to use session API, see \batch_process(). + $this->bootSession($request, $type); + $result = $this->handleRaw($request); + $this->shutdownSession($request); + + return $result; + } + catch (\Exception $e) { + return $this->handleException($e, $request, $type); + } + } + + /** + * Generates the actual result of update.php. + * + * The actual logic of the update is done in the db update controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + * + * @return \Symfony\Component\HttpFoundation\Response + * A response object. + * + * @see \Drupal\system\Controller\DbUpdateController + */ + protected function handleRaw(Request $request) { + $container = $this->getContainer(); + + $this->handleAccess($request, $container); + + /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */ + $controller_resolver = $container->get('controller_resolver'); + + /** @var callable $db_update_controller */ + $db_update_controller = $controller_resolver->getControllerFromDefinition('\Drupal\system\Controller\DbUpdateController::handle'); + + $this->setupRequestMatch($request); + + $arguments = $controller_resolver->getArguments($request, $db_update_controller); + return call_user_func_array($db_update_controller, $arguments); + } + + /** + * Boots up the session. + * + * bootSession() + shutdownSession() basically simulates what + * \Drupal\Core\StackMiddleware\Session does. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + */ + protected function bootSession(Request $request) { + $container = $this->getContainer(); + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = $container->get('session'); + $session->start(); + $request->setSession($session); + } + + /** + * Ensures that the session is saved. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + */ + protected function shutdownSession(Request $request) { + if ($request->hasSession()) { + $request->getSession()->save(); + } + } + + /** + * Ensures that the request has a fake routing data for update.php. + * + * This fake routing data is needed in order to make batch API working + * properly. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + */ + protected function setupRequestMatch(Request $request) { + $path = $request->getPathInfo(); + $args = explode('/', ltrim($path, '/')); + + $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'system.db_update'); + $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/update.php/{op}', ['op' => 'info'], ['_access' => 'TRUE'])); + $op = $args[0] ?: 'info'; + $request->attributes->set('op', $op); + $request->attributes->set('_raw_variables', new ParameterBag(['op' => $op])); + } + + /** + * Checks access and throws an access, in case it is not. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when update.php should not be accessible. + */ + protected function handleAccess(Request $request) { + /** @var \Drupal\Core\Authentication\AuthenticationManager $authentication_manager */ + $authentication_manager = $this->getContainer()->get('authentication'); + $account = $authentication_manager->authenticate($request) ?: new AnonymousUserSession(); + + /** @var \Drupal\Core\Session\AccountProxyInterface $current_user */ + $current_user = $this->getContainer()->get('current_user'); + $current_user->setAccount($account); + + /** @var \Drupal\system\Access\DbUpdateAccessCheck $db_update_access */ + $db_update_access = $this->getContainer()->get('access_check.db_update'); + + if (!Settings::get('update_free_access', FALSE) && !$db_update_access->access($account)->isAllowed()) { + 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.'); + } + } + +} diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index b0efbe5..945257a 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -378,6 +378,30 @@ protected function assertNoLinkByHref($href, $message = '', $group = 'Other') { } /** + * Passes if a link containing a given href is not found in the main region. + * + * @param string $href + * The full or partial value of the 'href' attribute of the anchor tag. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use format_string() to embed variables in the message text, not + * t(). If left blank, a default message will be displayed. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNoLinkByHrefInMainRegion($href, $message = '', $group = 'Other') { + $links = $this->xpath('//main//a[contains(@href, :href)]', array(':href' => $href)); + $message = ($message ? $message : SafeMarkup::format('No link containing href %href found.', array('%href' => $href))); + return $this->assert(empty($links), $message, $group); + } + + /** * Passes if the raw text IS found on the loaded page, fail otherwise. * * Raw text refers to the raw HTML that the page generated. diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index c2e195e..6c177a2 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -166,7 +166,7 @@ public function handle($op, Request $request) { switch ($op) { case 'selection': $regions['sidebar_first'] = $this->updateTasksList('selection'); - $output = $this->selection(); + $output = $this->selection($request); break; case 'run': @@ -176,12 +176,12 @@ public function handle($op, Request $request) { case 'info': $regions['sidebar_first'] = $this->updateTasksList('info'); - $output = $this->info(); + $output = $this->info($request); break; case 'results': $regions['sidebar_first'] = $this->updateTasksList('results'); - $output = $this->results(); + $output = $this->results($request); break; // Regular batch ops : defer to batch processing API. @@ -207,7 +207,7 @@ public function handle($op, Request $request) { * @return array * A render array. */ - protected function info() { + protected function info(Request $request) { // Change query-strings on css/js files to enforce reload for all users. _drupal_flush_css_js(); // Flush the cache of all data for the update status module. @@ -233,12 +233,11 @@ protected function info() { '#markup' => '

' . $this->t('When you have performed the steps above, you may proceed.') . '

', ); - $url = new Url('system.db_update', array('op' => 'selection')); $build['link'] = array( '#type' => 'link', '#title' => $this->t('Continue'), '#attributes' => array('class' => array('button', 'button--primary')), - '#url' => $url, + '#url' => Url::fromUri($request->getUriForPath('/selection')), ); return $build; } @@ -246,10 +245,13 @@ protected function info() { /** * Renders a list of available database updates. * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * * @return array * A render array. */ - protected function selection() { + protected function selection(Request $request) { // Make sure there is no stale theme registry. $this->cache->deleteAll(); @@ -342,7 +344,7 @@ protected function selection() { unset($build); $build['links'] = array( '#theme' => 'links', - '#links' => $this->helpfulLinks(), + '#links' => $this->helpfulLinks($request), ); // No updates to run, so caches won't get flushed later. Clear them now. @@ -364,7 +366,8 @@ protected function selection() { else { $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates'); } - $url = new Url('system.db_update', array('op' => 'run')); + $base_url = str_replace('/update.php', '', $request->getBaseUrl()); + $url = (new Url('system.db_update', array('op' => 'run')))->setOption('base_url', $base_url); $build['link'] = array( '#type' => 'link', '#title' => $this->t('Apply pending updates'), @@ -380,15 +383,20 @@ protected function selection() { /** * Displays results of the update script with any accompanying errors. * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * * @return array * A render array. */ - protected function results() { + protected function results(Request $request) { + $base_url = str_replace('/update.php', '', $request->getBaseUrl()); + // Report end result. $dblog_exists = $this->moduleHandler->moduleExists('dblog'); if ($dblog_exists && $this->account->hasPermission('access site reports')) { $log_message = $this->t('All errors have been logged.', array( - '@url' => Url::fromRoute('dblog.overview')->toString(TRUE)->getGeneratedUrl(), + '@url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(), )); } else { @@ -396,7 +404,7 @@ protected function results() { } if (!empty($_SESSION['update_success'])) { - $message = '

' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your site. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('')->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '

'; + $message = '

' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your site. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '

'; } else { $last = reset($_SESSION['updates_remaining']); @@ -420,7 +428,7 @@ protected function results() { ); $build['links'] = array( '#theme' => 'links', - '#links' => $this->helpfulLinks(), + '#links' => $this->helpfulLinks($request), ); // Output a list of info messages. @@ -497,7 +505,7 @@ protected function results() { */ public function requirements($severity, array $requirements) { $options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array(); - $try_again_url = Url::fromRoute('system.db_update', $options)->toString(TRUE)->getGeneratedUrl(); + $try_again_url = Url::fromUri(\Drupal::request()->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl(); $build['status_report'] = array( '#theme' => 'status_report', @@ -603,7 +611,7 @@ protected function triggerBatch(Request $request) { ); batch_set($batch); - return batch_process('update.php/results', Url::fromRoute('system.db_update', array('op' => 'start'))); + return batch_process(Url::fromUri($request->getUriForPath('/results')), Url::fromUri($request->getUriForPath('/start'))); } /** @@ -640,18 +648,22 @@ public static function batchFinished($success, $results, $operations) { /** * Provides links to the homepage and administration pages. * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * * @return array * An array of links. */ - protected function helpfulLinks() { + protected function helpfulLinks(Request $request) { + $base_url = str_replace('/update.php', '', $request->getBaseUrl()); $links['front'] = array( 'title' => $this->t('Front page'), - 'url' => Url::fromRoute(''), + 'url' => Url::fromRoute('')->setOption('base_url', $base_url), ); if ($this->account->hasPermission('access administration pages')) { $links['admin-pages'] = array( 'title' => $this->t('Administration pages'), - 'url' => Url::fromRoute('system.admin'), + 'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url), ); } return $links; diff --git a/core/modules/system/src/Tests/Update/UpdateScriptTest.php b/core/modules/system/src/Tests/Update/UpdateScriptTest.php index 7aed861..cba80ca 100644 --- a/core/modules/system/src/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php @@ -153,7 +153,7 @@ function testNoUpdateFunctionality() { $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.')); $this->assertNoLink('Administration pages'); - $this->assertNoLinkByHref('update.php', 0); + $this->assertNoLinkByHrefInMainRegion('update.php', 0); $this->clickLink('Front page'); $this->assertResponse(200); @@ -164,7 +164,7 @@ function testNoUpdateFunctionality() { $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.')); $this->assertLink('Administration pages'); - $this->assertNoLinkByHref('update.php', 1); + $this->assertNoLinkByHrefInMainRegion('update.php', 1); $this->clickLink('Administration pages'); $this->assertResponse(200); } @@ -198,7 +198,7 @@ function testSuccessfulUpdateFunctionality() { $this->assertText('Updates were attempted.'); $this->assertLink('logged'); $this->assertLink('Administration pages'); - $this->assertNoLinkByHref('update.php', 1); + $this->assertNoLinkByHrefInMainRegion('update.php', 1); $this->clickLink('Administration pages'); $this->assertResponse(200); } @@ -253,7 +253,7 @@ protected function updateScriptTest($maintenance_mode) { // Verify that there are no links to different parts of the workflow. $this->assertNoLink('Administration pages'); - $this->assertNoLinkByHref('update.php', 0); + $this->assertNoLinkByHrefInMainRegion('update.php', 0); $this->assertNoLink('logged'); // Verify the front page can be visited following the upgrade. diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index a656ab3..9ef3720 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -451,13 +451,9 @@ system.batch_page.json: system.db_update: path: '/update.php/{op}' defaults: - _title: 'Drupal database update' - _controller: '\Drupal\system\Controller\DbUpdateController::handle' op: 'info' - options: - _maintenance_access: TRUE requirements: - _access_system_update: 'TRUE' + _access: 'TRUE' system.admin_content: path: '/admin/content' diff --git a/update.php b/update.php new file mode 100644 index 0000000..04125a3 --- /dev/null +++ b/update.php @@ -0,0 +1,14 @@ +handle($request); +$response->send(); + +$kernel->terminate($request, $response);