Commit 4983036c authored by catch's avatar catch

Issue #2540416 by dawehner, hussainweb: Move update.php back to a front controller

parent 7730866f
<?php
/**
* @file
* Contains \Drupal\Core\Update\UpdateKernel.
*/
namespace Drupal\Core\Update;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Site\Settings;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a kernel which is used primarily to run the update of Drupal.
*
* We use a dedicated kernel + front controller (update.php) in order to be able
* to repair Drupal if it is in a broken state.
*
* @see update.php
* @see \Drupal\system\Controller\DbUpdateController
*/
class UpdateKernel extends DrupalKernel {
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
try {
static::bootEnvironment();
// First boot up basic things, like loading the include files.
$this->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. We need the session both for authentication
// as well as the DB update, like
// \Drupal\system\Controller\DbUpdateController::batchFinished.
$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();
}
}
/**
* Set up the request with fake routing data for update.php.
*
* This fake routing data is needed in order to make batch API work 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, $this->getContainer()->get('router.route_provider')->getRouteByName('system.db_update'));
$op = $args[0] ?: 'info';
$request->attributes->set('op', $op);
$request->attributes->set('_raw_variables', new ParameterBag(['op' => $op]));
}
/**
* Checks if the current user has rights to access updates page.
*
* If the current user does not have the rights, an exception is thrown.
*
* @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.');
}
}
}
......@@ -286,7 +286,7 @@ protected function getAllOptions(\SimpleXMLElement $element) {
* Link position counting from zero.
* @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
* messages: use strtr() 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
......@@ -299,7 +299,7 @@ protected function getAllOptions(\SimpleXMLElement $element) {
*/
protected function assertLink($label, $index = 0, $message = '', $group = 'Other') {
$links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label));
$message = ($message ? $message : SafeMarkup::format('Link with label %label found.', array('%label' => $label)));
$message = ($message ? $message : strtr('Link with label %label found.', array('%label' => $label)));
return $this->assert(isset($links[$index]), $message, $group);
}
......@@ -377,6 +377,30 @@ protected function assertNoLinkByHref($href, $message = '', $group = 'Other') {
return $this->assert(empty($links), $message, $group);
}
/**
* 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.
*
......
......@@ -160,13 +160,13 @@ public function handle($op, Request $request) {
$severity = drupal_requirements_severity($requirements);
if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) {
$regions['sidebar_first'] = $this->updateTasksList('requirements');
$output = $this->requirements($severity, $requirements);
$output = $this->requirements($severity, $requirements, $request);
}
else {
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.
......@@ -204,10 +204,13 @@ public function handle($op, Request $request) {
/**
* Returns the info database update page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current 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 +236,11 @@ protected function info() {
'#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
);
$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 +248,13 @@ protected function info() {
/**
* Renders a list of available database updates.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current 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 +347,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 +369,9 @@ 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'));
// @todo Simplify with https://www.drupal.org/node/2548095
$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 +387,21 @@ protected function selection() {
/**
* Displays results of the update script with any accompanying errors.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
protected function results() {
protected function results(Request $request) {
// @todo Simplify with https://www.drupal.org/node/2548095
$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 <a href="@url">logged</a>.', 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 +409,7 @@ protected function results() {
}
if (!empty($_SESSION['update_success'])) {
$message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('<front>')->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '</p>';
$message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('<front>')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '</p>';
}
else {
$last = reset($_SESSION['updates_remaining']);
......@@ -420,7 +433,7 @@ protected function results() {
);
$build['links'] = array(
'#theme' => 'links',
'#links' => $this->helpfulLinks(),
'#links' => $this->helpfulLinks($request),
);
// Output a list of info messages.
......@@ -492,12 +505,15 @@ protected function results() {
/**
* Renders a list of requirement errors or warnings.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
public function requirements($severity, array $requirements) {
public function requirements($severity, array $requirements, Request $request) {
$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($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
$build['status_report'] = array(
'#theme' => 'status_report',
......@@ -603,7 +619,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 +656,23 @@ public static function batchFinished($success, $results, $operations) {
/**
* Provides links to the homepage and administration pages.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* An array of links.
*/
protected function helpfulLinks() {
protected function helpfulLinks(Request $request) {
// @todo Simplify with https://www.drupal.org/node/2548095
$base_url = str_replace('/update.php', '', $request->getBaseUrl());
$links['front'] = array(
'title' => $this->t('Front page'),
'url' => Url::fromRoute('<front>'),
'url' => Url::fromRoute('<front>')->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;
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Update\UpdatePathWithBrokenRoutingTest.
*/
namespace Drupal\system\Tests\Update;
/**
* Tests the update path with a broken router.
*
* @group Update
*/
class UpdatePathWithBrokenRoutingTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/drupal-8.broken_routing.php',
];
parent::setUp();
}
/**
* Tests running update.php with some form of broken routing.
*/
public function testWithBrokenRouting() {
// Make sure we can get to the front page.
$this->drupalGet('<front>');
$this->assertResponse(200);
// Simulate a broken router, and make sure the front page is
// inaccessible.
\Drupal::state()->set('update_script_test_broken_inbound', TRUE);
\Drupal::service('cache_tags.invalidator')->invalidateTags(['route_match', 'rendered']);
$this->drupalGet('<front>');
$this->assertResponse(500);
// The exceptions are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
foreach ($this->assertions as $key => $assertion) {
if (strpos($assertion['message'], 'core/modules/system/tests/modules/update_script_test/src/PathProcessor/BrokenInboundPathProcessor.php') !== FALSE) {
unset($this->assertions[$key]);
$this->deleteAssert($assertion['message_id']);
}
}
$this->runUpdates();
// Remove the simulation of the broken router, and make sure we can get to
// the front page again.
\Drupal::state()->set('update_script_test_broken_inbound', FALSE);
$this->drupalGet('<front>');
$this->assertResponse(200);
}
}
......@@ -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.
......
......@@ -448,16 +448,14 @@ system.batch_page.json:
options:
_admin_route: TRUE
# Note: This route just exists for generating URLs, the dedicated
# frontcontroller is used if the URL is accessed.
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'
......
<?php
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$config = unserialize($connection->query("SELECT data FROM {config} where name = :name", [':name' => 'core.extension'])->fetchField());
$config['module']['update_script_test'] = 0;
$connection->update('config')
->fields(['data' => serialize($config)])
->condition('name', 'core.extension')
->execute();
$connection->insert('key_value')
->fields(['collection' => 'system.schema', 'name' => 'update_script_test', 'value' => serialize(8000)])
->execute();
<?php
/**
* @file
* Contains \Drupal\update_script_test\PathProcessor\BrokenInboundPathProcessor.
*/
namespace Drupal\update_script_test\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Example path processor which breaks on inbound.
*/
class BrokenInboundPathProcessor implements InboundPathProcessorInterface {
/**
* The state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a new BrokenInboundPathProcessor instance.
*
* @param \Drupal\Core\State\StateInterface $state
* The state.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
if ($this->state->get('update_script_test_broken_inbound', FALSE)) {
throw new \RuntimeException();
}
else {
return $path;
}
}
}
services:
update_script_test.broken_path_processor:
class: Drupal\update_script_test\PathProcessor\BrokenInboundPathProcessor
arguments: ['@state']
tags:
- { name: path_processor_inbound, priority: 1000 }
<?php
/**
* @file
* The PHP page that handles updating the Drupal installation.
*
* All Drupal code is released under the GNU General Public License.
* See COPYRIGHT.txt and LICENSE.txt files in the "core" directory.
*/
use Drupal\Core\Update\UpdateKernel;
use Symfony\Component\HttpFoundation\Request;
$autoloader = require_once 'autoload.php';
$kernel = new UpdateKernel('prod', $autoloader);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment