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 c376882..b2e5e18 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 @@ -626,7 +627,7 @@ function drupal_settings_initialize() { // '/index.php', whereas $_SERVER['PHP_SELF'] is '/index.php/foo'. 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); @@ -1535,6 +1536,27 @@ function request_uri($omit_query_string = FALSE) { } /** + * 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 17c626b..b398002 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ 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 5cb3399..2dce416 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/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/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 3805864..2c215e7 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -524,15 +524,15 @@ 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. Its message is the extra + // database debug information. We have to do it this way because PHP + // does not allow us to override Exception::getMessage(). + + $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 @@ +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..2392bb9 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -0,0 +1,63 @@ +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 once code has been refactored to use the request + // object directly. + _current_path($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..69e9dd5 --- /dev/null +++ b/core/lib/Drupal/Core/ExceptionController.php @@ -0,0 +1,373 @@ +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 generic exception into an HTTP 500 response. + * + * @param FlattenException $exception + * Metadata about the exception that was thrown. + * @param 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; + + + //return _drupal_log_error(_drupal_decode_exception($exception), TRUE); + } + + /** + * This method is a temporary port of _drupal_decode_exception(). + * + * @todo This should get refactored. Flatten Exception 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 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 c43e725..75942fc 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/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 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 271f15d..7ab907b 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 afe0d2b..0a33b86 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -1834,6 +1834,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)); } @@ -2041,6 +2042,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)) { @@ -2184,7 +2186,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 9619461..3bd3b89 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. */ @@ -2267,6 +2269,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 f0e36cc..50d3c3c 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 20afc29..054bc2c 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', @@ -1112,6 +1120,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() { @@ -1977,8 +2015,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 4fdad71..6a0b377 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); } /** 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 f352c75..48ce3cf 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/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 1b4e6ca..0c33114 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 4219728..809cd8a 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 fb5e82c..b18f5a4 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);