diff --git a/.htaccess b/.htaccess index 0c89072..f0c5d96 100644 --- a/.htaccess +++ b/.htaccess @@ -108,7 +108,7 @@ DirectoryIndex index.php index.html index.htm RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} !=/favicon.ico - RewriteRule ^ index.php [L] + RewriteRule ^(.*)$ index.php [L] # Rules to correctly serve gzip compressed CSS and JS files. # Requires both mod_rewrite and mod_headers to be enabled. diff --git a/core/authorize.php b/core/authorize.php index d703b33..fd0774d 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -84,10 +84,7 @@ module_list(TRUE, FALSE, FALSE, $module_list); drupal_load('module', 'system'); drupal_load('module', 'user'); -// We also want to have the language system available, but we do *NOT* want to -// actually call drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE), since that would -// also force us through the DRUPAL_BOOTSTRAP_PAGE_HEADER phase, which loads -// all the modules, and that's exactly what we're trying to avoid. +// Initialize the language system. drupal_language_initialize(); // Initialize the maintenance theme for this administrative script. diff --git a/core/includes/batch.inc b/core/includes/batch.inc index 83ddd30..0b07d8e 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -14,6 +14,8 @@ * @see batch_get() */ +use \Symfony\Component\HttpFoundation\JsonResponse; + /** * Loads a batch from the database. * @@ -77,7 +79,7 @@ function _batch_page() { case 'do': // JavaScript-based progress page callback. - _batch_do(); + $output = _batch_do(); break; case 'do_nojs': @@ -160,7 +162,7 @@ function _batch_do() { // Perform actual processing. list($percentage, $message) = _batch_process(); - drupal_json_output(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message)); + return new JsonResponse(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message)); } /** diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index f65ab4c..4e22259 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -4,6 +4,7 @@ use Drupal\Core\Database\Database; use Symfony\Component\ClassLoader\UniversalClassLoader; use Symfony\Component\ClassLoader\ApcUniversalClassLoader; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; /** * @file @@ -137,12 +138,13 @@ const DRUPAL_BOOTSTRAP_SESSION = 4; const DRUPAL_BOOTSTRAP_PAGE_HEADER = 5; /** - * Seventh bootstrap phase: find out language of the page. + * Seventh bootstrap phase: load code for subsystems and modules; validate and + * fix input data. */ -const DRUPAL_BOOTSTRAP_LANGUAGE = 6; +const DRUPAL_BOOTSTRAP_CODE = 6; /** - * Final bootstrap phase: Drupal is fully loaded; validate and fix input data. + * Final bootstrap phase: initialize language, path, theme, and modules. */ const DRUPAL_BOOTSTRAP_FULL = 7; @@ -1535,6 +1537,29 @@ function request_uri($omit_query_string = FALSE) { } /** + * Returns the current global request object. + * + * @todo Replace this function with a proper dependency injection container. + * + * @staticvar Symfony\Component\HttpFoundation\Request $request + * + * @param Symfony\Component\HttpFoundation\Request $new_request + * Optional. The new request object to store. This parameter should only be + * used by index.php. + * + * @return Symfony\Component\HttpFoundation\Request + * The current request object. + */ +function request(Request $new_request = NULL) { + static $request; + + if ($new_request) { + $request = $new_request; + } + return $request; +} + +/** * Logs an exception. * * This is a wrapper function for watchdog() which automatically decodes an @@ -2023,7 +2048,7 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { DRUPAL_BOOTSTRAP_VARIABLES, DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_PAGE_HEADER, - DRUPAL_BOOTSTRAP_LANGUAGE, + DRUPAL_BOOTSTRAP_CODE, DRUPAL_BOOTSTRAP_FULL, ); // Not drupal_static(), because the only legitimate API to control this is to @@ -2076,12 +2101,12 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { _drupal_bootstrap_page_header(); break; - case DRUPAL_BOOTSTRAP_LANGUAGE: - drupal_language_initialize(); + case DRUPAL_BOOTSTRAP_CODE: + require_once DRUPAL_ROOT . '/core/includes/common.inc'; + _drupal_bootstrap_code(); break; case DRUPAL_BOOTSTRAP_FULL: - require_once DRUPAL_ROOT . '/core/includes/common.inc'; _drupal_bootstrap_full(); break; } @@ -2301,11 +2326,6 @@ function _drupal_bootstrap_variables() { */ function _drupal_bootstrap_page_header() { bootstrap_invoke_all('boot'); - - if (!drupal_is_cli()) { - ob_start(); - drupal_page_header(); - } } /** diff --git a/core/includes/common.inc b/core/includes/common.inc index 14154af..8437eff 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ uid == 0) || ($token == drupal_get_token($value))); } -function _drupal_bootstrap_full() { +function _drupal_bootstrap_code() { static $called = FALSE; if ($called) { @@ -5195,6 +5205,24 @@ function _drupal_bootstrap_full() { ini_set('log_errors', 1); ini_set('error_log', 'public://error.log'); } +} + +/** + * Temporary BC function for scripts not using DrupalKernel. + * + * DrupalKernel skips this and replicates it via event listeners. + */ +function _drupal_bootstrap_full($skip = FALSE) { + static $called = FALSE; + + if ($called || $skip) { + $called = TRUE; + return; + } + + // Initialize language (which can strip path prefix) prior to initializing + // current_path(). + drupal_language_initialize(); // Initialize current_path() prior to invoking hook_init(). drupal_path_initialize(); @@ -5228,7 +5256,7 @@ function _drupal_bootstrap_full() { * * @see drupal_page_header() */ -function drupal_page_set_cache() { +function drupal_page_set_cache($response_body) { global $base_root; if (drupal_page_is_cacheable()) { diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 0524170..97f5b6a 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -5,6 +5,8 @@ * Functions for error handling. */ +use Symfony\Component\HttpFoundation\Response; + /** * Error reporting level: display no errors. */ @@ -215,10 +217,6 @@ function _drupal_log_error($error, $fatal = FALSE) { watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); - if ($fatal) { - drupal_add_http_header('Status', '500 Service unavailable (with message)'); - } - if (drupal_is_cli()) { if ($fatal) { // When called from CLI, simply output a plain text message. @@ -254,8 +252,14 @@ function _drupal_log_error($error, $fatal = FALSE) { drupal_set_title(t('Error')); // We fallback to a maintenance page at this point, because the page generation // itself can generate errors. - print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); - exit; + $output = theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); + + $response = new Response($output, 500); + if ($fatal) { + $response->setStatusCode(500, '500 Service unavailable (with message)'); + } + + return $response; } } } diff --git a/core/includes/file.inc b/core/includes/file.inc index b476bc7..7edd4be 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -5,6 +5,9 @@ * API for handling file uploads and server file management. */ +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\StreamedResponse; use Drupal\Core\StreamWrapper\LocalStream; /** @@ -2046,18 +2049,27 @@ function file_download() { $function = $module . '_file_download'; $result = $function($uri); if ($result == -1) { - return drupal_access_denied(); + throw new AccessDeniedHttpException(); } if (isset($result) && is_array($result)) { $headers = array_merge($headers, $result); } } if (count($headers)) { - file_transfer($uri, $headers); + return new StreamedResponse(function() use ($uri) { + $scheme = file_uri_scheme($uri); + // Transfer file in 1024 byte chunks to save memory usage. + if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) { + while (!feof($fd)) { + print fread($fd, 1024); + } + fclose($fd); + } + }, 200, $headers); } - return drupal_access_denied(); + throw new AccessDeniedHttpException(); } - return drupal_not_found(); + throw new NotFoundHttpException(); } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 31ef052..dea784b 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -3,6 +3,9 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Install\TaskException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + /** * @file * API functions for installing Drupal. @@ -241,6 +244,14 @@ function install_begin_request(&$install_state) { drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + // A request object from the HTTPFoundation to tell us about the request. + $request = Request::createFromGlobals(); + + // Set the global $request object. This is a temporary measure to + // keep legacy utility functions working. It should be moved to a dependency + // injection container at some point. + request($request); + // This must go after drupal_bootstrap(), which unsets globals! global $conf; @@ -468,6 +479,15 @@ function install_run_task($task, &$install_state) { elseif ($current_batch == $function) { include_once DRUPAL_ROOT . '/core/includes/batch.inc'; $output = _batch_page(); + // Because Batch API now returns a JSON response for intermediary steps, + // but the installer doesn't handle Response objects yet, just send the + // output here and emulate the old model. + // @todo Replace this when we refactor the installer to use a request- + // response workflow. + if ($output instanceof Response) { + $output->send(); + $output = NULL; + } // The task is complete when we try to access the batch page and receive // FALSE in return, since this means we are at a URL where we are no // longer requesting a batch ID. diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php new file mode 100644 index 0000000..6302db8 --- /dev/null +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -0,0 +1,54 @@ +. + if ($request->get('ajax_iframe_upload', FALSE)) { + return 'iframeupload'; + } + + // AJAX calls need to be run through ajax rendering functions + elseif ($request->isXmlHttpRequest()) { + return 'ajax'; + } + + foreach ($request->getAcceptableContentTypes() as $mime_type) { + $format = $request->getFormat($mime_type); + if (!is_null($format)) { + return $format; + } + } + + // Do HTML last so that it always wins. + return 'html'; + } +} diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 3805864..dbf7c36 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -524,15 +524,14 @@ abstract class Connection extends PDO { } catch (PDOException $e) { if ($options['throw_exception']) { - // Add additional debug information. - if ($query instanceof DatabaseStatementInterface) { - $e->query_string = $stmt->getQueryString(); - } - else { - $e->query_string = $query; - } - $e->args = $args; - throw $e; + // Wrap the exception in another exception, because PHP does not allow + // overriding Exception::getMessage(). Its message is the extra database + // debug information. + $query_string = ($query instanceof DatabaseStatementInterface) ? $stmt->getQueryString() : $query; + $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE); + $exception = new DatabaseExceptionWrapper($message, 0, $e); + + throw $exception; } return NULL; } diff --git a/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php b/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php new file mode 100644 index 0000000..701262a --- /dev/null +++ b/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php @@ -0,0 +1,19 @@ +query('RELEASE SAVEPOINT ' . $name); } - catch (PDOException $e) { + catch (DatabaseExceptionWrapper $e) { // However, in MySQL (InnoDB), savepoints are automatically committed // when tables are altered or created (DDL transactions are not // supported). This can cause exceptions due to trying to release @@ -188,7 +190,7 @@ class Connection extends DatabaseConnection { // // To avoid exceptions when no actual error has occurred, we silently // succeed for MySQL error code 1305 ("SAVEPOINT does not exist"). - if ($e->errorInfo[1] == '1305') { + if ($e->getPrevious()->errorInfo[1] == '1305') { // If one SAVEPOINT was released automatically, then all were. // Therefore, clean the transaction stack. $this->transactionLayers = array(); diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php new file mode 100644 index 0000000..dc6762b --- /dev/null +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -0,0 +1,90 @@ +dispatcher = $dispatcher; + $this->resolver = $resolver; + + $this->matcher = new UrlMatcher(); + $this->dispatcher->addSubscriber(new RouterListener($this->matcher)); + + $negotiation = new ContentNegotiation(); + + // @todo Make this extensible rather than just hard coding some. + // @todo Add a subscriber to handle other things, too, like our Ajax + // replacement system. + $this->dispatcher->addSubscriber(new ViewSubscriber($negotiation)); + $this->dispatcher->addSubscriber(new AccessSubscriber()); + $this->dispatcher->addSubscriber(new MaintenanceModeSubscriber()); + $this->dispatcher->addSubscriber(new PathSubscriber()); + $this->dispatcher->addSubscriber(new LegacyRequestSubscriber()); + $this->dispatcher->addSubscriber(new LegacyControllerSubscriber()); + $this->dispatcher->addSubscriber(new FinishResponseSubscriber()); + $this->dispatcher->addSubscriber(new RequestCloseSubscriber()); + + // Some other form of error occured that wasn't handled by another kernel + // listener. That could mean that it's a method/mime-type/error + // combination that is not accounted for, or some other type of error. + // Either way, treat it as a server-level error and return an HTTP 500. + // By default, this will be an HTML-type response because that's a decent + // best guess if we don't know otherwise. + $this->dispatcher->addSubscriber(new ExceptionListener(array(new ExceptionController($this, $negotiation), 'execute'))); + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php new file mode 100644 index 0000000..683f1fb --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -0,0 +1,53 @@ +getRequest()->attributes->get('drupal_menu_item'); + + if (isset($router_item['access']) && !$router_item['access']) { + throw new AccessDeniedHttpException(); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestAccessCheck', 30); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php new file mode 100644 index 0000000..6e23845 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -0,0 +1,83 @@ +getResponse(); + + // Set the X-UA-Compatible HTTP header to force IE to use the most recent + // rendering engine or use Chrome's frame rendering engine if available. + $response->headers->set('X-UA-Compatible', 'IE=edge,chrome=1', false); + + // Set the Content-language header. + $response->headers->set('Content-language', drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode); + + // Because pages are highly dynamic, set the last-modified time to now + // since the page is in fact being regenerated right now. + // @todo Remove this and use a more intelligent default so that HTTP + // caching can function properly. + $response->headers->set('Last-Modified', gmdate(DATE_RFC1123, REQUEST_TIME)); + + // Also give each page a unique ETag. This will force clients to include + // both an If-Modified-Since header and an If-None-Match header when doing + // conditional requests for the page (required by RFC 2616, section 13.3.4), + // making the validation more robust. This is a workaround for a bug in + // Mozilla Firefox that is triggered when Drupal's caching is enabled and + // the user accesses Drupal via an HTTP proxy (see + // https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an + // authenticated user requests a page, and then logs out and requests the + // same page again, Firefox may send a conditional request based on the + // page that was cached locally when the user was logged in. If this page + // did not have an ETag header, the request only contains an + // If-Modified-Since header. The date will be recent, because with + // authenticated users the Last-Modified header always refers to the time + // of the request. If the user accesses Drupal via a proxy server, and the + // proxy already has a cached copy of the anonymous page with an older + // Last-Modified date, the proxy may respond with 304 Not Modified, making + // the client think that the anonymous and authenticated pageviews are + // identical. + // @todo Remove this line as no longer necessary per + // http://drupal.org/node/1573064 + $response->headers->set('ETag', '"' . REQUEST_TIME . '"'); + + // Authenticated users are always given a 'no-cache' header, and will fetch + // a fresh page on every request. This prevents authenticated users from + // seeing locally cached pages. + // @todo Revisit whether or not this is still appropriate now that the + // Response object does its own cache control procesisng and we intend to + // use partial page caching more extensively. + $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT'); + $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0'); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = array('onRespond'); + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php new file mode 100644 index 0000000..cc4da15 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php @@ -0,0 +1,64 @@ +getRequest()->attributes->get('drupal_menu_item'); + $controller = $event->getController(); + + // This BC logic applies only to functions. Otherwise, skip it. + if (is_string($controller) && function_exists($controller)) { + $new_controller = function() use ($router_item) { + return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); + }; + $event->setController($new_controller); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::CONTROLLER][] = array('onKernelControllerLegacy', 30); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php new file mode 100644 index 0000000..12ca8b6 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php @@ -0,0 +1,56 @@ +getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + menu_set_custom_theme(); + drupal_theme_initialize(); + module_invoke_all('init'); + + // Tell Drupal it is now fully bootstrapped (for the benefit of code that + // calls drupal_get_bootstrap_phase()), but without having + // _drupal_bootstrap_full() do anything, since we've already done the + // equivalent above and in earlier listeners. + _drupal_bootstrap_full(TRUE); + drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 90); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php new file mode 100644 index 0000000..daed1c9 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php @@ -0,0 +1,59 @@ +getRequest()->attributes->get('system_path'); + drupal_alter('menu_site_status', $status, $read_only_path); + + // Only continue if the site is online. + if ($status != MENU_SITE_ONLINE) { + // Deliver the 503 page. + drupal_maintenance_theme(); + drupal_set_title(t('Site under maintenance')); + $content = theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))))); + $response = new Response('Service unavailable', 503); + $response->setContent($content); + $event->setResponse($response); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestMaintenanceModeCheck', 40); + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php b/core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php new file mode 100644 index 0000000..8f29b4d --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php @@ -0,0 +1,30 @@ +attributes->get('system_path'); + return isset($path) ? $path : trim($request->getPathInfo(), '/'); + } + + public function setPath(Request $request, $path) { + $request->attributes->set('system_path', $path); + + // @todo Remove this line once code has been refactored to use the request + // object directly. + _current_path($path); + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php new file mode 100644 index 0000000..1e2a95a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -0,0 +1,127 @@ +getRequest(); + + $path = $this->extractPath($request); + + $path = drupal_get_normal_path($path); + + $this->setPath($request, $path); + } + + /** + * Resolve the front-page default path. + * + * @todo The path system should be objectified to remove the function calls in + * this method. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestFrontPageResolve(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + if (empty($path)) { + // @todo Temporary hack. Fix when configuration is injectable. + $path = variable_get('site_frontpage', 'user'); + } + + $this->setPath($request, $path); + } + + /** + * Decode language information embedded in the request path. + * + * @todo Refactor this entire method to inline the relevant portions of + * drupal_language_initialize(). See the inline comment for more details. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestLanguageResolve(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + // drupal_language_initialize() combines: + // - Determination of language from $request information (e.g., path). + // - Determination of language from other information (e.g., site default). + // - Population of determined language into drupal_container(). + // - Removal of language code from _current_path(). + // @todo Decouple the above, but for now, invoke it and update the path + // prior to front page and alias resolution. When above is decoupled, also + // add 'langcode' (determined from $request only) to $request->attributes. + _current_path($path); + drupal_language_initialize(); + $path = _current_path(); + + $this->setPath($request, $path); + } + + /** + * Decodes the path of the request. + * + * Parameters in the URL sometimes represent code-meaningful strings. It is + * therefore useful to always urldecode() those values so that individual + * controllers need not concern themselves with it. This is Drupal-specific + * logic and may not be familiar for developers used to other Symfony-family + * projects. + * + * @todo Revisit whether or not this logic is appropriate for here or if + * controllers should be required to implement this logic themselves. If we + * decide to keep this code, remove this TODO. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestDecodePath(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + $path = urldecode($path); + + $this->setPath($request, $path); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestDecodePath', 200); + $events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageResolve', 150); + $events[KernelEvents::REQUEST][] = array('onKernelRequestFrontPageResolve', 101); + $events[KernelEvents::REQUEST][] = array('onKernelRequestPathResolve', 100); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php new file mode 100644 index 0000000..8b156d4 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php @@ -0,0 +1,64 @@ +getResponse(); + $config = config('system.performance'); + + if ($config->get('cache') && ($cache = drupal_page_set_cache($response->getContent()))) { + drupal_serve_page_from_cache($cache); + } + else { + ob_flush(); + } + + _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); + drupal_cache_system_paths(); + module_implements_write_cache(); + system_run_automated_cron(); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::TERMINATE][] = array('onTerminate'); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/RouterListener.php b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php new file mode 100644 index 0000000..f1a96f7 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php @@ -0,0 +1,81 @@ +urlMatcher = $urlMatcher; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + * + * This method is nearly identical to the parent, except it passes the + * $request->attributes->get('system_path') variable to the matcher. + * That is where Drupal stores its processed, de-aliased, and sanitized + * internal path. We also pass the full request object to the URL Matcher, + * since we want attributes to be available to the matcher and to controllers. + */ + public function onKernelRequest(GetResponseEvent $event) { + $request = $event->getRequest(); + + if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + $this->urlMatcher->getContext()->fromRequest($request); + $this->urlMatcher->setRequest($event->getRequest()); + } + + if ($request->attributes->has('_controller')) { + // routing is already done + return; + } + + // add attributes based on the path info (routing) + try { + $parameters = $this->urlMatcher->match($request->attributes->get('system_path')); + + if (null !== $this->logger) { + $this->logger->info(sprintf('Matched route "%s" (parameters: %s)', $parameters['_route'], $this->parametersToString($parameters))); + } + + $request->attributes->add($parameters); + unset($parameters['_route']); + unset($parameters['_controller']); + $request->attributes->set('_route_params', $parameters); + } + catch (ResourceNotFoundException $e) { + $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo()); + + throw new NotFoundHttpException($message, $e); + } + catch (MethodNotAllowedException $e) { + $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), strtoupper(implode(', ', $e->getAllowedMethods()))); + + throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php new file mode 100644 index 0000000..4761d47 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -0,0 +1,128 @@ +negotiation = $negotiation; + } + + /** + * Processes a successful controller into an HTTP 200 response. + * + * Some controllers may not return a response object but simply the body of + * one. The VIEW event is called in that case, to allow us to mutate that + * body into a Response object. In particular we assume that the return + * from an JSON-type response is a JSON string, so just wrap it into a + * Response object. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onView(GetResponseEvent $event) { + + $request = $event->getRequest(); + + $method = 'on' . $this->negotiation->getContentType($request); + + if (method_exists($this, $method)) { + $event->setResponse($this->$method($event)); + } + else { + $event->setResponse(new Response('Unsupported Media Type', 415)); + } + } + + public function onJson(GetResponseEvent $event) { + $page_callback_result = $event->getControllerResult(); + + $response = new JsonResponse(); + $response->setContent($page_callback_result); + + return $response; + } + + public function onAjax(GetResponseEvent $event) { + $page_callback_result = $event->getControllerResult(); + + // Construct the response content from the page callback result. + $commands = ajax_prepare_response($page_callback_result); + $json = ajax_render($commands); + + // Build the actual response object. + $response = new JsonResponse(); + $response->setContent($json); + + return $response; + } + + public function onIframeUpload(GetResponseEvent $event) { + $page_callback_result = $event->getControllerResult(); + + // Construct the response content from the page callback result. + $commands = ajax_prepare_response($page_callback_result); + $json = ajax_render($commands); + + // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification + // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into + // links. This corrupts the JSON response. Protect the integrity of the + // JSON data by making it the value of a textarea. + // @see http://malsup.com/jquery/form/#file-upload + // @see http://drupal.org/node/1009382 + $html = ''; + + return new Response($html); + } + + /** + * Processes a successful controller into an HTTP 200 response. + * + * Some controllers may not return a response object but simply the body of + * one. The VIEW event is called in that case, to allow us to mutate that + * body into a Response object. In particular we assume that the return from + * an HTML-type response is a render array from a legacy page callback and + * render it. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onHtml(GetResponseEvent $event) { + $page_callback_result = $event->getControllerResult(); + return new Response(drupal_render_page($page_callback_result)); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::VIEW][] = array('onView'); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/ExceptionController.php b/core/lib/Drupal/Core/ExceptionController.php new file mode 100644 index 0000000..c52013d --- /dev/null +++ b/core/lib/Drupal/Core/ExceptionController.php @@ -0,0 +1,382 @@ +kernel = $kernel; + $this->negotiation = $negotiation; + } + + /** + * Handles an exception on a request. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request that generated the exception. + * + * @return Symfony\Component\HttpFoundation\Response + * A response object to be sent to the server. + */ + public function execute(FlattenException $exception, Request $request) { + $method = 'on' . $exception->getStatusCode() . $this->negotiation->getContentType($request); + + if (method_exists($this, $method)) { + return $this->$method($exception, $request); + } + + return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode()); + } + + /** + * Processes a MethodNotAllowed exception into an HTTP 405 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on405Html(FlattenException $exception, Request $request) { + $event->setResponse(new Response('Method Not Allowed', 405)); + } + + /** + * Processes an AccessDenied exception into an HTTP 403 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on403Html(FlattenException $exception, Request $request) { + $system_path = $request->attributes->get('system_path'); + watchdog('access denied', $system_path, NULL, WATCHDOG_WARNING); + + $path = drupal_get_normal_path(variable_get('site_403', '')); + if ($path && $path != $system_path) { + // Keep old path for reference, and to allow forms to redirect to it. + if (!isset($_GET['destination'])) { + $_GET['destination'] = $system_path; + } + + $subrequest = Request::create('/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); + + // The active trail is being statically cached from the parent request to + // the subrequest, like any other static. Unfortunately that means the + // data in it is incorrect and does not get regenerated correctly for + // the subrequest. In this instance, that even causes a fatal error in + // some circumstances because menu_get_active_trail() ends up having + // a missing localized_options value. To work around that, reset the + // menu static variables and let them be regenerated as needed. + // @todo It is likely that there are other such statics that need to be + // reset that are not triggering test failures right now. If found, + // add them here. + // @todo Refactor the breadcrumb system so that it does not rely on static + // variables in the first place, which will eliminate the need for this + // hack. + drupal_static_reset('menu_set_active_trail'); + menu_reset_static_cache(); + + $response = $this->kernel->handle($subrequest, DrupalKernel::SUB_REQUEST); + $response->setStatusCode(403, 'Access denied'); + } + else { + $response = new Response('Access Denied', 403); + + // @todo Replace this block with something cleaner. + $return = t('You are not authorized to access this page.'); + drupal_set_title(t('Access denied')); + drupal_set_page_content($return); + $page = element_info('page'); + $content = drupal_render_page($page); + + $response->setContent($content); + } + + return $response; + } + + /** + * Processes a generic exception into an HTTP 500 response. + * + * @param FlattenException $exception + * Metadata about the exception that was thrown. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on500Html(FlattenException $exception, Request $request) { + $error = $this->decodeException($exception); + + // Because the kernel doesn't run until full bootstrap, we know that + // most subsystems are already initialized. + + $headers = array(); + + // When running inside the testing framework, we relay the errors + // to the tested site by the way of HTTP headers. + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + // $number does not use drupal_static as it should not be reset + // as it uniquely identifies each PHP error. + static $number = 0; + $assertion = array( + $error['!message'], + $error['%type'], + array( + 'function' => $error['%function'], + 'file' => $error['%file'], + 'line' => $error['%line'], + ), + ); + $headers['X-Drupal-Assertion-' . $number] = rawurlencode(serialize($assertion)); + $number++; + } + + watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + + // Display the message if the current error reporting level allows this type + // of message to be displayed, and unconditionnaly in update.php. + if (error_displayable($error)) { + $class = 'error'; + + // If error type is 'User notice' then treat it as debug information + // instead of an error message, see dd(). + if ($error['%type'] == 'User notice') { + $error['%type'] = 'Debug'; + $class = 'status'; + } + + drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); + } + + drupal_set_title(t('Error')); + // We fallback to a maintenance page at this point, because the page + // generation itself can generate errors. + $output = theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); + + $response = new Response($output, 500); + $response->setStatusCode(500, '500 Service unavailable (with message)'); + + return $response; + } + + /** + * This method is a temporary port of _drupal_decode_exception(). + * + * @todo This should get refactored. FlattenException could use some + * improvement as well. + * + * @return array + */ + protected function decodeException(FlattenException $exception) { + $message = $exception->getMessage(); + + $backtrace = $exception->getTrace(); + + // This value is missing from the stack for some reason in the + // FlattenException version of the backtrace. + $backtrace[0]['line'] = $exception->getLine(); + + // For database errors, we try to return the initial caller, + // skipping internal functions of the database layer. + if (strpos($exception->getClass(), 'DatabaseExceptionWrapper') !== FALSE) { + // A DatabaseExceptionWrapper exception is actually just a courier for + // the original PDOException. It's the stack trace from that exception + // that we care about. + $backtrace = $exception->getPrevious()->getTrace(); + $backtrace[0]['line'] = $exception->getLine(); + + // The first element in the stack is the call, the second element gives us the caller. + // We skip calls that occurred in one of the classes of the database layer + // or in one of its global functions. + $db_functions = array('db_query', 'db_query_range'); + while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && + ((strpos($caller['namespace'], 'Drupal\Core\Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || + in_array($caller['function'], $db_functions)) { + // We remove that call. + array_shift($backtrace); + } + } + $caller = $this->getLastCaller($backtrace); + + return array( + '%type' => $exception->getClass(), + // The standard PHP exception handler considers that the exception message + // is plain-text. We mimick this behavior here. + '!message' => check_plain($message), + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + 'severity_level' => WATCHDOG_ERROR, + ); + } + + /** + * Gets the last caller from a backtrace. + * + * The last caller is not necessarily the first item in the backtrace. Rather, + * it is the first item in the backtrace that is a PHP userspace function, + * and not one of our debug functions. + * + * @param $backtrace + * A standard PHP backtrace. + * + * @return + * An associative array with keys 'file', 'line' and 'function'. + */ + protected function getLastCaller($backtrace) { + // Ignore black listed error handling functions. + $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); + + // Errors that occur inside PHP internal functions do not generate + // information about file and line. + while (($backtrace && !isset($backtrace[0]['line'])) || + (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { + array_shift($backtrace); + } + + // The first trace is the call itself. + // It gives us the line and the file of the last call. + $call = $backtrace[0]; + + // The second call give us the function where the call originated. + if (isset($backtrace[1])) { + if (isset($backtrace[1]['class'])) { + $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; + } + else { + $call['function'] = $backtrace[1]['function'] . '()'; + } + } + else { + $call['function'] = 'main()'; + } + return $call; + } + + /** + * Processes a NotFound exception into an HTTP 403 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on404Html(FlattenException $exception, Request $request) { + watchdog('page not found', check_plain($request->attributes->get('system_path')), NULL, WATCHDOG_WARNING); + + // Check for and return a fast 404 page if configured. + // @todo Inline this rather than using a function. + drupal_fast_404(); + + $system_path = $request->attributes->get('system_path'); + + // Keep old path for reference, and to allow forms to redirect to it. + if (!isset($_GET['destination'])) { + $_GET['destination'] = $system_path; + } + + $path = drupal_get_normal_path(variable_get('site_404', '')); + if ($path && $path != $system_path) { + // @todo Um, how do I specify an override URL again? Totally not clear. Do + // that and sub-call the kernel rather than using meah(). + // @todo The create() method expects a slash-prefixed path, but we store a + // normal system path in the site_404 variable. + $subrequest = Request::create('/' . $path, 'get', array(), $request->cookies->all(), array(), $request->server->all()); + + $response = $this->kernel->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $response->setStatusCode(404, 'Not Found'); + } + else { + $response = new Response('Not Found', 404); + + // @todo Replace this block with something cleaner. + $return = t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())); + drupal_set_title(t('Page not found')); + drupal_set_page_content($return); + $page = element_info('page'); + $content = drupal_render_page($page); + + $response->setContent($content); + } + + return $response; + } + + /** + * Processes an AccessDenied exception into an HTTP 403 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on403Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(403, 'Access Denied'); + return $response; + } + + /** + * Processes a NotFound exception into an HTTP 404 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on404Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(404, 'Not Found'); + return $response; + } + + /** + * Processes a MethodNotAllowed exception into an HTTP 405 response. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function on405Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(405, 'Method Not Allowed'); + return $response; + } +} diff --git a/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php b/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php index 6971d9f..d877dba 100644 --- a/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php +++ b/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Lock; -use PDOException; +use Drupal\Core\Database\DatabaseExceptionWrapper; /** * Defines the database lock backend. This is the default backend in Drupal. @@ -53,7 +53,7 @@ class DatabaseLockBackend extends LockBackendAbstract { // We never need to try again. $retry = FALSE; } - catch (PDOException $e) { + catch (DatabaseExceptionWrapper $e) { // Suppress the error. If this is our first pass through the loop, // then $retry is FALSE. In this case, the insert must have failed // meaning some other request acquired the lock but did not release it. diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 0cd76e0..1b4abc9 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -44,7 +44,7 @@ abstract class LocalStream implements StreamWrapperInterface { /** * Gets the path that the wrapper is responsible for. - * @TODO: Review this method name in D8 per http://drupal.org/node/701358 + * @todo: Review this method name in D8 per http://drupal.org/node/701358 * * @return string * String specifying the path. diff --git a/core/lib/Drupal/Core/Updater/Updater.php b/core/lib/Drupal/Core/Updater/Updater.php index 2dca5ba..ad2213a 100644 --- a/core/lib/Drupal/Core/Updater/Updater.php +++ b/core/lib/Drupal/Core/Updater/Updater.php @@ -213,7 +213,7 @@ class Updater { $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name); // Run the updates. - // @TODO: decide if we want to implement this. + // @todo: decide if we want to implement this. $this->postUpdate(); // For now, just return a list of links of things to do. @@ -252,7 +252,7 @@ class Updater { $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name); // Potentially enable something? - // @TODO: decide if we want to implement this. + // @todo: decide if we want to implement this. $this->postInstall(); // For now, just return a list of links of things to do. return $this->postInstallTasks(); diff --git a/core/lib/Drupal/Core/UrlMatcher.php b/core/lib/Drupal/Core/UrlMatcher.php new file mode 100644 index 0000000..7d6e24c --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,140 @@ +context = new RequestContext(); + } + + /** + * Sets the request context. + * + * This method is just to satisfy the interface, and is largely vestigial. + * The request context object does not contain the information we need, so + * we will use the original request object. + * + * @param Symfony\Component\Routing\RequestContext $context + * The context. + * + * @api + */ + public function setContext(RequestContext $context) { + $this->context = $context; + } + + /** + * Gets the request context. + * + * This method is just to satisfy the interface, and is largely vestigial. + * The request context object does not contain the information we need, so + * we will use the original request object. + * + * @return Symfony\Component\Routing\RequestContext + * The context. + */ + public function getContext() { + return $this->context; + } + + public function setRequest(Request $request) { + $this->request = $request; + } + + public function getRequest() { + return $this->request; + } + + /** + * {@inheritDoc} + * + * @api + */ + public function match($pathinfo) { + if ($router_item = $this->matchDrupalItem($pathinfo)) { + $ret = $this->convertDrupalItem($router_item); + // Stash the router item in the attributes while we're transitioning. + $ret['drupal_menu_item'] = $router_item; + + // Most legacy controllers (aka page callbacks) are in a separate file, + // so we have to include that. + if ($router_item['include_file']) { + require_once DRUPAL_ROOT . '/' . $router_item['include_file']; + } + + return $ret; + } + + // This matcher doesn't differentiate by method, so don't bother with those + // exceptions. + throw new ResourceNotFoundException(); + } + + /** + * Get a drupal menu item. + * + * @todo Make this return multiple possible candidates for the resolver to + * consider. + * + * @param string $path + * The path being looked up by + */ + protected function matchDrupalItem($path) { + // For now we can just proxy our procedural method. At some point this will + // become more complicated because we'll need to get back candidates for a + // path and them resolve them based on things like method and scheme which + // we currently can't do. + return menu_get_item($path); + } + + protected function convertDrupalItem($router_item) { + $route = array( + '_controller' => $router_item['page_callback'] + ); + + // Place argument defaults on the route. + // @todo For some reason drush test runs have a serialized page_arguments + // but HTTP requests are unserialized. Hack to get around this for now. + // This might be because page arguments aren't unserialized in + // menu_get_item() when the access is denied. + !is_array($router_item['page_arguments']) ? $page_arguments = unserialize($router_item['page_arguments']) : $page_arguments = $router_item['page_arguments']; + foreach ($page_arguments as $k => $v) { + $route[$k] = $v; + } + return $route; + } +} diff --git a/core/modules/aggregator/aggregator.admin.inc b/core/modules/aggregator/aggregator.admin.inc index 09da1cf..ac868b4 100644 --- a/core/modules/aggregator/aggregator.admin.inc +++ b/core/modules/aggregator/aggregator.admin.inc @@ -401,11 +401,9 @@ function _aggregator_parse_opml($opml) { * An object describing the feed to be refreshed. * * @see aggregator_menu() + * @see aggregator_admin_refresh_feed_access() */ function aggregator_admin_refresh_feed($feed) { - if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'aggregator/update/' . $feed->fid)) { - return MENU_ACCESS_DENIED; - } aggregator_refresh($feed); drupal_goto('admin/config/services/aggregator'); } diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module index 0626f72..ee285e2 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -138,7 +138,8 @@ function aggregator_menu() { 'title' => 'Update items', 'page callback' => 'aggregator_admin_refresh_feed', 'page arguments' => array(5), - 'access arguments' => array('administer news feeds'), + 'access callback' => 'aggregator_admin_refresh_feed_access', + 'access arguments' => array(5), 'file' => 'aggregator.admin.inc', ); $items['admin/config/services/aggregator/list'] = array( @@ -796,3 +797,23 @@ function aggregator_preprocess_block(&$variables) { $variables['attributes_array']['role'] = 'complementary'; } } + +/** + * Access callback: Determines if feed refresh is accessible. + * + * @param $feed + * An object describing the feed to be refreshed. + * + * @see aggregator_admin_refresh_feed() + * @see aggregator_menu() + */ +function aggregator_admin_refresh_feed_access($feed) { + if (!user_access('administer news feeds')) { + return FALSE; + } + $token = request()->query->get('token'); + if (!isset($token) || !drupal_valid_token($token, 'aggregator/update/' . $feed->fid)) { + return FALSE; + } + return TRUE; +} diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index ccc307e..9eabe13 100644 --- a/core/modules/comment/comment.admin.inc +++ b/core/modules/comment/comment.admin.inc @@ -260,7 +260,7 @@ function comment_confirm_delete_page($cid) { if ($comment = comment_load($cid)) { return drupal_get_form('comment_confirm_delete', $comment); } - return MENU_NOT_FOUND; + drupal_not_found(); } /** diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 79f2ecb..285d9fe 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -277,7 +277,8 @@ function comment_menu() { 'title' => 'Approve', 'page callback' => 'comment_approve', 'page arguments' => array(1), - 'access arguments' => array('administer comments'), + 'access callback' => 'comment_approve_access', + 'access arguments' => array(1), 'file' => 'comment.pages.inc', 'weight' => 1, ); @@ -2514,3 +2515,23 @@ function comment_file_download_access($field, $entity_type, $entity) { return FALSE; } } + +/** + * Access callback: Determines if comment approval is accessible. + * + * @param $cid + * A comment identifier. + * + * @see comment_approve() + * @see comment_menu() + */ +function comment_approve_access($cid) { + if (!user_access('administer comments')) { + return FALSE; + } + $token = request()->query->get('token'); + if (!isset($token) || !drupal_valid_token($token, "comment/$cid/approve")) { + return FALSE; + } + return TRUE; +} diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc index bac078b..59423ec 100644 --- a/core/modules/comment/comment.pages.inc +++ b/core/modules/comment/comment.pages.inc @@ -105,11 +105,9 @@ function comment_reply(Node $node, $pid = NULL) { * A comment identifier. * * @see comment_menu() + * @see comment_approve_access() */ function comment_approve($cid) { - if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "comment/$cid/approve")) { - return MENU_ACCESS_DENIED; - } if ($comment = comment_load($cid)) { $comment->status = COMMENT_PUBLISHED; comment_save($comment); @@ -117,5 +115,5 @@ function comment_approve($cid) { drupal_set_message(t('Comment approved.')); drupal_goto('node/' . $comment->nid); } - return MENU_NOT_FOUND; + drupal_not_found(); } diff --git a/core/modules/image/image.test b/core/modules/image/image.test index ff783be..84b1296 100644 --- a/core/modules/image/image.test +++ b/core/modules/image/image.test @@ -655,7 +655,7 @@ class ImageFieldDisplayTestCase extends ImageFieldTestCase { // sent by Drupal. $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png; name="' . $test_image->filename . '"', t('Content-Type header was sent.')); $this->assertEqual($this->drupalGetHeader('Content-Disposition'), 'inline; filename="' . $test_image->filename . '"', t('Content-Disposition header was sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'private', t('Cache-Control header was sent.')); + $this->assertTrue(strstr($this->drupalGetHeader('Cache-Control'),'private') !== FALSE, t('Cache-Control header was sent.')); // Log out and try to access the file. $this->drupalLogout(); diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test index 14e3dcb..be2b319 100644 --- a/core/modules/locale/locale.test +++ b/core/modules/locale/locale.test @@ -1941,7 +1941,7 @@ class LocalePathFunctionalTest extends DrupalWebTestCase { // Check that the "xx" front page is readily available because path prefix // negotiation is pre-configured. $this->drupalGet($prefix); - $this->assertText(t('Welcome to Drupal'), t('The "xx" front page is readibly available.')); + $this->assertText(t('Welcome to Drupal'), t('The "xx" front page is readily available.')); // Create a node. $node = $this->drupalCreateNode(array('type' => 'page')); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 8dbc060..a21591e 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1,10 +1,5 @@ \n"; - drupal_add_http_header('Content-Type', 'application/rss+xml; charset=utf-8'); - print $output; + return new Response($output, 200, array('Content-Type' => 'application/rss+xml; charset=utf-8')); } /** diff --git a/core/modules/node/node.test b/core/modules/node/node.test index c908584..ab053d1 100644 --- a/core/modules/node/node.test +++ b/core/modules/node/node.test @@ -1781,11 +1781,8 @@ class NodeFeedTestCase extends DrupalWebTestCase { * Ensure that node_feed accepts and prints extra channel elements. */ function testNodeFeedExtraChannelElements() { - ob_start(); - node_feed(array(), array('copyright' => 'Drupal is a registered trademark of Dries Buytaert.')); - $output = ob_get_clean(); - - $this->assertTrue(strpos($output, 'Drupal is a registered trademark of Dries Buytaert.') !== FALSE); + $response = node_feed(array(), array('copyright' => 'Drupal is a registered trademark of Dries Buytaert.')); + $this->assertTrue(strpos($response->getContent(), 'Drupal is a registered trademark of Dries Buytaert.') !== FALSE); } } diff --git a/core/modules/openid/tests/openid_test.module b/core/modules/openid/tests/openid_test.module index ac49dbd..4481818 100644 --- a/core/modules/openid/tests/openid_test.module +++ b/core/modules/openid/tests/openid_test.module @@ -20,6 +20,9 @@ * key is used for verifying the signed messages from the provider. */ +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; + /** * Implements hook_menu(). */ @@ -97,8 +100,7 @@ function openid_test_yadis_xrds() { if (arg(3) == 'xri' && (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml')) { drupal_not_found(); } - drupal_add_http_header('Content-Type', 'application/xrds+xml'); - print ' + $output = ' @@ -127,7 +129,7 @@ function openid_test_yadis_xrds() { '; if (arg(3) == 'server') { - print ' + $output .= ' http://specs.openid.net/auth/2.0/server http://example.com/this-has-too-low-priority @@ -138,7 +140,7 @@ function openid_test_yadis_xrds() { '; } elseif (arg(3) == 'delegate') { - print ' + $output .= ' http://specs.openid.net/auth/2.0/signon http://openid.net/srv/ax/1.0 @@ -146,9 +148,10 @@ function openid_test_yadis_xrds() { http://example.com/xrds-delegate '; } - print ' + $output .= ' '; + return new Response($output, 200, array('Content-type' => 'application/xrds+xml; charset=utf-8')); } else { return t('This is a regular HTML page. If the client sends an Accept: application/xrds+xml header when requesting this URL, an XRDS document is returned.'); @@ -207,11 +210,9 @@ function openid_test_html_openid2() { function openid_test_endpoint() { switch ($_REQUEST['openid_mode']) { case 'associate': - _openid_test_endpoint_associate(); - break; + return _openid_test_endpoint_associate(); case 'checkid_setup': - _openid_test_endpoint_authenticate(); - break; + return _openid_test_endpoint_authenticate(); } } @@ -226,8 +227,7 @@ function openid_test_redirect($count = 0) { $url = url('openid-test/redirect/' . --$count, array('absolute' => TRUE)); } $http_response_code = variable_get('openid_test_redirect_http_reponse_code', 301); - header('Location: ' . $url, TRUE, $http_response_code); - exit(); + return new RedirectResponse($url, $http_response_code); } /** @@ -283,8 +283,7 @@ function _openid_test_endpoint_associate() { // Respond to Relying Party in the special Key-Value Form Encoding (see OpenID // Authentication 1.0, section 4.1.1). - drupal_add_http_header('Content-Type', 'text/plain'); - print _openid_create_message($response); + return new Response(_openid_create_message($response), 200, array('Content-Type' => 'text/plain')); } /** @@ -306,9 +305,7 @@ function _openid_test_endpoint_authenticate() { 'openid.mode' => 'error', 'openid.error' => 'Unexpted identity', ); - drupal_add_http_header('Content-Type', 'text/plain'); - header('Location: ' . url($_REQUEST['openid_return_to'], array('query' => $response, 'external' => TRUE))); - return; + return new RedirectResponse(url($_REQUEST['openid_return_to'], array('query' => $response, 'external' => TRUE))); } // Generate unique identifier for this authentication. @@ -348,8 +345,7 @@ function _openid_test_endpoint_authenticate() { // Put the signed message into the query string of a URL supplied by the // Relying Party, and redirect the user. - drupal_add_http_header('Content-Type', 'text/plain'); - header('Location: ' . url($_REQUEST['openid_return_to'], array('query' => $response, 'external' => TRUE))); + return new RedirectResponse(url($_REQUEST['openid_return_to'], array('query' => $response, 'external', TRUE))); } /** diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index 02c0883..2628260 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -5,6 +5,8 @@ * Displays the Drupal administration interface in an overlay. */ +use Symfony\Component\HttpFoundation\Response; + /** * Implements hook_help(). */ @@ -19,7 +21,7 @@ function overlay_help($path, $arg) { } /** - * Implements hook_menu(). + * Implements hook_menu() */ function overlay_menu() { $items['overlay-ajax/%'] = array( @@ -32,7 +34,7 @@ function overlay_menu() { $items['overlay/dismiss-message'] = array( 'title' => '', 'page callback' => 'overlay_user_dismiss_message', - 'access arguments' => array('access overlay'), + 'access callback' => 'overlay_user_dismiss_message_access', 'type' => MENU_CALLBACK, ); return $items; @@ -299,25 +301,44 @@ function overlay_page_alter(&$page) { } /** - * Menu callback; dismisses the overlay accessibility message for this user. + * Access callback; determines access to dismiss the overlay accessibility message. + * + * @see overlay_user_dismiss_message() + * @see overlay_menu() */ -function overlay_user_dismiss_message() { +function overlay_user_dismiss_message_access() { global $user; + if (!user_access('access overlay')) { + return FALSE; + } // It's unlikely, but possible that "access overlay" permission is granted to // the anonymous role. In this case, we do not display the message to disable - // the overlay, so there is nothing to dismiss. Also, protect against - // cross-site request forgeries by validating a token. - if (empty($user->uid) || !isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'overlay')) { - return MENU_ACCESS_DENIED; + // the overlay, so there is nothing to dismiss. + if (empty($user->uid)) { + return FALSE; } - else { - $account = user_load($user->uid); - $account->data['overlay_message_dismissed'] = 1; - $account->save(); - drupal_set_message(t('The message has been dismissed. You can change your overlay settings at any time by visiting your profile page.')); - // Destination is normally given. Go to the user profile as a fallback. - drupal_goto('user/' . $user->uid . '/edit'); + // Protect against cross-site request forgeries by validating a token. + $token = request()->query->get('token'); + if (!isset($token) || !drupal_valid_token($token, 'overlay')) { + return FALSE; } + return TRUE; +} + +/** + * Menu callback; dismisses the overlay accessibility message for this user. + * + * @see overlay_user_dismiss_message_access() + * @see overlay_menu() + */ +function overlay_user_dismiss_message() { + global $user; + $account = user_load($user->uid); + $account->data['overlay_message_dismissed'] = 1; + $account->save(); + drupal_set_message(t('The message has been dismissed. You can change your overlay settings at any time by visiting your profile page.')); + // Destination is normally given. Go to the user profile as a fallback. + drupal_goto('user/' . $user->uid . '/edit'); } /** @@ -667,7 +688,8 @@ function overlay_overlay_child_initialize() { // it to the same content rendered in overlay_exit(), at the end of the page // request. This allows us to check if anything actually did change, and, if // so, trigger an immediate Ajax refresh of the parent window. - if (!empty($_POST) || isset($_GET['token'])) { + $token = request()->query->get('token'); + if (!empty($_POST) || isset($token)) { foreach (overlay_supplemental_regions() as $region) { overlay_store_rendered_content($region, overlay_render_region($region)); } @@ -979,5 +1001,5 @@ function overlay_trigger_refresh() { * @see Drupal.overlay.refreshRegions() */ function overlay_ajax_render_region($region) { - print overlay_render_region($region); + return new Response(overlay_render_region($region)); } diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index 8b85dfc..8e59c54 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -1895,6 +1895,7 @@ class DrupalWebTestCase extends DrupalTestCase { * Retrieve a Drupal path or an absolute path and JSON decode the result. */ protected function drupalGetAJAX($path, array $options = array(), array $headers = array()) { + $headers[] = 'X-Requested-With: XMLHttpRequest'; return drupal_json_decode($this->drupalGet($path, $options, $headers)); } @@ -2102,6 +2103,7 @@ class DrupalWebTestCase extends DrupalTestCase { } $content = $this->content; $drupal_settings = $this->drupalSettings; + $headers[] = 'X-Requested-With: XMLHttpRequest'; // Get the Ajax settings bound to the triggering element. if (!isset($ajax_settings)) { diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index dcc289a..075129d 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -5,6 +5,8 @@ * Admin page callbacks for the system module. */ +use Symfony\Component\HttpFoundation\Response; + /** * Menu callback; Provide the administration overview page. */ @@ -2268,6 +2270,9 @@ function system_batch_page() { if ($output === FALSE) { drupal_access_denied(); } + elseif ($output instanceof Response) { + return $output; + } elseif (isset($output)) { // Force a page without blocks or messages to // display a list of collected messages later. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index f26bb07..85f41c4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -5,6 +5,8 @@ * Configuration system that lets administrators modify the workings of the site. */ +use Symfony\Component\HttpFoundation\Response; + /** * Maximum age of temporary files in seconds. */ @@ -1119,11 +1121,13 @@ function system_menu() { * * @see system_cron_access(). */ + function system_cron_page() { drupal_page_is_cacheable(FALSE); drupal_cron_run(); - // Returning nothing causes no output to be generated. + // HTTP 204 is "No content", meaning "I did what you asked and we're done." + return new Response('a', 204); } /** @@ -2013,8 +2017,11 @@ function system_add_module_assets() { * Implements hook_custom_theme(). */ function system_custom_theme() { - if (user_access('view the administration theme') && path_is_admin(current_path())) { - return variable_get('admin_theme'); + if ($request = request()) { + $path = $request->attributes->get('system_path'); + if (user_access('view the administration theme') && path_is_admin($path)) { + return variable_get('admin_theme'); + } } } diff --git a/core/modules/system/system.test b/core/modules/system/system.test index 30a270d..a938ea1 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -819,7 +819,7 @@ class CronRunTestCase extends DrupalWebTestCase { // Run cron anonymously with the valid cron key. $key = variable_get('cron_key', 'drupal'); $this->drupalGet('cron/' . $key); - $this->assertResponse(200); + $this->assertResponse(204); } /** diff --git a/core/modules/system/tests/bootstrap.test b/core/modules/system/tests/bootstrap.test index c305a06..7535617 100644 --- a/core/modules/system/tests/bootstrap.test +++ b/core/modules/system/tests/bootstrap.test @@ -191,7 +191,7 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, t('Vary: Cookie header was not sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', t('Cache-Control header was sent.')); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', t('Cache-Control header was sent.')); $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); diff --git a/core/modules/system/tests/error.test b/core/modules/system/tests/error.test index ead3526..e05cef4 100644 --- a/core/modules/system/tests/error.test +++ b/core/modules/system/tests/error.test @@ -72,14 +72,14 @@ class DrupalErrorHandlerUnitTest extends DrupalWebTestCase { '%type' => 'Exception', '!message' => 'Drupal is awesome', '%function' => 'error_test_trigger_exception()', - '%line' => 57, + '%line' => 56, '%file' => drupal_get_path('module', 'error_test') . '/error_test.module', ); $error_pdo_exception = array( - '%type' => 'PDOException', + '%type' => 'DatabaseExceptionWrapper', '!message' => 'SELECT * FROM bananas_are_awesome', '%function' => 'error_test_trigger_pdo_exception()', - '%line' => 65, + '%line' => 64, '%file' => drupal_get_path('module', 'error_test') . '/error_test.module', ); diff --git a/core/modules/system/tests/modules/error_test/error_test.info b/core/modules/system/tests/modules/error_test/error_test.info index d5db3ee..d00075d 100644 --- a/core/modules/system/tests/modules/error_test/error_test.info +++ b/core/modules/system/tests/modules/error_test/error_test.info @@ -3,4 +3,4 @@ description = "Support module for error and exception testing." package = Testing version = VERSION core = 8.x -hidden = TRUE +hidden = FALSE diff --git a/core/modules/system/tests/xmlrpc.test b/core/modules/system/tests/xmlrpc.test index 60b9624..4442211 100644 --- a/core/modules/system/tests/xmlrpc.test +++ b/core/modules/system/tests/xmlrpc.test @@ -17,6 +17,8 @@ class XMLRPCBasicTestCase extends DrupalWebTestCase { * Ensure that a basic XML-RPC call with no parameters works. */ protected function testListMethods() { + global $base_url; + // Minimum list of methods that should be included. $minimum = array( 'system.multicall', @@ -27,7 +29,7 @@ class XMLRPCBasicTestCase extends DrupalWebTestCase { ); // Invoke XML-RPC call to get list of methods. - $url = url(NULL, array('absolute' => TRUE)) . 'core/xmlrpc.php'; + $url = $base_url . '/core/xmlrpc.php'; $methods = xmlrpc($url, array('system.listMethods' => array())); // Ensure that the minimum methods were found. @@ -45,7 +47,9 @@ class XMLRPCBasicTestCase extends DrupalWebTestCase { * Ensure that system.methodSignature returns an array of signatures. */ protected function testMethodSignature() { - $url = url(NULL, array('absolute' => TRUE)) . 'core/xmlrpc.php'; + global $base_url; + + $url = $base_url . '/core/xmlrpc.php'; $signature = xmlrpc($url, array('system.methodSignature' => array('system.listMethods'))); $this->assert(is_array($signature) && !empty($signature) && is_array($signature[0]), t('system.methodSignature returns an array of signature arrays.')); @@ -97,7 +101,8 @@ class XMLRPCValidator1IncTestCase extends DrupalWebTestCase { * Run validator1 tests. */ function testValidator1() { - $xml_url = url(NULL, array('absolute' => TRUE)) . 'core/xmlrpc.php'; + global $base_url; + $xml_url = $base_url . '/core/xmlrpc.php'; srand(); mt_srand(); @@ -211,7 +216,9 @@ class XMLRPCMessagesTestCase extends DrupalWebTestCase { * Make sure that XML-RPC can transfer large messages. */ function testSizedMessages() { - $xml_url = url(NULL, array('absolute' => TRUE)) . 'core/xmlrpc.php'; + global $base_url; + + $xml_url = $base_url . '/core/xmlrpc.php'; $sizes = array(8, 80, 160); foreach ($sizes as $size) { $xml_message_l = xmlrpc_test_message_sized_in_kb($size); @@ -225,10 +232,11 @@ class XMLRPCMessagesTestCase extends DrupalWebTestCase { * Ensure that hook_xmlrpc_alter() can hide even builtin methods. */ protected function testAlterListMethods() { + global $base_url; // Ensure xmlrpc_test_xmlrpc_alter() is disabled and retrieve regular list of methods. variable_set('xmlrpc_test_xmlrpc_alter', FALSE); - $url = url(NULL, array('absolute' => TRUE)) . 'core/xmlrpc.php'; + $url = $base_url . '/core/xmlrpc.php'; $methods1 = xmlrpc($url, array('system.listMethods' => array())); // Enable the alter hook and retrieve the list of methods again. diff --git a/core/modules/taxonomy/taxonomy.test b/core/modules/taxonomy/taxonomy.test index 93b86f9..428c8ab 100644 --- a/core/modules/taxonomy/taxonomy.test +++ b/core/modules/taxonomy/taxonomy.test @@ -767,9 +767,8 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { $path = 'taxonomy/autocomplete/taxonomy_'; $path .= $this->vocabulary->machine_name . '/' . $input; // The result order is not guaranteed, so check each term separately. - $url = url($path, array('absolute' => TRUE)); - $result = drupal_http_request($url); - $data = drupal_json_decode($result->data); + $result = $this->drupalGet($path); + $data = drupal_json_decode($result); $this->assertEqual($data[$first_term->name], check_plain($first_term->name), 'Autocomplete returned the first matching term'); $this->assertEqual($data[$second_term->name], check_plain($second_term->name), 'Autocomplete returned the second matching term'); diff --git a/core/modules/update/tests/modules/update_test/update_test.module b/core/modules/update/tests/modules/update_test/update_test.module index ff5aad5..b40f274 100644 --- a/core/modules/update/tests/modules/update_test/update_test.module +++ b/core/modules/update/tests/modules/update_test/update_test.module @@ -1,5 +1,8 @@ 'text/xml; charset=utf-8')); + } + return new StreamedResponse(function() use ($file) { + // Transfer file in 1024 byte chunks to save memory usage. + if ($fd = fopen($file, 'rb')) { + while (!feof($fd)) { + print fread($fd, 1024); + } + fclose($fd); + } + }, 200, array('Content-Type' => 'text/xml; charset=utf-8')); } /** diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index 6de781d..06fe7f7 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -142,7 +142,7 @@ function _update_process_fetch_task($project) { $project_name = $project['name']; if (empty($fail[$fetch_url_base]) || $fail[$fetch_url_base] < $max_fetch_attempts) { - $xml = drupal_http_request($url); + $xml = drupal_http_request($url, array('headers' => array('accept' => 'text/xml'))); if (!isset($xml->error) && isset($xml->data)) { $data = $xml->data; } diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 836f101..ee2a711 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -2325,9 +2325,12 @@ function user_delete_multiple(array $uids) { * Page callback wrapper for user_view(). */ function user_view_page($account) { + if (is_object($account)) { + return user_view($account); + } // An administrator may try to view a non-existent account, // so we give them a 404 (versus a 403 for non-admins). - return is_object($account) ? user_view($account) : MENU_NOT_FOUND; + drupal_not_found(); } /** diff --git a/core/update.php b/core/update.php index 9797833..ab050a8 100644 --- a/core/update.php +++ b/core/update.php @@ -14,6 +14,9 @@ * back to its original state! */ +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + // Change the directory to the Drupal root. chdir('..'); @@ -391,11 +394,24 @@ $default = language_default(); drupal_container()->register(LANGUAGE_TYPE_INTERFACE, 'Drupal\\Core\\Language\\Language') ->addMethodCall('extend', array($default)); +// A request object from the HTTPFoundation to tell us about the request. +// @todo These two lines were copied from index.php which has its own todo about +// a change required here. Revisit this when that change has been made. +$request = Request::createFromGlobals(); +request($request); + +// There can be conflicting 'op' parameters because both update and batch use +// this parameter name. We need the 'op' coming from a POST request to trump +// that coming from a GET request. +$op = $request->request->get('op'); +if (is_null($op)) { + $op = $request->query->get('op'); +} + // Only allow the requirements check to proceed if the current user has access // to run updates (since it may expose sensitive information about the site's // configuration). -$op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; -if (empty($op) && update_access_allowed()) { +if (is_null($op) && update_access_allowed()) { require_once DRUPAL_ROOT . '/core/includes/install.inc'; require_once DRUPAL_ROOT . '/core/modules/system/system.install'; @@ -423,23 +439,16 @@ if (empty($op) && update_access_allowed()) { install_goto('core/update.php?op=info'); } -// update_fix_d8_requirements() needs to run before bootstrapping beyond path. -// So bootstrap to DRUPAL_BOOTSTRAP_LANGUAGE then include unicode.inc. - -drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE); -include_once DRUPAL_ROOT . '/core/includes/unicode.inc'; - -update_fix_d8_requirements(); - -// Now proceed with a full bootstrap. - +// Bootstrap, fix requirements, and set the maintenance theme. drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); +update_fix_d8_requirements(); drupal_maintenance_theme(); // Turn error reporting back on. From now on, only fatal errors (which are // not passed through the error handler) will cause a message to be printed. ini_set('display_errors', TRUE); + // Only proceed with updates if the user is allowed to run them. if (update_access_allowed()) { @@ -453,27 +462,29 @@ if (update_access_allowed()) { // no errors, skip reporting them if the user has provided a URL parameter // acknowledging the warnings and indicating a desire to continue anyway. See // drupal_requirements_url(). - $skip_warnings = !empty($_GET['continue']); + $continue = $request->query->get('continue'); + $skip_warnings = !empty($continue); update_check_requirements($skip_warnings); - $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; switch ($op) { // update.php ops. case 'selection': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + $token = $request->query->get('token'); + if (isset($token) && drupal_valid_token($token, 'update')) { $output = update_selection_page(); break; } case 'Apply pending updates': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + $token = $request->query->get('token'); + if (isset($token) && drupal_valid_token($token, 'update')) { // Generate absolute URLs for the batch processing (using $base_root), // since the batch API will pass them to url() which does not handle // update.php correctly by default. $batch_url = $base_root . drupal_current_script_url(); $redirect_url = $base_root . drupal_current_script_url(array('op' => 'results')); - update_batch($_POST['start'], $redirect_url, $batch_url); + update_batch($request->request->get('start'), $redirect_url, $batch_url); break; } @@ -500,5 +511,11 @@ if (isset($output) && $output) { drupal_session_start(); // We defer the display of messages until all updates are done. $progress_page = ($batch = batch_get()) && isset($batch['running']); - print theme('update_page', array('content' => $output, 'show_messages' => !$progress_page)); + if ($output instanceof Response) { + $output->send(); + } + else { + print theme('update_page', array('content' => $output, 'show_messages' => !$progress_page)); + } + } diff --git a/index.php b/index.php index b91fb1e..8e2c882 100644 --- a/index.php +++ b/index.php @@ -11,11 +11,34 @@ * See COPYRIGHT.txt and LICENSE.txt files in the "core" directory. */ +use Drupal\Core\DrupalKernel; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpKernel\Controller\ControllerResolver; + /** * Root directory of Drupal installation. */ define('DRUPAL_ROOT', getcwd()); - +// Bootstrap the lowest level of what we need. require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; -drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); -menu_execute_active_handler(); +drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + +// Create a request object from the HTTPFoundation. +$request = Request::createFromGlobals(); + +// Set the global $request object. This is a temporary measure to keep legacy +// utility functions working. It should be moved to a dependency injection +// container at some point. +request($request); + +drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE); + +$dispatcher = new EventDispatcher(); +$resolver = new ControllerResolver(); + +$kernel = new DrupalKernel($dispatcher, $resolver); +$response = $kernel->handle($request); +$response->prepare($request); +$response->send(); +$kernel->terminate($request, $response);