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/bootstrap.inc b/core/includes/bootstrap.inc index 12b1d80..53ce688 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3,6 +3,7 @@ use Drupal\Core\Database\Database; use Symfony\Component\ClassLoader\UniversalClassLoader; use Symfony\Component\ClassLoader\ApcUniversalClassLoader; +use Symfony\Component\HttpFoundation\Request; /** * @file @@ -1511,6 +1512,27 @@ function request_uri() { } /** + * Returns the current global request 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 5ff6167..f970cd6 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ getAcceptableContentTypes(); + if (in_array('application/json', $acceptable_content_types)) { + return 'json'; + } + + // Do HTML last so that it always wins for */* formats. + if(in_array('text/html', $acceptable_content_types) || in_array('*/*', $acceptable_content_types)) { + return 'html'; + } + } + +} + diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php new file mode 100644 index 0000000..3899802 --- /dev/null +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -0,0 +1,84 @@ +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 PathSubscriber()); + $this->dispatcher->addSubscriber(new LegacyControllerSubscriber()); + + // 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..2435a2b --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -0,0 +1,53 @@ +getRequest()->attributes->get('drupal_menu_item'); + + if (isset($router_item['access']) && !$router_item['access']) { + throw new AccessDeniedHttpException(); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestAccessCheck', 30); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/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/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php new file mode 100644 index 0000000..e5722f7 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -0,0 +1,67 @@ +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/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php new file mode 100644 index 0000000..9f3c3f5 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -0,0 +1,97 @@ +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; + } + + /** + * 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..63e3f87 --- /dev/null +++ b/core/lib/Drupal/Core/ExceptionController.php @@ -0,0 +1,209 @@ +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'); + $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/UrlMatcher.php b/core/lib/Drupal/Core/UrlMatcher.php new file mode 100644 index 0000000..ca397a6 --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,114 @@ +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); + } + + /** + * @param array $router_item + * + * @return array|\Symfony\Component\Routing\Route + */ + 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. + $page_arguments = !is_array($router_item['page_arguments']) ? unserialize($router_item['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/overlay/overlay.module b/core/modules/overlay/overlay.module index 58dcbab..ab577a3 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( @@ -975,5 +977,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/tests/file.test b/core/modules/simpletest/tests/file.test index c5eced1..00bda25 100644 --- a/core/modules/simpletest/tests/file.test +++ b/core/modules/simpletest/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/index.php b/index.php index b91fb1e..76a34cd 100644 --- a/index.php +++ b/index.php @@ -11,11 +11,32 @@ * 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); +$kernel->handle($request)->send();