diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php new file mode 100644 index 0000000..6302db8 --- /dev/null +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -0,0 +1,54 @@ +. + if ($request->get('ajax_iframe_upload', FALSE)) { + return 'iframeupload'; + } + + // AJAX calls need to be run through ajax rendering functions + elseif ($request->isXmlHttpRequest()) { + return 'ajax'; + } + + foreach ($request->getAcceptableContentTypes() as $mime_type) { + $format = $request->getFormat($mime_type); + if (!is_null($format)) { + return $format; + } + } + + // Do HTML last so that it always wins. + return 'html'; + } +} diff --git a/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php b/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php new file mode 100644 index 0000000..b212478 --- /dev/null +++ b/core/lib/Drupal/Core/Database/DatabaseExceptionWrapper.php @@ -0,0 +1,19 @@ +dispatcher = $dispatcher; + $this->resolver = $resolver; + + $this->matcher = new UrlMatcher(); + $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 LegacyRequestSubscriber()); + $this->dispatcher->addSubscriber(new LegacyControllerSubscriber()); + $this->dispatcher->addSubscriber(new FinishResponseSubscriber()); + $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..4d10489 --- /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/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php new file mode 100644 index 0000000..6e23845 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -0,0 +1,83 @@ +getResponse(); + + // Set the X-UA-Compatible HTTP header to force IE to use the most recent + // rendering engine or use Chrome's frame rendering engine if available. + $response->headers->set('X-UA-Compatible', 'IE=edge,chrome=1', false); + + // Set the Content-language header. + $response->headers->set('Content-language', drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode); + + // Because pages are highly dynamic, set the last-modified time to now + // since the page is in fact being regenerated right now. + // @todo Remove this and use a more intelligent default so that HTTP + // caching can function properly. + $response->headers->set('Last-Modified', gmdate(DATE_RFC1123, REQUEST_TIME)); + + // Also give each page a unique ETag. This will force clients to include + // both an If-Modified-Since header and an If-None-Match header when doing + // conditional requests for the page (required by RFC 2616, section 13.3.4), + // making the validation more robust. This is a workaround for a bug in + // Mozilla Firefox that is triggered when Drupal's caching is enabled and + // the user accesses Drupal via an HTTP proxy (see + // https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an + // authenticated user requests a page, and then logs out and requests the + // same page again, Firefox may send a conditional request based on the + // page that was cached locally when the user was logged in. If this page + // did not have an ETag header, the request only contains an + // If-Modified-Since header. The date will be recent, because with + // authenticated users the Last-Modified header always refers to the time + // of the request. If the user accesses Drupal via a proxy server, and the + // proxy already has a cached copy of the anonymous page with an older + // Last-Modified date, the proxy may respond with 304 Not Modified, making + // the client think that the anonymous and authenticated pageviews are + // identical. + // @todo Remove this line as no longer necessary per + // http://drupal.org/node/1573064 + $response->headers->set('ETag', '"' . REQUEST_TIME . '"'); + + // Authenticated users are always given a 'no-cache' header, and will fetch + // a fresh page on every request. This prevents authenticated users from + // seeing locally cached pages. + // @todo Revisit whether or not this is still appropriate now that the + // Response object does its own cache control procesisng and we intend to + // use partial page caching more extensively. + $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT'); + $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0'); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = array('onRespond'); + 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..48cc8a0 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php @@ -0,0 +1,63 @@ +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/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php new file mode 100644 index 0000000..2620c07 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php @@ -0,0 +1,55 @@ +getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + menu_set_custom_theme(); + drupal_theme_initialize(); + module_invoke_all('init'); + + // Tell Drupal it is now fully bootstrapped (for the benefit of code that + // calls drupal_get_bootstrap_phase()), but without having + // _drupal_bootstrap_full() do anything, since we've already done the + // equivalent above and in earlier listeners. + _drupal_bootstrap_full(TRUE); + drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + } + } + + /** + * 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('onKernelRequestLegacy', 90); + + 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..daed1c9 --- /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/PathListenerBase.php b/core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php new file mode 100644 index 0000000..fd0a765 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php @@ -0,0 +1,29 @@ +attributes->get('system_path'); + return isset($path) ? $path : trim($request->getPathInfo(), '/'); + } + + public function setPath(Request $request, $path) { + $request->attributes->set('system_path', $path); + + // @todo Remove this line once code has been refactored to use the request + // object directly. + _current_path($path); + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php new file mode 100644 index 0000000..1e2a95a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -0,0 +1,127 @@ +getRequest(); + + $path = $this->extractPath($request); + + $path = drupal_get_normal_path($path); + + $this->setPath($request, $path); + } + + /** + * Resolve the front-page default path. + * + * @todo The path system should be objectified to remove the function calls in + * this method. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestFrontPageResolve(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + if (empty($path)) { + // @todo Temporary hack. Fix when configuration is injectable. + $path = variable_get('site_frontpage', 'user'); + } + + $this->setPath($request, $path); + } + + /** + * Decode language information embedded in the request path. + * + * @todo Refactor this entire method to inline the relevant portions of + * drupal_language_initialize(). See the inline comment for more details. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestLanguageResolve(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + // drupal_language_initialize() combines: + // - Determination of language from $request information (e.g., path). + // - Determination of language from other information (e.g., site default). + // - Population of determined language into drupal_container(). + // - Removal of language code from _current_path(). + // @todo Decouple the above, but for now, invoke it and update the path + // prior to front page and alias resolution. When above is decoupled, also + // add 'langcode' (determined from $request only) to $request->attributes. + _current_path($path); + drupal_language_initialize(); + $path = _current_path(); + + $this->setPath($request, $path); + } + + /** + * Decodes the path of the request. + * + * Parameters in the URL sometimes represent code-meaningful strings. It is + * therefore useful to always urldecode() those values so that individual + * controllers need not concern themselves with it. This is Drupal-specific + * logic and may not be familiar for developers used to other Symfony-family + * projects. + * + * @todo Revisit whether or not this logic is appropriate for here or if + * controllers should be required to implement this logic themselves. If we + * decide to keep this code, remove this TODO. + * + * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestDecodePath(GetResponseEvent $event) { + $request = $event->getRequest(); + $path = $this->extractPath($request); + + $path = urldecode($path); + + $this->setPath($request, $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('onKernelRequestDecodePath', 200); + $events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageResolve', 150); + $events[KernelEvents::REQUEST][] = array('onKernelRequestFrontPageResolve', 101); + $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..9b3ef45 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php @@ -0,0 +1,63 @@ +getResponse(); + $config = config('system.performance'); + + if ($config->get('cache') && ($cache = drupal_page_set_cache())) { + 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/RouterListener.php b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php new file mode 100644 index 0000000..5b6246d --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php @@ -0,0 +1,81 @@ +urlMatcher = $urlMatcher; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + * + * This method is nearly identical to the parent, except it passes the + * $request->attributes->get('system_path') variable to the matcher. + * That is where Drupal stores its processed, de-aliased, and sanitized + * internal path. We also pass the full request object to the URL Matcher, + * since we want attributes to be available to the matcher and to controllers. + */ + public function onKernelRequest(GetResponseEvent $event) { + $request = $event->getRequest(); + + if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + $this->urlMatcher->getContext()->fromRequest($request); + $this->urlMatcher->setRequest($request); + } + + if ($request->attributes->has('_controller')) { + // Routing is already done. + return; + } + + // Add attributes based on the path info (routing). + try { + $parameters = $this->urlMatcher->match($request->attributes->get('system_path')); + + if (null !== $this->logger) { + $this->logger->info(sprintf('Matched route "%s" (parameters: %s)', $parameters['_route'], $this->parametersToString($parameters))); + } + + $request->attributes->add($parameters); + unset($parameters['_route']); + unset($parameters['_controller']); + $request->attributes->set('_route_params', $parameters); + } + catch (ResourceNotFoundException $e) { + $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo()); + + throw new NotFoundHttpException($message, $e); + } + catch (MethodNotAllowedException $e) { + $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), strtoupper(implode(', ', $e->getAllowedMethods()))); + + throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php new file mode 100644 index 0000000..4761d47 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -0,0 +1,128 @@ +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 Symfony\Component\HttpKernel\Event\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(); + + $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 Symfony\Component\HttpKernel\Event\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..a2ab913 --- /dev/null +++ b/core/lib/Drupal/Core/ExceptionController.php @@ -0,0 +1,411 @@ +kernel = $kernel; + $this->negotiation = $negotiation; + } + + /** + * Handles an exception on a request. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\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 Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + 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 Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + 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()); + + // The active trail is being statically cached from the parent request to + // the subrequest, like any other static. Unfortunately that means the + // data in it is incorrect and does not get regenerated correctly for + // the subrequest. In this instance, that even causes a fatal error in + // some circumstances because menu_get_active_trail() ends up having + // a missing localized_options value. To work around that, reset the + // menu static variables and let them be regenerated as needed. + // @todo It is likely that there are other such statics that need to be + // reset that are not triggering test failures right now. If found, + // add them here. + // @todo Refactor the breadcrumb system so that it does not rely on static + // variables in the first place, which will eliminate the need for this + // hack. + drupal_static_reset('menu_set_active_trail'); + menu_reset_static_cache(); + + $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 404 response. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Sonfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on404Html(FlattenException $exception, Request $request) { + watchdog('page not found', check_plain($request->attributes->get('system_path')), 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()); + + // The active trail is being statically cached from the parent request to + // the subrequest, like any other static. Unfortunately that means the + // data in it is incorrect and does not get regenerated correctly for + // the subrequest. In this instance, that even causes a fatal error in + // some circumstances because menu_get_active_trail() ends up having + // a missing localized_options value. To work around that, reset the + // menu static variables and let them be regenerated as needed. + // @todo It is likely that there are other such statics that need to be + // reset that are not triggering test failures right now. If found, + // add them here. + // @todo Refactor the breadcrumb system so that it does not rely on static + // variables in the first place, which will eliminate the need for this + // hack. + drupal_static_reset('menu_set_active_trail'); + menu_reset_static_cache(); + + $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 a generic exception into an HTTP 500 response. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * Metadata about the exception that was thrown. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on500Html(FlattenException $exception, Request $request) { + $error = $this->decodeException($exception); + + // Because the kernel doesn't run until full bootstrap, we know that + // most subsystems are already initialized. + + $headers = array(); + + // When running inside the testing framework, we relay the errors + // to the tested site by the way of HTTP headers. + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + // $number does not use drupal_static as it should not be reset + // as it uniquely identifies each PHP error. + static $number = 0; + $assertion = array( + $error['!message'], + $error['%type'], + array( + 'function' => $error['%function'], + 'file' => $error['%file'], + 'line' => $error['%line'], + ), + ); + $headers['X-Drupal-Assertion-' . $number] = rawurlencode(serialize($assertion)); + $number++; + } + + watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + + // Display the message if the current error reporting level allows this type + // of message to be displayed, and unconditionnaly in update.php. + if (error_displayable($error)) { + $class = 'error'; + + // If error type is 'User notice' then treat it as debug information + // instead of an error message, see dd(). + if ($error['%type'] == 'User notice') { + $error['%type'] = 'Debug'; + $class = 'status'; + } + + drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); + } + + drupal_set_title(t('Error')); + // We fallback to a maintenance page at this point, because the page + // generation itself can generate errors. + $output = theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); + + $response = new Response($output, 500); + $response->setStatusCode(500, '500 Service unavailable (with message)'); + + return $response; + } + + /** + * Processes an AccessDenied exception that occured on a JSON request. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on403Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(403, 'Access Denied'); + return $response; + } + + /** + * Processes a NotFound exception that occured on a JSON request. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on404Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(404, 'Not Found'); + return $response; + } + + /** + * Processes a MethodNotAllowed exception that occured on a JSON request. + * + * @param Symfony\Component\HttpKernel\Exception\FlattenException $exception + * The flattened exception. + * @param Symfony\Component\HttpFoundation\Request $request + * The request object that triggered this exception. + */ + public function on405Json(FlattenException $exception, Request $request) { + $response = new JsonResponse(); + $response->setStatusCode(405, 'Method Not Allowed'); + return $response; + } + + + /** + * This method is a temporary port of _drupal_decode_exception(). + * + * @todo This should get refactored. FlattenException could use some + * improvement as well. + * + * @return array + */ + protected function decodeException(FlattenException $exception) { + $message = $exception->getMessage(); + + $backtrace = $exception->getTrace(); + + // This value is missing from the stack for some reason in the + // FlattenException version of the backtrace. + $backtrace[0]['line'] = $exception->getLine(); + + // For database errors, we try to return the initial caller, + // skipping internal functions of the database layer. + if (strpos($exception->getClass(), 'DatabaseExceptionWrapper') !== FALSE) { + // A DatabaseExceptionWrapper exception is actually just a courier for + // the original PDOException. It's the stack trace from that exception + // that we care about. + $backtrace = $exception->getPrevious()->getTrace(); + $backtrace[0]['line'] = $exception->getLine(); + + // The first element in the stack is the call, the second element gives us the caller. + // We skip calls that occurred in one of the classes of the database layer + // or in one of its global functions. + $db_functions = array('db_query', 'db_query_range'); + while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && + ((strpos($caller['namespace'], 'Drupal\Core\Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || + in_array($caller['function'], $db_functions)) { + // We remove that call. + array_shift($backtrace); + } + } + $caller = $this->getLastCaller($backtrace); + + return array( + '%type' => $exception->getClass(), + // The standard PHP exception handler considers that the exception message + // is plain-text. We mimick this behavior here. + '!message' => check_plain($message), + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + 'severity_level' => WATCHDOG_ERROR, + ); + } + + /** + * Gets the last caller from a backtrace. + * + * The last caller is not necessarily the first item in the backtrace. Rather, + * it is the first item in the backtrace that is a PHP userspace function, + * and not one of our debug functions. + * + * @param $backtrace + * A standard PHP backtrace. + * + * @return + * An associative array with keys 'file', 'line' and 'function'. + */ + protected function getLastCaller($backtrace) { + // Ignore black listed error handling functions. + $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); + + // Errors that occur inside PHP internal functions do not generate + // information about file and line. + while (($backtrace && !isset($backtrace[0]['line'])) || + (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { + array_shift($backtrace); + } + + // The first trace is the call itself. + // It gives us the line and the file of the last call. + $call = $backtrace[0]; + + // The second call give us the function where the call originated. + if (isset($backtrace[1])) { + if (isset($backtrace[1]['class'])) { + $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; + } + else { + $call['function'] = $backtrace[1]['function'] . '()'; + } + } + else { + $call['function'] = 'main()'; + } + return $call; + } +} diff --git a/core/lib/Drupal/Core/UrlMatcher.php b/core/lib/Drupal/Core/UrlMatcher.php new file mode 100644 index 0000000..57b6317 --- /dev/null +++ b/core/lib/Drupal/Core/UrlMatcher.php @@ -0,0 +1,168 @@ +context = new RequestContext(); + } + + /** + * Sets the request context. + * + * This method is just to satisfy the interface, and is largely vestigial. + * The request context object does not contain the information we need, so + * we will use the original request object. + * + * @param Symfony\Component\Routing\RequestContext $context + * The context. + * + * @api + */ + public function setContext(RequestContext $context) { + $this->context = $context; + } + + /** + * Gets the request context. + * + * This method is just to satisfy the interface, and is largely vestigial. + * The request context object does not contain the information we need, so + * we will use the original request object. + * + * @return Symfony\Component\Routing\RequestContext + * The context. + */ + public function getContext() { + return $this->context; + } + + /** + * Sets the request object to use. + * + * This is used by the RouterListener to make additional request attributes + * available. + * + * @param Symfony\Component\HttpFoundation\Request $request + * The request object. + */ + public function setRequest(Request $request) { + $this->request = $request; + } + + /** + * Gets the request object. + * + * @return Symfony\Component\HttpFoundation\Request $request + * The request object. + */ + public function getRequest() { + return $this->request; + } + + /** + * {@inheritDoc} + * + * @api + */ + public function match($pathinfo) { + if ($router_item = $this->matchDrupalItem($pathinfo)) { + $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); + } + + /** + * Converts a Drupal menu item to a route array. + * + * @param array $router_item + * The Drupal menu item. + * + * @return + * An array of parameters. + */ + protected function convertDrupalItem($router_item) { + $route = array( + '_controller' => $router_item['page_callback'] + ); + + // @todo menu_get_item() does not unserialize page arguments when the access + // is denied. Remove this temporary hack that always does that. + if (!is_array($router_item['page_arguments'])) { + $router_item['page_arguments'] = unserialize($router_item['page_arguments']); + } + + // Place argument defaults on the route. + foreach ($router_item['page_arguments'] as $k => $v) { + $route[$k] = $v; + } + return $route; + } +} diff --git a/index.php b/index.php index b91fb1e..8a36c6e 100644 --- a/index.php +++ b/index.php @@ -18,4 +18,11 @@ define('DRUPAL_ROOT', getcwd()); require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + +$tmp_classes = unserialize('a:42:{s:40:"Symfony\Component\HttpFoundation\Request";s:56:"core/vendor/Symfony/Component/HttpFoundation/Request.php";s:45:"Symfony\Component\HttpFoundation\ParameterBag";s:61:"core/vendor/Symfony/Component/HttpFoundation/ParameterBag.php";s:40:"Symfony\Component\HttpFoundation\FileBag";s:56:"core/vendor/Symfony/Component/HttpFoundation/FileBag.php";s:42:"Symfony\Component\HttpFoundation\ServerBag";s:58:"core/vendor/Symfony/Component/HttpFoundation/ServerBag.php";s:42:"Symfony\Component\HttpFoundation\HeaderBag";s:58:"core/vendor/Symfony/Component/HttpFoundation/HeaderBag.php";s:58:"Symfony\Component\EventDispatcher\EventDispatcherInterface";s:74:"core/vendor/Symfony/Component/EventDispatcher/EventDispatcherInterface.php";s:49:"Symfony\Component\EventDispatcher\EventDispatcher";s:65:"core/vendor/Symfony/Component/EventDispatcher/EventDispatcher.php";s:67:"Symfony\Component\HttpKernel\Controller\ControllerResolverInterface";s:83:"core/vendor/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php";s:58:"Symfony\Component\HttpKernel\Controller\ControllerResolver";s:74:"core/vendor/Symfony/Component/HttpKernel/Controller/ControllerResolver.php";s:48:"Symfony\Component\HttpKernel\HttpKernelInterface";s:64:"core/vendor/Symfony/Component/HttpKernel/HttpKernelInterface.php";s:48:"Symfony\Component\HttpKernel\TerminableInterface";s:64:"core/vendor/Symfony/Component/HttpKernel/TerminableInterface.php";s:39:"Symfony\Component\HttpKernel\HttpKernel";s:55:"core/vendor/Symfony/Component/HttpKernel/HttpKernel.php";s:24:"Drupal\Core\DrupalKernel";s:37:"core/lib/Drupal/Core/DrupalKernel.php";s:54:"Symfony\Component\Routing\RequestContextAwareInterface";s:70:"core/vendor/Symfony/Component/Routing/RequestContextAwareInterface.php";s:53:"Symfony\Component\Routing\Matcher\UrlMatcherInterface";s:69:"core/vendor/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php";s:22:"Drupal\Core\UrlMatcher";s:35:"core/lib/Drupal/Core/UrlMatcher.php";s:40:"Symfony\Component\Routing\RequestContext";s:56:"core/vendor/Symfony/Component/Routing/RequestContext.php";s:58:"Symfony\Component\EventDispatcher\EventSubscriberInterface";s:74:"core/vendor/Symfony/Component/EventDispatcher/EventSubscriberInterface.php";s:57:"Symfony\Component\HttpKernel\EventListener\RouterListener";s:73:"core/vendor/Symfony/Component/HttpKernel/EventListener/RouterListener.php";s:42:"Drupal\Core\EventSubscriber\RouterListener";s:55:"core/lib/Drupal/Core/EventSubscriber/RouterListener.php";s:41:"Symfony\Component\HttpKernel\KernelEvents";s:57:"core/vendor/Symfony/Component/HttpKernel/KernelEvents.php";s:30:"Drupal\Core\ContentNegotiation";s:43:"core/lib/Drupal/Core/ContentNegotiation.php";s:42:"Drupal\Core\EventSubscriber\ViewSubscriber";s:55:"core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php";s:44:"Drupal\Core\EventSubscriber\AccessSubscriber";s:57:"core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php";s:53:"Drupal\Core\EventSubscriber\MaintenanceModeSubscriber";s:66:"core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php";s:44:"Drupal\Core\EventSubscriber\PathListenerBase";s:57:"core/lib/Drupal/Core/EventSubscriber/PathListenerBase.php";s:42:"Drupal\Core\EventSubscriber\PathSubscriber";s:55:"core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php";s:51:"Drupal\Core\EventSubscriber\LegacyRequestSubscriber";s:64:"core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php";s:54:"Drupal\Core\EventSubscriber\LegacyControllerSubscriber";s:67:"core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php";s:52:"Drupal\Core\EventSubscriber\FinishResponseSubscriber";s:65:"core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php";s:50:"Drupal\Core\EventSubscriber\RequestCloseSubscriber";s:63:"core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber.php";s:60:"Symfony\Component\HttpKernel\EventListener\ExceptionListener";s:76:"core/vendor/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php";s:31:"Drupal\Core\ExceptionController";s:44:"core/lib/Drupal/Core/ExceptionController.php";s:39:"Symfony\Component\EventDispatcher\Event";s:55:"core/vendor/Symfony/Component/EventDispatcher/Event.php";s:46:"Symfony\Component\HttpKernel\Event\KernelEvent";s:62:"core/vendor/Symfony/Component/HttpKernel/Event/KernelEvent.php";s:51:"Symfony\Component\HttpKernel\Event\GetResponseEvent";s:67:"core/vendor/Symfony/Component/HttpKernel/Event/GetResponseEvent.php";s:56:"Symfony\Component\HttpKernel\Event\FilterControllerEvent";s:72:"core/vendor/Symfony/Component/HttpKernel/Event/FilterControllerEvent.php";s:70:"Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent";s:86:"core/vendor/Symfony/Component/HttpKernel/Event/GetResponseForControllerResultEvent.php";s:41:"Symfony\Component\HttpFoundation\Response";s:57:"core/vendor/Symfony/Component/HttpFoundation/Response.php";s:50:"Symfony\Component\HttpFoundation\ResponseHeaderBag";s:66:"core/vendor/Symfony/Component/HttpFoundation/ResponseHeaderBag.php";s:54:"Symfony\Component\HttpKernel\Event\FilterResponseEvent";s:70:"core/vendor/Symfony/Component/HttpKernel/Event/FilterResponseEvent.php";s:52:"Symfony\Component\HttpKernel\Event\PostResponseEvent";s:68:"core/vendor/Symfony/Component/HttpKernel/Event/PostResponseEvent.php";}'); +foreach ($tmp_classes as $tmp_class => $tmp_file) { + require DRUPAL_ROOT . '/' . $tmp_file; +} + menu_execute_active_handler(); +