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/common.inc b/core/includes/common.inc index a18e1e4..e588ff8 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ getDispatcher(); + + $matcher = $this->getMatcher($request); + $dispatcher->addSubscriber(new RouterListener($matcher)); + $dispatcher->addSubscriber(new AccessSubscriber()); + $dispatcher->addSubscriber(new PathSubscriber()); + $dispatcher->addSubscriber(new LegacyControllerSubscriber()); + + $resolver = new ControllerResolver(); + + $kernel = new HttpKernel($dispatcher, $resolver); + $response = $kernel->handle($request); + } + catch (Exception $e) { + // 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. + $response = new Response('A fatal error occurred: ' . $e->getMessage(), 500); + } + + return $response; + } + + /** + * Returns an EventDispatcher for the Kernel to use. + * + * The EventDispatcher is pre-wired with some event listeners/subscribers. + * + * @todo Make the listeners that get attached extensible, but without using + * hooks. + * + * @return EventDispatcher + */ + protected function getDispatcher() { + $dispatcher = new EventDispatcher(); + + // @todo Make this extensible rather than just hard coding some. + // @todo Add a subscriber to handle other things, too, like our Ajax + // replacement system. + $dispatcher->addSubscriber(new HtmlSubscriber()); + $dispatcher->addSubscriber(new JsonSubscriber()); + + return $dispatcher; + } + + /** + * Returns a UrlMatcher object for the specified request. + * + * @param Request $request + * The request object for this matcher to use. + * @return UrlMatcher + */ + protected function getMatcher(Request $request) { + // Resolve a routing context(path, etc) using the routes object to a + // Set a routing context to translate. + $context = new RequestContext(); + $context->fromRequest($request); + $matcher = new UrlMatcher($context); + + return $matcher; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php new file mode 100644 index 0000000..77f827d --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -0,0 +1,54 @@ +getRequest()->attributes->get('drupal_menu_item'); + + if (!$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/HtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlSubscriber.php new file mode 100644 index 0000000..060888d --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlSubscriber.php @@ -0,0 +1,185 @@ +getRequest()->getAcceptableContentTypes(); + return in_array('text/html', $acceptable_content_types) || in_array('*/*', $acceptable_content_types); + } + + /** + * Processes an AccessDenied exception into an HTTP 403 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onAccessDeniedException(GetResponseEvent $event) { + if ($this->isHtmlRequestEvent($event) && $event->getException() instanceof AccessDeniedHttpException) { + $system_path = $event->getRequest()->attributes->get('system_path'); + + watchdog('access denied', $system_path, NULL, WATCHDOG_WARNING); + + // Keep the 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_403', '')); + if ($path && $path != $system_path) { + // Custom 403 page. + // @todo: Same hack as in onNotFoundHttpException. Find a better way to + // override URLs. + $request = Request::create('/' . $path); + + $kernel = new DrupalKernel(); + $response = $kernel->handle($request, DrupalKernel::SUB_REQUEST); + $response->setStatusCode(403, 'Forbidden'); + } + else { + drupal_set_title(t('Access denied')); + drupal_set_page_content(t('You are not authorized to access this page.')); + $page = element_info('page'); + $content = drupal_render_page($page); + + $response = new Response('Forbidden', 403); + $response->setContent($content); + } + + $event->setResponse($response); + } + } + + /** + * Processes a NotFound exception into an HTTP 404 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onNotFoundHttpException(GetResponseEvent $event) { + if ($this->isHtmlRequestEvent($event) && $event->getException() instanceof NotFoundHttpException) { + $system_path = $event->getRequest()->attributes->get('system_path'); + + watchdog('page not found', $system_path, NULL, WATCHDOG_WARNING); + + // Check for and return a fast 404 page if configured. + // @todo Inline this rather than using a function. If drupal_fast_404 is + // mainly for image styles maybe we should have a handler for that. + drupal_fast_404(); + + + // 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(). + // Also the need to prefix / seams especially hacky for now. + $request = Request::create('/' . $path); + + $kernel = new DrupalKernel(); + $response = $kernel->handle($request, DrupalKernel::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' => $event->getRequest()->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); + } + + $event->setResponse($response); + } + } + + /** + * Processes a MethodNotAllowed exception into an HTTP 405 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onMethodAllowedException(GetResponseEvent $event) { + if ($this->isHtmlRequestEvent($event) && $event->getException() instanceof MethodNotAllowedException) { + $event->setResponse(new Response('Method Not Allowed', 405)); + } + } + + /** + * 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 onView(GetResponseEvent $event) { + if ($this->isHtmlRequestEvent($event)) { + $page_callback_result = $event->getControllerResult(); + $event->setResponse(new Response(drupal_render_page($page_callback_result))); + } + else { + $event->setResponse(new Response('Unsupported Media Type', 415)); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + // Since we want HTML to be our default, catch-all response type, give its + // listeners a very low priority so that they always check last. + $events[KernelEvents::EXCEPTION][] = array('onNotFoundHttpException', -5); + $events[KernelEvents::EXCEPTION][] = array('onAccessDeniedException', -5); + $events[KernelEvents::EXCEPTION][] = array('onMethodAllowedException', -5); + + $events[KernelEvents::VIEW][] = array('onView', -5); + + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/JsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/JsonSubscriber.php new file mode 100644 index 0000000..92ce1e8 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/JsonSubscriber.php @@ -0,0 +1,124 @@ +getRequest()->getAcceptableContentTypes()); + } + + protected function createJsonResponse() { + $response = new Response(); + $response->headers->set('Content-Type', 'application/json; charset=utf-8'); + + return $response; + } + + /** + * Processes an AccessDenied exception into an HTTP 403 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onAccessDeniedException(GetResponseEvent $event) { + if ($this->isJsonRequestEvent($event) && $event->getException() instanceof AccessDeniedHttpException) { + $response = $this->createJsonResponse(); + $response->setStatusCode(403, 'Access Denied'); + $event->setResponse($response); + } + } + + /** + * Processes a NotFound exception into an HTTP 404 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onNotFoundHttpException(GetResponseEvent $event) { + if ($this->isJsonRequestEvent($event) && $event->getException() instanceof NotFoundHttpException) { + $response = $this->createJsonResponse(); + $response->setStatusCode(404, 'Not Found'); + $event->setResponse($response); + } + } + + /** + * Processes a MethodNotAllowed exception into an HTTP 405 response. + * + * @param GetResponseEvent $event + * The Event to process. + */ + public function onMethodAllowedException(GetResponseEvent $event) { + if ($this->isJsonRequestEvent($event) && $event->getException() instanceof MethodNotAllowedException) { + $response = $this->createJsonResponse(); + $response->setStatusCode(405, 'Method Not Allowed'); + $event->setResponse($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 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) { + if ($this->isJsonRequestEvent($event)) { + $page_callback_result = $event->getControllerResult(); + + $response = $this->createJsonResponse(); + $response->setContent($page_callback_result); + + $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::EXCEPTION][] = array('onNotFoundHttpException'); + $events[KernelEvents::EXCEPTION][] = array('onAccessDeniedException'); + $events[KernelEvents::EXCEPTION][] = array('onMethodAllowedException'); + + $events[KernelEvents::VIEW][] = array('onView'); + + 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..e85d89f --- /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 (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..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/UrlMatcher.php b/core/lib/Drupal/Core/UrlMatcher.php new file mode 100644 index 0000000..366405a --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,99 @@ +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)) { + + $routes = new RouteCollection(); + $routes->add(hash('sha256', $router_item['path']), $this->convertDrupalItem($router_item)); + + if ($ret = $this->matchCollection($pathinfo, $routes)) { + //drupal_set_message('
' . var_export('test', TRUE) . '
'); + // 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; + } + } + + throw 0 < count($this->allow) + ? new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow))) + : 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. + foreach ($router_item['page_arguments'] as $k => $v) { + $route[$k] = $v; + } + return new Route($router_item['href'], $route); + } +} diff --git a/index.php b/index.php index b91fb1e..7609b3e 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,8 @@ handle($request)->send();