diff --git a/.htaccess b/.htaccess index a69bdd4..f0c5d96 100644 --- a/.htaccess +++ b/.htaccess @@ -99,8 +99,7 @@ DirectoryIndex index.php index.html index.htm # Redirect common PHP files to their new locations. RewriteCond %{REQUEST_URI} ^(.*)?/(update.php) [OR] - RewriteCond %{REQUEST_URI} ^(.*)?/(install.php) [OR] - RewriteCond %{REQUEST_URI} ^(.*)?/(cron.php) + RewriteCond %{REQUEST_URI} ^(.*)?/(install.php) RewriteCond %{REQUEST_URI} !core RewriteRule ^ %1/core/%2 [L,QSA,R=301] @@ -109,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/INSTALL.txt b/core/INSTALL.txt index 5c0c7b2..8772d13 100644 --- a/core/INSTALL.txt +++ b/core/INSTALL.txt @@ -273,10 +273,10 @@ INSTALLATION It is also possible to run the cron tasks independent of site visits; this is recommended for most sites. To do this, you will need to set up an automated - process to visit the page cron.php on your site, which executes the cron + process to visit the page /cron on your site, which executes the cron tasks. - The URL of the cron.php page requires a "cron key" to protect against + The URL of the cron page requires a "cron key" to protect against unauthorized access. Your site's cron key is automatically generated during installation and is specific to your site. The full URL of the page, with the cron key, is available in the "Cron maintenance tasks" section of the Status @@ -284,11 +284,11 @@ INSTALLATION As an example for how to set up this automated process, you can use the crontab utility on Unix/Linux systems. The following crontab line uses the - wget command to visit the cron.php page, and runs each hour, on the hour: + wget command to visit the cron page, and runs each hour, on the hour: - 0 * * * * wget -O - -q -t 1 http://example.com/core/cron.php?cron_key=YOURKEY + 0 * * * * wget -O - -q -t 1 http://example.com/cron?cron_key=YOURKEY - Replace the text "http://example.com/core/cron.php?cron_key=YOURKEY" in the + Replace the text "http://example.com/cron?cron_key=YOURKEY" in the example with the full URL displayed under "Cron maintenance tasks" on the "Status report" page. diff --git a/core/cron.php b/core/cron.php deleted file mode 100644 index fa9aa14..0000000 --- a/core/cron.php +++ /dev/null @@ -1,29 +0,0 @@ - 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 6abe08a..6d37ff6 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 Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; /** * @file @@ -630,7 +631,7 @@ function drupal_settings_initialize() { // be modified by a visitor. if ($dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')) { // Remove "core" directory if present, allowing install.php, update.php, - // cron.php and others to auto-detect a base path. + // and others to auto-detect a base path. $core_position = strrpos($dir, '/core'); if ($core_position !== FALSE && strlen($dir) - 5 == $core_position) { $base_path = substr($dir, 0, $core_position); @@ -1511,6 +1512,27 @@ function request_uri() { } /** + * Returns the current global reuqest object. + * + * @todo Replace this function with a proper dependency injection container. + * + * @staticvar Request $request + * @param Request $new_request + * The new request object to store. If you are not index.php, you probably + * should not be using this parameter. + * @return 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 diff --git a/core/includes/common.inc b/core/includes/common.inc index 353a9b5..ae256c6 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ $base_root . request_uri(), 'data' => array( 'path' => $_GET['q'], - 'body' => ob_get_clean(), + 'body' => $response_body, 'title' => drupal_get_title(), 'headers' => array(), ), diff --git a/core/includes/file.inc b/core/includes/file.inc index c4356a9..7c38429 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 125852a..5adfea3 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, we will 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/includes/menu.inc b/core/includes/menu.inc index 96791e3..cccc217 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -3813,7 +3813,7 @@ function _menu_site_is_offline($check_only = FALSE) { // Ensure that the maintenance mode message is displayed only once // (allowing for page redirects) and specifically suppress its display on // the maintenance mode settings page. - if (!$check_only && $_GET['q'] != 'admin/config/development/maintenance') { + if (!$check_only && request()->attributes->get('system_path') != 'admin/config/development/maintenance') { if (user_access('administer site configuration')) { drupal_set_message(t('Operating in maintenance mode. Go online.', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE); } diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php new file mode 100644 index 0000000..b87b33c --- /dev/null +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -0,0 +1,53 @@ +. + 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/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php new file mode 100644 index 0000000..26ff906 --- /dev/null +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -0,0 +1,88 @@ +dispatcher = $dispatcher; + $this->resolver = $resolver; + + $context = new RequestContext(); + $this->matcher = new UrlMatcher($context); + $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 LegacyControllerSubscriber()); + $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..04eac0a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -0,0 +1,54 @@ +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/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/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php new file mode 100644 index 0000000..2befe92 --- /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/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php new file mode 100644 index 0000000..dc5059b --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -0,0 +1,68 @@ +getRequest(); + + $path = ltrim($request->getPathInfo(), '/'); + + if (empty($path)) { + // @todo Temporary hack. Fix when configuration is injectable. + $path = variable_get('site_frontpage', 'user'); + } + $system_path = drupal_get_normal_path($path); + + $request->attributes->set('system_path', $system_path); + + // @todo Remove this line. + // Drupal uses $_GET['q'] directly in over 100 places at present, + // including writing back to it at times. Those are all critical bugs, + // even by Drupal 7 standards, but as many of the places that it does so + // are slated to be rewritten anyway we will save time and include this + // temporary hack. Removal of this line is a critical, Drupal-release + // blocking bug. + $_GET['q'] = $system_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('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..a6f9ad8 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php @@ -0,0 +1,66 @@ +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/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php new file mode 100644 index 0000000..647b641 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -0,0 +1,131 @@ +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 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(); + + //print_r($page_callback_result); + + $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 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..3e92e83 --- /dev/null +++ b/core/lib/Drupal/Core/ExceptionController.php @@ -0,0 +1,211 @@ +kernel = $kernel; + $this->negotiation = $negotiation; + } + + /** + * Handles an exception on a request. + * + * @param FlattenException $exception + * The flattened exception. + * @param 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 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 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()); + + $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 NotFound exception into an HTTP 403 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function on404Html(FlattenException $exception, Request $request) { + watchdog('page not found', check_plain($_GET['q']), 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 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 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 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/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..585fc45 --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,111 @@ +context = $context; + } + + /** + * {@inheritDoc} + * + * @api + */ + public function match($pathinfo) { + + $this->allow = array(); + + // Symfony uses a prefixing / but we don't yet. + $dpathinfo = ltrim($pathinfo, '/'); + + // Do our fancy frontpage logic. + if (empty($dpathinfo)) { + $dpathinfo = variable_get('site_frontpage', 'user'); + $pathinfo = '/' . $dpathinfo; + } + + if ($router_item = $this->matchDrupalItem($dpathinfo)) { + $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; + return new Route($router_item['href'], $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 caac279..5efc773 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/aggregator/aggregator.test b/core/modules/aggregator/aggregator.test index 61ad16b..8379325 100644 --- a/core/modules/aggregator/aggregator.test +++ b/core/modules/aggregator/aggregator.test @@ -801,11 +801,11 @@ class AggregatorCronTestCase extends AggregatorTestCase { $key = variable_get('cron_key', 'drupal'); $this->createSampleNodes(); $feed = $this->createFeed(); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->cronRun(); $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); $this->removeFeedItems($feed); $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->cronRun(); $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); // Test feed locking when queued for update. @@ -816,7 +816,7 @@ class AggregatorCronTestCase extends AggregatorTestCase { 'queued' => REQUEST_TIME, )) ->execute(); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->cronRun(); $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); db_update('aggregator_feed') ->condition('fid', $feed->fid) @@ -824,7 +824,7 @@ class AggregatorCronTestCase extends AggregatorTestCase { 'queued' => 0, )) ->execute(); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->cronRun(); $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); } } diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index d84b785..9758075 100644 --- a/core/modules/comment/comment.admin.inc +++ b/core/modules/comment/comment.admin.inc @@ -258,7 +258,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 0302913..d821316 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, ); @@ -2516,3 +2517,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 d3b3374..9e0bf6e 100644 --- a/core/modules/image/image.test +++ b/core/modules/image/image.test @@ -210,7 +210,7 @@ class ImageStylesPathAndUrlUnitTest extends DrupalWebTestCase { $generate_url = image_style_url($this->style_name, $original_uri); if (!$clean_url) { - $this->assertTrue(strpos($generate_url, '?q=') !== FALSE, 'When using non-clean URLS, the system path contains the query string.'); + $this->assertTrue(strpos($generate_url, 'index.php') !== FALSE, 'When using non-clean URLS, the system path contains the query string.'); } // Fetch the URL that generates the file. @@ -651,7 +651,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/node/node.module b/core/modules/node/node.module index 5e1c3b3..40163ab 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 655bc0b..9010665 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 5bd2f4d..7f50d9a 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(). */ @@ -108,8 +111,7 @@ function openid_test_yadis_xrds() { } } } - drupal_add_http_header('Content-Type', 'application/xrds+xml'); - print ' + $output = ' @@ -138,7 +140,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 @@ -149,7 +151,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 @@ -157,9 +159,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.'); @@ -218,11 +221,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(); } } @@ -237,8 +238,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); } /** @@ -294,8 +294,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')); } /** @@ -317,9 +316,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. @@ -359,8 +356,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 e2ecd19..fc93859 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'); } /** @@ -669,7 +690,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)); } @@ -981,5 +1003,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 540dc0e..fc7e843 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -1836,6 +1836,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)); } @@ -2043,6 +2044,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)) { @@ -2186,7 +2188,7 @@ class DrupalWebTestCase extends DrupalTestCase { * Runs cron in the Drupal installed by Simpletest. */ protected function cronRun() { - $this->drupalGet($GLOBALS['base_url'] . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))); + $this->drupalGet('cron', array('query' => array('cron_key' => variable_get('cron_key', 'drupal')))); } /** diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index aac0c3d..34b3798 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. */ @@ -2357,6 +2359,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.install b/core/modules/system/system.install index 51603ff..22ff585 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -283,7 +283,7 @@ function system_requirements($phase) { } $description .= ' ' . $t('You can run cron manually.', array('@cron' => url('admin/reports/status/run-cron'))); - $description .= '
' . $t('To run cron from outside the site, go to !cron', array('!cron' => url($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))))); + $description .= '
' . $t('To run cron from outside the site, go to !cron', array('!cron' => url('cron', array('query' => array('cron_key' => variable_get('cron_key', 'drupal')))))); $requirements['cron'] = array( 'title' => $t('Cron maintenance tasks'), diff --git a/core/modules/system/system.module b/core/modules/system/system.module index dfb8936..1269729 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. */ @@ -580,6 +582,12 @@ function system_element_info() { * Implements hook_menu(). */ function system_menu() { + $items['cron'] = array( + 'title' => 'Run cron', + 'page callback' => 'system_cron_callback', + 'access callback' => 'system_cron_access', + 'type' => MENU_CALLBACK, + ); $items['system/files'] = array( 'title' => 'File download', 'page callback' => 'file_download', @@ -1129,6 +1137,36 @@ function system_menu() { } /** + * Page callback; Execute cron tasks. + * + * @see system_cron_access(). + */ +function system_cron_callback() { + drupal_cron_run(); + + // HTTP 204 is "No content", meaning "I did what you asked and we're done." + return new Response('a', 204); +} + +/** + *Access callback for system_cron(). + * + * @see system_cron_callback(). + */ +function system_cron_access() { + if (request()->get('cron_key') != variable_get('cron_key', 'drupal')) { + watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE); + return FALSE; + } + elseif (variable_get('maintenance_mode', 0)) { + watchdog('cron', 'Cron could not run because the site is in maintenance mode.', array(), WATCHDOG_NOTICE); + return FALSE; + } + + return TRUE; +} + +/** * Theme callback for the default batch page. */ function _system_batch_theme() { @@ -1995,8 +2033,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 = ltrim($request->getPathInfo(), '/'); + 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 16348cc..f3ba2e3 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -811,18 +811,18 @@ class CronRunTestCase extends DrupalWebTestCase { global $base_url; // Run cron anonymously without any cron key. - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE)); + $this->drupalGet('cron'); $this->assertResponse(403); // Run cron anonymously with a random cron key. $key = $this->randomName(16); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->drupalGet('cron', array('query' => array('cron_key' => $key))); $this->assertResponse(403); // Run cron anonymously with the valid cron key. $key = variable_get('cron_key', 'drupal'); - $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); - $this->assertResponse(200); + $this->drupalGet('cron', array('query' => array('cron_key' => $key))); + $this->assertResponse(204); } /** @@ -2900,6 +2900,6 @@ class SystemIndexPhpTest extends DrupalWebTestCase { $this->assertResponse(200, t('Make sure index.php?q=user returns a valid page.')); $this->drupalGet($index_php .'/user', array('external' => TRUE)); - $this->assertResponse(404, t("Make sure index.php/user returns a 'page not found'.")); + $this->assertResponse(200, t("Make sure index.php/user returns a valid page.")); } } diff --git a/core/modules/system/tests/file.test b/core/modules/system/tests/file.test index 173d05a..ac0418c 100644 --- a/core/modules/system/tests/file.test +++ b/core/modules/system/tests/file.test @@ -2436,7 +2436,7 @@ class FileDownloadTest extends FileTestCase { $this->checkUrl('public', '', $basename, $base_url . '/' . file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/' . $basename_encoded); $this->checkUrl('private', '', $basename, $base_url . '/system/files/' . $basename_encoded); - $this->checkUrl('private', '', $basename, $base_url . '/?q=system/files/' . $basename_encoded, '0'); + $this->checkUrl('private', '', $basename, $base_url . '/index.php/system/files/' . $basename_encoded, '0'); } /** 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 5c16884..7303ecd 100644 --- a/core/modules/taxonomy/taxonomy.test +++ b/core/modules/taxonomy/taxonomy.test @@ -765,9 +765,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/user/user.module b/core/modules/user/user.module index e6fa2dd..e6e1e1c 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/scripts/cron-curl.sh b/core/scripts/cron-curl.sh index 71f06b9..b36da12 100644 --- a/core/scripts/cron-curl.sh +++ b/core/scripts/cron-curl.sh @@ -1,3 +1,3 @@ #!/bin/sh -curl --silent --compressed http://example.com/core/cron.php +curl --silent --compressed http://example.com/cron diff --git a/core/scripts/cron-lynx.sh b/core/scripts/cron-lynx.sh index 36880d2..516ba65 100644 --- a/core/scripts/cron-lynx.sh +++ b/core/scripts/cron-lynx.sh @@ -1,3 +1,3 @@ #!/bin/sh -/usr/bin/lynx -source http://example.com/core/cron.php > /dev/null 2>&1 +/usr/bin/lynx -source http://example.com/cron > /dev/null 2>&1 diff --git a/core/scripts/drupal.sh b/core/scripts/drupal.sh index cf17e68..4822d25 100755 --- a/core/scripts/drupal.sh +++ b/core/scripts/drupal.sh @@ -43,8 +43,8 @@ All arguments are long options. If the given path and file exists it will be executed directly, i.e. if URI is set to http://default/bar/foo.php and bar/foo.php exists, this script will be executed without - bootstrapping Drupal. To execute Drupal's cron.php, specify - http://default/core/cron.php as the URI. + bootstrapping Drupal. To execute Drupal's update.php, specify + http://default/core/update.php as the URI. To run this script without --root argument invoke it from the root directory diff --git a/core/update.php b/core/update.php index 9797833..d75f5b6 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'; @@ -440,6 +456,7 @@ drupal_maintenance_theme(); // 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 +470,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 +519,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..6ebb47f 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_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); + drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); -menu_execute_active_handler(); + +$dispatcher = new EventDispatcher(); +$resolver = new ControllerResolver(); + +$kernel = new DrupalKernel($dispatcher, $resolver); +$response = $kernel->handle($request); +$response->prepare($request); +$response->send(); +$kernel->terminate($request, $response);