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 c26ec86..7673d23 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 @@ -1510,6 +1511,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 a18e1e4..c15bd1c 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..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/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/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php new file mode 100644 index 0000000..a8745767 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -0,0 +1,99 @@ +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..1c2d75e --- /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..2dd9d25 --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,109 @@ +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. + !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/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/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();