diff --git a/.htaccess b/.htaccess
index a69bdd4..725897e 100644
--- a/.htaccess
+++ b/.htaccess
@@ -109,7 +109,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/includes/batch.inc b/core/includes/batch.inc
index 83ddd30..0b07d8e 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -14,6 +14,8 @@
* @see batch_get()
*/
+use \Symfony\Component\HttpFoundation\JsonResponse;
+
/**
* Loads a batch from the database.
*
@@ -77,7 +79,7 @@ function _batch_page() {
case 'do':
// JavaScript-based progress page callback.
- _batch_do();
+ $output = _batch_do();
break;
case 'do_nojs':
@@ -160,7 +162,7 @@ function _batch_do() {
// Perform actual processing.
list($percentage, $message) = _batch_process();
- drupal_json_output(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+ return new JsonResponse(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
}
/**
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 369fdfc..3574658 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
@@ -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 08ce2ab..7cf9fc2 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 675a2d5..42fcaff 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 37b3281..e7bf3f8 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.
@@ -261,6 +264,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;
@@ -488,6 +499,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/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 e676e28..ff95122 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -275,7 +275,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,
);
@@ -2515,3 +2516,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 21fe465..02020ff 100644
--- a/core/modules/comment/comment.pages.inc
+++ b/core/modules/comment/comment.pages.inc
@@ -103,11 +103,9 @@ function comment_reply($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);
@@ -115,5 +113,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 2c422a7..160c183 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.
@@ -635,7 +635,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 a59a5c7..c9dadd2 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -3,6 +3,7 @@
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\Query\SelectInterface;
+use Symfony\Component\HttpFoundation\Response;
/**
* @file
@@ -2651,8 +2652,7 @@ function node_feed($nids = FALSE, $channel = array()) {
$output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language'], $channel_extras);
$output .= "\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/overlay/overlay.module b/core/modules/overlay/overlay.module
index 144c2ab..fdb19ab 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;
@@ -300,22 +302,41 @@ function overlay_page_alter(&$page) {
/**
* 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;
+ user_save(user_load($user->uid), array('data' => array('overlay_message_dismissed' => 1)));
+ 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');
+}
+
+/**
+ * Access callback; determines access to dismiss the overlay accessibility message.
+ *
+ * @see overlay_user_dismiss_message()
+ * @see overlay_menu()
+ */
+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 {
- user_save(user_load($user->uid), array('data' => array('overlay_message_dismissed' => 1)));
- 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;
}
/**
@@ -667,7 +688,8 @@ function overlay_overlay_child_initialize() {
// it to the same content rendered in overlay_exit(), at the end of the page
// request. This allows us to check if anything actually did change, and, if
// so, trigger an immediate Ajax refresh of the parent window.
- if (!empty($_POST) || isset($_GET['token'])) {
+ $token = request()->query->get('token');
+ if (!empty($_POST) || isset($token)) {
foreach (overlay_supplemental_regions() as $region) {
overlay_store_rendered_content($region, overlay_render_region($region));
}
@@ -979,5 +1001,5 @@ function overlay_trigger_refresh() {
* @see Drupal.overlay.refreshRegions()
*/
function overlay_ajax_render_region($region) {
- print overlay_render_region($region);
+ return new Response(overlay_render_region($region));
}
diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php
index 5d868a2..1fa9506 100644
--- a/core/modules/simpletest/drupal_web_test_case.php
+++ b/core/modules/simpletest/drupal_web_test_case.php
@@ -1837,6 +1837,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));
}
@@ -2044,6 +2045,7 @@ class DrupalWebTestCase extends DrupalTestCase {
}
$content = $this->content;
$drupal_settings = $this->drupalSettings;
+ $headers[] = 'X-Requested-With: XMLHttpRequest';
// Get the Ajax settings bound to the triggering element.
if (!isset($ajax_settings)) {
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 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.module b/core/modules/system/system.module
index 200a335..5320610 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1995,8 +1995,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 78f6a23..5f4594a 100644
--- a/core/modules/system/system.test
+++ b/core/modules/system/system.test
@@ -2859,6 +2859,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 c5eced1..00bda25 100644
--- a/core/modules/system/tests/file.test
+++ b/core/modules/system/tests/file.test
@@ -2423,7 +2423,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/taxonomy/taxonomy.test b/core/modules/taxonomy/taxonomy.test
index fe60239..d62b429 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 3b1d9cb..86881f9 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2484,9 +2484,12 @@ function user_delete_multiple(array $uids) {
* Page callback wrapper for user_view().
*/
function user_view_page($account) {
+ if (is_object($account)) {
+ return user_view($account);
+ }
// An administrator may try to view a non-existent account,
// so we give them a 404 (versus a 403 for non-admins).
- return is_object($account) ? user_view($account) : MENU_NOT_FOUND;
+ drupal_not_found();
}
/**
diff --git a/core/update.php b/core/update.php
index 9797833..3b05dd0 100644
--- a/core/update.php
+++ b/core/update.php
@@ -461,13 +461,15 @@ if (update_access_allowed()) {
// 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.
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);