diff --git a/core/core.services.yml b/core/core.services.yml index e75a5a4..e78de78 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -338,12 +338,12 @@ services: - { name: legacy_route_enhancer, priority: 20 } route_enhancer.entity: class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer - arguments: ['@content_negotiation'] + arguments: ['@controller.entityform'] tags: - { name: route_enhancer, priority: 15 } route_enhancer.form: class: Drupal\Core\Routing\Enhancer\FormEnhancer - arguments: ['@content_negotiation'] + arguments: ['@controller.form'] tags: - { name: route_enhancer, priority: 10 } route_special_attributes_subscriber: @@ -352,10 +352,19 @@ services: - { name: event_subscriber } controller.page: class: Drupal\Core\Controller\HtmlPageController - arguments: ['@http_kernel', '@controller_resolver'] + arguments: ['@controller_resolver', '@language_manager'] + controller.ajax: + class: Drupal\Core\Controller\AjaxController + arguments: ['@controller_resolver'] + controller.form: + class: Drupal\Core\Controller\FormController + arguments: ['@controller_resolver', '@service_container'] + controller.entityform: + class: Drupal\Core\Entity\HtmlEntityFormController + arguments: ['@controller_resolver', '@service_container', '@plugin.manager.entity'] controller.dialog: class: Drupal\Core\Controller\DialogController - arguments: ['@http_kernel'] + arguments: ['@controller_resolver'] router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener tags: @@ -458,7 +467,7 @@ services: arguments: ['@language_manager', '@string_translation'] exception_controller: class: Drupal\Core\Controller\ExceptionController - arguments: ['@content_negotiation'] + arguments: ['@content_negotiation', '@language_manager'] calls: - [setContainer, ['@service_container']] exception_listener: diff --git a/core/includes/common.inc b/core/includes/common.inc index c6f6688..edb0dd4 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -369,12 +369,23 @@ function _drupal_default_html_head() { } /** - * Retrieves output to be displayed in the HEAD tag of the HTML page. + * Retrieves output or array to be displayed in the HEAD tag of the HTML page. + * + * @param bool $render + * Should the HTML head be rendered. + * + * @return string|array + * Either the rendered or the structural data of the HTML head. */ -function drupal_get_html_head() { +function drupal_get_html_head($render = TRUE) { $elements = drupal_add_html_head(); drupal_alter('html_head', $elements); - return drupal_render($elements); + if ($render) { + return drupal_render($elements); + } + else { + return $elements; + } } /** @@ -3629,7 +3640,7 @@ function drupal_pre_render_dropbutton($element) { } /** - * Renders the page, including all theming. + * Processes the page render array, enhancing it as necessary. * * @param $page * A string or array representing the content of a page. The array consists of @@ -3639,10 +3650,17 @@ function drupal_pre_render_dropbutton($element) { * - #show_messages: Suppress drupal_get_message() items. Used by Batch * API (optional). * + * @param boolean $legacy + * TRUE if this function should use the old-style page element that includes + * the HTML wrapper. FALSE to use the page element that doesn't self-render + * the HTML part of the page. Defaults to false. + * @return array + * The processed render array for the page. + * * @see hook_page_alter() * @see element_info() */ -function drupal_render_page($page) { +function drupal_prepare_page($page, $legacy = FALSE) { $main_content_display = &drupal_static('system_main_content_added', FALSE); // Pull out the page title to set it back later. @@ -3655,7 +3673,7 @@ function drupal_render_page($page) { // in the page with defaults. if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) { drupal_set_page_content($page); - $page = element_info('page'); + $page = $legacy ? element_info('page') : element_info('page_body'); } // Modules can add elements to $page as needed in hook_page_build(). @@ -3679,6 +3697,25 @@ function drupal_render_page($page) { $page['#title'] = $title; } + return $page; +} + +/** + * Renders the page, including all theming. + * + * @param $page + * A string or array representing the content of a page. The array consists of + * the following keys: + * - #type: Value is always 'page'. This pushes the theming through + * the page template (required). + * - #show_messages: Suppress drupal_get_message() items. Used by Batch + * API (optional). + * + * @see hook_page_alter() + * @see element_info() + */ +function drupal_render_page($page) { + $page = drupal_prepare_page($page, TRUE); return drupal_render($page); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 122cc63..8f30484 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -8,6 +8,7 @@ * customized by user themes. */ +use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\String; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\Config; @@ -19,6 +20,9 @@ use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Page\Link; +use Drupal\Core\Page\Metatag; + /** * @defgroup content_flags Content markers * @{ @@ -2494,6 +2498,123 @@ function _template_preprocess_default_variables() { * * @see system_elements() */ +function template_preprocess_html_page(&$variables) { + + /** @var $page \Drupal\Core\Page\HtmlPage */ + $page = $variables['page_object']; + + $variables['html_attributes'] = $page->getHtmlAttributes(); + $variables['attributes'] = $page->getAttributes(); + $variables['page'] = $page->getContent(); + + // Compile a list of classes that are going to be applied to the body element. + // This allows advanced theming based on context (home page, node of certain type, etc.). + $body_classes = $variables['attributes']['class']; + $body_classes[] = 'html'; + // Add a class that tells us whether we're on the front page or not. + $body_classes[] = $variables['is_front'] ? 'front' : 'not-front'; + // Add a class that tells us whether the page is viewed by an authenticated user or not. + $body_classes[] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; + $variables['attributes']['class'] = $body_classes; + + // Populate the body classes. + if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { + foreach ($suggestions as $suggestion) { + if ($suggestion != 'page-front') { + // Add current suggestion to page classes to make it possible to theme + // the page depending on the current page type (e.g. node, admin, user, + // etc.) as well as more specific data like node-12 or node-edit. + $variables['attributes']['class'][] = drupal_html_class($suggestion); + } + } + } + + // If on an individual node page, add the node type to body classes. + if ($node = menu_get_object()) { + $variables['attributes']['class'][] = drupal_html_class('node-type-' . $node->type); + } + + // Add favicon. + if (theme_get_setting('features.favicon')) { + $favicon = theme_get_setting('favicon.url'); + $type = theme_get_setting('favicon.mimetype'); + $link = new Link(drupal_strip_dangerous_protocols($favicon), 'shortcut icon'); + $link->setAttribute('type', $type); + $page->addLink($link); + } + + $site_config = Drupal::config('system.site'); + if ($page->hasTitle()) { + $head_title = array( + 'title' => strip_tags($page->getTitle()), + 'name' => String::checkPlain($site_config->get('name')), + ); + } + else { + $head_title = array('name' => String::checkPlain($site_config->get('name'))); + if ($site_config->get('slogan')) { + $head_title['slogan'] = strip_tags(Xss::filterAdmin($site_config->get('slogan'))); + } + } + + $variables['head_title_array'] = $head_title; + $variables['head_title'] = implode(' | ', $head_title); + + // Display the html.tpl.php's default mobile metatags for responsive design. + $page->addMetatag(new Metatag('MobileOptimized', 'width')); + $page->addMetatag(new Metatag('HandheldFriendly', 'true')); + $page->addMetatag(new Metatag('viewport', 'width=device-width')); + + // @todo Decide whether we want to get drupal_get_html_head() support until + // each instance is ported (drupal_add_html_head() and + // drupal_add_html_head_link() would be needed). + $html_heads = drupal_get_html_head(FALSE); + uasort($html_heads, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + foreach ($html_heads as $name => $tag) { + if ($tag['#tag'] == 'link') { + $page->addLink(new Link($name, isset($tag['#attributes']['content']) ? $tag['#attributes']['content'] : '', $tag['#attributes'])); + } + elseif ($tag['#tag'] == 'meta') { + $page->addMetatag(new Metatag($name, isset($tag['#attributes']['content']) ? $tag['#attributes']['content'] : '', $tag['#attributes'])); + } + } + + // Populate the page template suggestions. + if ($suggestions = theme_get_suggestions(arg(), 'html')) { + $variables['theme_hook_suggestions'] = $suggestions; + } + + drupal_add_library('system', 'html5shiv', TRUE); + + $variables['page_top'][] = array('#markup' => $page->getContentTop()); + $variables['page_bottom'][] = array('#markup' => $page->getContentBottom()); + + // Add footer scripts as '#markup' so they can be rendered with other + // elements in page_bottom. + $footer_scripts = new RenderWrapper('drupal_get_js', array('footer')); + $variables['page_bottom'][] = array('#markup' => $footer_scripts); + + // Wrap function calls in an object so they can be called when printed. + $variables['head'] = new RenderWrapper(function() use ($page) { + return implode("\n", $page->metatags()) . implode("\n", $page->links()); + }); + $variables['styles'] = new RenderWrapper('drupal_get_css'); + $variables['scripts'] = new RenderWrapper('drupal_get_js'); +} + +/** + * Prepares variables for HTML document templates. + * + * This is specifically for the legacy router only. + * + * Default template: html.html.twig. + * + * @param array $variables + * An associative array containing: + * - page: A render element representing the page. + * + * @see system_elements() + */ function template_preprocess_html(&$variables) { $language_interface = language(Language::TYPE_INTERFACE); @@ -3006,9 +3127,14 @@ function drupal_common_theme() { 'render element' => 'page', 'template' => 'html', ), + 'html_page' => array( + 'variables' => array('page_object' => NULL), + 'template' => 'html', + ), 'page' => array( 'render element' => 'page', 'template' => 'page', + 'theme wrapper' => 'html', ), 'region' => array( 'render element' => 'elements', diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php index 7b2cece..cc949f9 100644 --- a/core/lib/Drupal/Core/Controller/AjaxController.php +++ b/core/lib/Drupal/Core/Controller/AjaxController.php @@ -10,8 +10,10 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Ajax\PrependCommand; +use Drupal\Core\Page\HtmlPage; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Default controller for ajax requests. @@ -19,6 +21,23 @@ class AjaxController extends ContainerAware { /** + * The controller resolver. + * + * @var \Drupal\Core\Controller\ControllerResolverInterface + */ + protected $controllerResolver; + + /** + * Constructs a new AjaxController instance. + * + * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver + * The controller resolver. + */ + public function __construct(ControllerResolverInterface $controller_resolver) { + $this->controllerResolver = $controller_resolver; + } + + /** * Controller method for AJAX content. * * @param \Symfony\Component\HttpFoundation\Request $request @@ -30,50 +49,63 @@ class AjaxController extends ContainerAware { * A response object. */ public function content(Request $request, $_content) { + $content = $this->getContentResult($request, $_content); + // If there is already an AjaxResponse, then return it without + // manipulation. + if ($content instanceof AjaxResponse && $content->isOk()) { + return $content; + } - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - $controller = $_content; + if ($content instanceof HtmlPage) { + $content = $content->getContent(); + } + if ($content instanceof Response) { + $content = $content->getContent(); + } - // We need to clean up the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('_system_path'); - $attributes->remove('_content'); - $attributes->remove('_legacy'); + // A page callback could return a render array or a string. + if (!is_array($content)) { + $content = array( + '#markup' => $content, + ); + } - // Remove the accept header so the subrequest does not end up back in this - // controller. - $request->headers->remove('accept'); - // Remove the header in order to let the subrequest not think that it's an - // ajax request, see \Drupal\Core\ContentNegotiation. - $request->headers->remove('x-requested-with'); + $html = drupal_render($content); - $response = $this->container->get('http_kernel')->forward($controller, $attributes->all(), $request->query->all()); - // For successful (HTTP status 200) responses. - if ($response->isOk()) { - // If there is already an AjaxResponse, then return it without - // manipulation. - if (!($response instanceof AjaxResponse)) { - // Pull the content out of the response. - $content = $response->getContent(); - // A page callback could return a render array or a string. - $html = is_string($content) ? $content : drupal_render($content); - $response = new AjaxResponse(); - // The selector for the insert command is NULL as the new content will - // replace the element making the ajax call. The default 'replaceWith' - // behavior can be changed with #ajax['method']. - $response->addCommand(new InsertCommand(NULL, $html)); - $status_messages = array('#theme' => 'status_messages'); - $output = drupal_render($status_messages); - if (!empty($output)) { - $response->addCommand(new PrependCommand(NULL, $output)); - } - } + $response = new AjaxResponse(); + // The selector for the insert command is NULL as the new content will + // replace the element making the ajax call. The default 'replaceWith' + // behavior can be changed with #ajax['method']. + $response->addCommand(new InsertCommand(NULL, $html)); + $status_messages = array('#theme' => 'status_messages'); + $output = drupal_render($status_messages); + if (!empty($output)) { + $response->addCommand(new PrependCommand(NULL, $output)); } return $response; } + + /** + * Returns the result of invoking the sub-controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * @return array + * The render array that results from invoking the controller. + */ + public function getContentResult(Request $request, $controller_definition) { + if ($controller_definition instanceof \Closure) { + $callable = $controller_definition; + } + else { + $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); + } + $arguments = $this->controllerResolver->getArguments($request, $callable); + $page_content = call_user_func_array($callable, $arguments); + + return $page_content; + } + } diff --git a/core/lib/Drupal/Core/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php index cde2876..c8edc96 100644 --- a/core/lib/Drupal/Core/Controller/DialogController.php +++ b/core/lib/Drupal/Core/Controller/DialogController.php @@ -7,23 +7,26 @@ namespace Drupal\Core\Controller; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\OpenDialogCommand; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Page\HtmlPage; + /** * Defines a default controller for dialog requests. */ class DialogController { /** - * The HttpKernel object to use for subrequests. + * The controller resolver service * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface + * @var \Drupal\Core\Controller\ControllerResolver */ - protected $httpKernel; + protected $controllerResolver; /** * Constructs a new DialogController. @@ -31,8 +34,8 @@ class DialogController { * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel * The kernel. */ - public function __construct(HttpKernelInterface $kernel) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolver $controller_resolver) { + $this->controllerResolver = $controller_resolver; } /** @@ -75,8 +78,8 @@ protected function forward(Request $request) { * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a modal dialog. */ - public function modal(Request $request) { - return $this->dialog($request, TRUE); + public function modal(Request $request, $_content) { + return $this->dialog($request, $_content, TRUE); } /** @@ -90,45 +93,80 @@ public function modal(Request $request) { * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a dialog. */ - public function dialog(Request $request, $modal = FALSE) { - $subrequest = $this->forward($request); - if ($subrequest->isOk()) { - $content = $subrequest->getContent(); - // @todo Remove use of drupal_get_title() when - // http://drupal.org/node/1871596 is in. - $title = drupal_get_title(); - $response = new AjaxResponse(); - // Fetch any modal options passed in from data-dialog-options. - if (!($options = $request->request->get('dialogOptions'))) { - $options = array(); - } - // Set modal flag and re-use the modal ID. - if ($modal) { - $options['modal'] = TRUE; - $target = '#drupal-modal'; + public function dialog(Request $request, $_content, $modal = FALSE) { + $page_content = $this->getContentResult($request, $_content); + + if ($page_content instanceof HtmlPage) { + $page_content = $page_content->getContent(); + } + if ($page_content instanceof Response) { + $page_content = $page_content->getContent(); + } + + if (!is_array($page_content)) { + $page_content = array( + '#markup' => $page_content, + ); + } + + $content = drupal_render($page_content); + + // @todo Remove use of drupal_get_title() when + // http://drupal.org/node/1871596 is in. + $title = drupal_get_title(); + $response = new AjaxResponse(); + // Fetch any modal options passed in from data-dialog-options. + if (!($options = $request->request->get('dialogOptions'))) { + $options = array(); + } + // Set modal flag and re-use the modal ID. + if ($modal) { + $options['modal'] = TRUE; + $target = '#drupal-modal'; + } + else { + // Generate the target wrapper for the dialog. + if (isset($options['target'])) { + // If the target was nominated in the incoming options, use that. + $target = $options['target']; + // Ensure the target includes the #. + if (substr($target, 0, 1) != '#') { + $target = '#' . $target; + } + // This shouldn't be passed on to jQuery.ui.dialog. + unset($options['target']); } else { - // Generate the target wrapper for the dialog. - if (isset($options['target'])) { - // If the target was nominated in the incoming options, use that. - $target = $options['target']; - // Ensure the target includes the #. - if (substr($target, 0, 1) != '#') { - $target = '#' . $target; - } - // This shouldn't be passed on to jQuery.ui.dialog. - unset($options['target']); - } - else { - // Generate a target based on the route id. - $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); - $target = '#' . drupal_html_id("drupal-dialog-$route_name"); - } + // Generate a target based on the route id. + $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); + $target = '#' . drupal_html_id("drupal-dialog-$route_name"); } - $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); - return $response; } - // An error occurred in the subrequest, return that. - return $subrequest; + $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); + return $response; + } + + /** + * Returns the result of invoking the sub-controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * @return array + * The render array that results from invoking the controller. + */ + public function getContentResult(Request $request, $controller_definition) { + if ($controller_definition instanceof \Closure) { + $callable = $controller_definition; + } + else { + $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); + } + $arguments = $this->controllerResolver->getArguments($request, $callable); + $page_content = call_user_func_array($callable, $arguments); + + return $page_content; } + } diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index 8515af0..1659b43 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -7,19 +7,23 @@ namespace Drupal\Core\Controller; +use Drupal\Core\Language\LanguageManager; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Exception\FlattenException; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\ContentNegotiation; +use Drupal\Core\Page\HtmlPage; /** * This controller handles HTTP errors generated by the routing system. */ -class ExceptionController extends ContainerAware { +class ExceptionController extends HtmlControllerBase implements ContainerAwareInterface { /** * The content negotiation library. @@ -29,13 +33,34 @@ class ExceptionController extends ContainerAware { protected $negotiation; /** + * The service container. + * + * @var ContainerInterface + */ + protected $container; + + /** + * Sets the Container associated with this Controller. + * + * @param ContainerInterface $container A ContainerInterface instance + * + * @api + */ + public function setContainer(ContainerInterface $container = null) { + $this->container = $container; + } + + /** * Constructor. * - * @param Drupal\Core\ContentNegotiation $negotiation + * @param \Drupal\Core\ContentNegotiation $negotiation * The content negotiation library to use to determine the correct response * format. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. */ - public function __construct(ContentNegotiation $negotiation) { + public function __construct(ContentNegotiation $negotiation, LanguageManager $language_manager) { + parent::__construct($language_manager); $this->negotiation = $negotiation; } @@ -84,7 +109,8 @@ public function on403Html(FlattenException $exception, Request $request) { $system_path = $request->attributes->get('_system_path'); watchdog('access denied', $system_path, NULL, WATCHDOG_WARNING); - $path = $this->container->get('path.alias_manager')->getSystemPath(\Drupal::config('system.site')->get('page.403')); + $system_config = $this->container->get('config.factory')->get('system.site'); + $path = $this->container->get('path.alias_manager')->getSystemPath($system_config->get('page.403')); if ($path && $path != $system_path) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { @@ -113,15 +139,18 @@ public function on403Html(FlattenException $exception, Request $request) { $response->setStatusCode(403, 'Access denied'); } else { + $page_content = array( + '#markup' => t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())), + ); + + $page = $this->createHtmlPage($page_content, $request); + $page->setTitle(t('Access denied')); + $page->setStatusCode(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); + $output = theme('html_page', array('page_object' => $page)); + $response = new Response($output, $page->getStatusCode()); - $response = new Response($content, 403); + return $response; } return $response; @@ -186,17 +215,19 @@ public function on404Html(FlattenException $exception, Request $request) { $response->setStatusCode(404, 'Not Found'); } else { - // @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 = new Response($content, 404); - } + $page_content = array( + '#markup' => t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())), + ); - return $response; + $page = $this->createHtmlPage($page_content, $request); + $page->setTitle(t('Page not found')); + $page->setStatusCode(404); + + $output = theme('html_page', array('page_object' => $page)); + $response = new Response($output, $page->getStatusCode()); + + return $response; + } } /** @@ -252,17 +283,17 @@ public function on500Html(FlattenException $exception, Request $request) { 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. - $maintenance_page = array( + $page_content = array( '#theme' => 'maintenance_page', '#content' => t('The website has encountered an error. Please try again later.'), ); - $output = drupal_render($maintenance_page); - $response = new Response($output, 500); - $response->setStatusCode(500, '500 Service unavailable (with message)'); + $page = $this->createHtmlPage($page_content, $request); + $page->setTitle(t('Error')); + $page->setStatusCode(500); + + $output = drupal_render($page_content); + $response = new Response($output, $page->getStatusCode()); return $response; } diff --git a/core/lib/Drupal/Core/Controller/HtmlFormController.php b/core/lib/Drupal/Core/Controller/FormController.php similarity index 61% rename from core/lib/Drupal/Core/Controller/HtmlFormController.php rename to core/lib/Drupal/Core/Controller/FormController.php index 8e88b53..455bca0 100644 --- a/core/lib/Drupal/Core/Controller/HtmlFormController.php +++ b/core/lib/Drupal/Core/Controller/FormController.php @@ -2,51 +2,41 @@ /** * @file - * Contains \Drupal\Core\Controller\HtmlFormController. + * Contains \Drupal\Core\Controler\FormController. */ namespace Drupal\Core\Controller; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use \Drupal\Core\Controller\ControllerResolverInterface; -/** - * Wrapping controller for forms that serve as the main page body. - */ -class HtmlFormController implements ContainerAwareInterface { + +class FormController { + + /** + * The controller resolver. + * + * @var \Drupal\Core\Controller\ControllerResolverInterface + */ + protected $resolver; /** - * The injection container for this object. * * @var \Symfony\Component\DependencyInjection\ContainerInterface */ protected $container; /** - * Injects the service container used by this object. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * The service container this object should use. + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. */ - public function setContainer(ContainerInterface $container = NULL) { + public function __construct(ControllerResolverInterface $resolver, ContainerInterface $container) { + $this->resolver = $resolver; $this->container = $container; } - /** - * Controller method for generic HTML form pages. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - * @param callable $_form - * The name of the form class for this request. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function content(Request $request, $_form) { - $form_object = $this->getFormObject($request, $_form); + public function getContentResult(Request $request, $controller_definition) { + $form_object = $this->getFormObject($request, $controller_definition); // Using reflection, find all of the parameters needed by the form in the // request attributes, skipping $form and $form_state. @@ -56,7 +46,7 @@ public function content(Request $request, $_form) { $form_state = array(); $request->attributes->set('form', array()); $request->attributes->set('form_state', $form_state); - $args = $this->container->get('controller_resolver')->getArguments($request, array($form_object, 'buildForm')); + $args = $this->resolver->getArguments($request, array($form_object, 'buildForm')); $request->attributes->remove('form'); $request->attributes->remove('form_state'); @@ -92,5 +82,4 @@ protected function getFormObject(Request $request, $form_arg) { // Otherwise, it is a service. return $this->container->get($form_arg); } - -} +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php new file mode 100644 index 0000000..a8e1b3a --- /dev/null +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php @@ -0,0 +1,121 @@ +languageManager = $language_manager; + } + + /** + * Converts a render array into an HtmlPage object. + * + * @param array|string $page_content + * The page content area to display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Drupal\Core\Page\HtmlPage + * A page object. + */ + public function createHtmlPage($page_content, Request $request) { + if ($page_content instanceof HtmlPage || $page_content instanceof Response) { + return $page_content; + } + + if (!is_array($page_content)) { + $page_content = array( + '#markup' => $page_content, + ); + } + + $page = new HtmlPage('', $request->attributes->get('_title')); + + $page_array = drupal_prepare_page($page_content); + + $page = $this->preparePage($page, $page_array); + + $page->setContentTop(drupal_render($page_array['page_top'])); + $page->setContentBottom(drupal_render($page_array['page_bottom'])); + $page->setContent(drupal_render($page_array)); + + return $page; + } + + /** + * Enhances a page object based on a render array. + * + * @param \Drupal\Core\Page\HtmlPage $page + * The page object to enhance. + * @param array $page_array + * The page array to extract onto the page object. + * + * @return \Drupal\Core\Page\HtmlPage + * The modified page object. + */ + protected function preparePage(HtmlPage $page, $page_array) { + if (isset($page_array['#title'])) { + $page->setTitle($page_array['#title']); + unset($page_array['#title']); + } + elseif ($title = drupal_get_title()) { + // drupal_set_title() already ensured security, so not letting the + // title passing would cause double escaping. + $page->setTitle($title, PASS_THROUGH); + } + + $attributes = $page->getAttributes(); + + // Add information about the number of sidebars. + $classes = $attributes['class']; + if (!empty($page_array['sidebar_first']) && !empty($page_array['page']['sidebar_second'])) { + $classes[] = 'two-sidebars'; + } + elseif (!empty($page_array['sidebar_first'])) { + $classes[] = 'one-sidebar'; + $classes[] = 'sidebar-first'; + } + elseif (!empty($page_array['sidebar_second'])) { + $classes[] = 'one-sidebar'; + $classes[] = 'sidebar-second'; + } + else { + $classes[] = 'no-sidebars'; + } + $attributes['class'] = $classes; + + // HTML element attributes. + $language_interface = $this->languageManager->getLanguage(Language::TYPE_INTERFACE); + $html_attributes = $page->getHtmlAttributes(); + $html_attributes['lang'] = $language_interface->id; + $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; + + return $page; + } + +} + diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php index d87b3ed..e0e1c9f 100644 --- a/core/lib/Drupal/Core/Controller/HtmlPageController.php +++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php @@ -7,21 +7,16 @@ namespace Drupal\Core\Controller; +use Drupal\Core\Language\LanguageManager; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; + /** * Default controller for most HTML pages. */ -class HtmlPageController { - - /** - * The HttpKernel object to use for subrequests. - * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface - */ - protected $httpKernel; +class HtmlPageController extends HtmlControllerBase { /** * The controller resolver. @@ -33,12 +28,13 @@ class HtmlPageController { /** * Constructs a new HtmlPageController. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver * The controller resolver. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. */ - public function __construct(HttpKernelInterface $kernel, ControllerResolverInterface $controller_resolver) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolverInterface $controller_resolver, LanguageManager $language_manager) { + parent::__construct($language_manager); $this->controllerResolver = $controller_resolver; } @@ -54,24 +50,31 @@ public function __construct(HttpKernelInterface $kernel, ControllerResolverInter * A response object. */ public function content(Request $request, $_content) { - $callable = $this->controllerResolver->getControllerFromDefinition($_content); - $arguments = $this->controllerResolver->getArguments($request, $callable); - $page_content = call_user_func_array($callable, $arguments); - if ($page_content instanceof Response) { - return $page_content; - } - if (!is_array($page_content)) { - $page_content = array( - '#markup' => $page_content, - ); + $page_content = $this->getContentResult($request, $_content); + return $this->createHtmlPage($page_content, $request); + } + + /** + * Returns the result of invoking the sub-controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * @return array + * The render array that results from invoking the controller. + */ + public function getContentResult(Request $request, $controller_definition) { + if ($controller_definition instanceof \Closure) { + $callable = $controller_definition; } - // If no title was returned fall back to one defined in the route. - if (!isset($page_content['#title']) && $request->attributes->has('_title')) { - $page_content['#title'] = $request->attributes->get('_title'); + else { + $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); } + $arguments = $this->controllerResolver->getArguments($request, $callable); + $page_content = call_user_func_array($callable, $arguments); - $response = new Response(drupal_render_page($page_content)); - return $response; + return $page_content; } } diff --git a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php index 2cf8d47..75b0c95 100644 --- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php +++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php @@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Drupal\Core\ContentNegotiation; +use Drupal\Core\Entity\HtmlEntityFormController; /** * Enhances an entity form route with the appropriate controller. @@ -18,29 +18,29 @@ class EntityRouteEnhancer implements RouteEnhancerInterface { /** - * Content negotiation library. + * The form controller wrapper. * - * @var \Drupal\Core\ContentNegotiation + * @var Drupal\Core\Controller\HtmlEntityFormController */ - protected $negotiation; + protected $formController; /** - * Constructs a new \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer. - * - * @param \Drupal\Core\ContentNegotiation $negotiation - * The content negotiation library. + * Constructs a new EntityRouteEnhancer object. */ - public function __construct(ContentNegotiation $negotiation) { - $this->negotiation = $negotiation; + public function __construct(HtmlEntityFormController $form_controller) { + $this->formController = $form_controller; } /** * {@inheritdoc} */ public function enhance(array $defaults, Request $request) { - if (empty($defaults['_controller']) && $this->negotiation->getContentType($request) === 'html') { + if (empty($defaults['_content'])) { if (!empty($defaults['_entity_form'])) { - $defaults['_controller'] = '\Drupal\Core\Entity\HtmlEntityFormController::content'; + $controller_wrapper = $this->formController; + $defaults['_content'] = function() use ($controller_wrapper, $defaults, $request) { + return $controller_wrapper->getContentResult($request, $defaults['_entity_form']); + }; } elseif (!empty($defaults['_entity_list'])) { $defaults['_controller'] = 'controller.page:content'; diff --git a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php index 8f62a40..a337991 100644 --- a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php +++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php @@ -7,22 +7,31 @@ namespace Drupal\Core\Entity; -use Drupal\Core\Controller\HtmlFormController; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\DependencyInjection\ContainerInterface; + +use Drupal\Core\Controller\FormController; +use Drupal\Core\Entity\EntityManager; +use \Drupal\Core\Controller\ControllerResolverInterface; /** * Wrapping controller for entity forms that serve as the main page body. */ -class HtmlEntityFormController extends HtmlFormController { +class HtmlEntityFormController extends FormController { /** - * {@inheritdoc} + * The entity manager service. * - * Due to reflection, the argument must be named $_entity_form. The parent - * method has $request and $_form, but the parameter must match the route. + * @var \Drupal\Core\Entity\EntityManager + */ + protected $manager; + + /** + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. */ - public function content(Request $request, $_entity_form) { - return parent::content($request, $_entity_form); + public function __construct(ControllerResolverInterface $resolver, ContainerInterface $container, EntityManager $manager) { + parent::__construct($resolver, $container); + $this->manager = $manager; } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php index dca2708..5982943 100644 --- a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php @@ -7,10 +7,14 @@ namespace Drupal\Core\EventSubscriber; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Page\HtmlPage; + /** * Access subscriber for controller requests. */ @@ -43,7 +47,58 @@ public function onKernelControllerLegacy(FilterControllerEvent $event) { if ($request->attributes->get('_legacy')) { $router_item = $request->attributes->get('_drupal_menu_item'); $new_controller = function() use ($router_item) { - return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); + $page_content = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); + + if ($page_content instanceof HtmlPage || $page_content instanceof Response) { + return $page_content; + } + + $page = new HtmlPage(); + + $page_array = drupal_prepare_page($page_content); + + //$page = $this->preparePage($page, $page_array); + if (isset($page_array['#title'])) { + $page->setTitle($page_array['#title']); + unset($page_array['#title']); + } + elseif ($title = drupal_get_title()) { + // drupal_set_title() already ensured security, so not letting the + // title passing would cause double escaping. + $page->setTitle($title, PASS_THROUGH); + } + + $attributes = $page->getAttributes(); + + // Add information about the number of sidebars. + $classes = $attributes['class']; + if (!empty($page_array['sidebar_first']) && !empty($page_array['page']['sidebar_second'])) { + $classes[] = 'two-sidebars'; + } + elseif (!empty($page_array['sidebar_first'])) { + $classes[] = 'one-sidebar'; + $classes[] = 'sidebar-first'; + } + elseif (!empty($page_array['sidebar_second'])) { + $classes[] = 'one-sidebar'; + $classes[] = 'sidebar-second'; + } + else { + $classes[] = 'no-sidebars'; + } + $attributes['class'] = $classes; + + // HTML element attributes. + $language_interface = language(Language::TYPE_INTERFACE); + $html_attributes = $page->getHtmlAttributes(); + $html_attributes['lang'] = $language_interface->id; + $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; + + $page->setContentTop(drupal_render($page_array['page_top'])); + $page->setContentBottom(drupal_render($page_array['page_bottom'])); + $page->setContent(drupal_render($page_array)); + + return $page; }; $event->setController($new_controller); } diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php index fa4be98..f8bc8ae 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -15,6 +15,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Drupal\Core\ContentNegotiation; +use Drupal\Core\Page\HtmlPage; /** * Main subscriber for VIEW HTTP responses. @@ -32,6 +33,23 @@ public function __construct(ContentNegotiation $negotiation) { } /** + * Renders an HtmlPage object to a Response. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The Event to process. + */ + public function onHtmlPage(GetResponseForControllerResultEvent $event) { + $page = $event->getControllerResult(); + if ($page instanceof HtmlPage) { + $output = theme('html_page', array('page_object' => $page)); + + $response = new Response($output, $page->getStatusCode()); + + $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 @@ -62,22 +80,14 @@ public function onView(GetResponseForControllerResultEvent $event) { $event->setResponse(new Response('Not Acceptable', 406)); } } - elseif ($request->attributes->get('_legacy')) { - // This is an old hook_menu-based subrequest, which means we assume - // the body is supposed to be the complete page. - $page_result = $event->getControllerResult(); - if (!is_array($page_result)) { - $page_result = array( - '#markup' => $page_result, - ); - } - $event->setResponse(new Response(drupal_render_page($page_result))); - } else { // This is a new-style Symfony-esque subrequest, which means we assume // the body is not supposed to be a complete page but just a page // fragment. $page_result = $event->getControllerResult(); + if ($page_result instanceof HtmlPage || $page_result instanceof Response) { + return $page_result; + } if (!is_array($page_result)) { $page_result = array( '#markup' => $page_result, @@ -141,6 +151,9 @@ public function onIframeUpload(GetResponseForControllerResultEvent $event) { * The Event to process. */ public function onHtml(GetResponseForControllerResultEvent $event) { + $attributes = $event->getRequest()->attributes; + $error = sprintf("Route is returning a render array when it shouldn't: _controller: %s, _content: %s, route: %s", $attributes->get('_controller'), $attributes->get('_content'), $attributes->get('_route')); + trigger_error($error, E_USER_DEPRECATED); $page_callback_result = $event->getControllerResult(); return new Response(drupal_render_page($page_callback_result)); } @@ -152,6 +165,7 @@ public function onHtml(GetResponseForControllerResultEvent $event) { * An array of event listener definitions. */ static function getSubscribedEvents() { + $events[KernelEvents::VIEW][] = array('onHtmlPage', 10); $events[KernelEvents::VIEW][] = array('onView'); return $events; diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php new file mode 100644 index 0000000..ce28d53 --- /dev/null +++ b/core/lib/Drupal/Core/Page/HeadElement.php @@ -0,0 +1,57 @@ +attributes); + + $rendered = ''; + foreach ($attributes as $key => $value) { + $rendered .= " $key=\"{$value}\""; + } + + return "<{$this->element}{$rendered} />"; + } + + /** + * Sets an attribute on this element. + * + * @param type $key + * The attribute to set. + * @param type $value + * The value to which to set it. + */ + public function setAttribute($key, $value) { + $this->attributes[$key] = $value; + } +} diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php new file mode 100644 index 0000000..abf99a5 --- /dev/null +++ b/core/lib/Drupal/Core/Page/HtmlFragment.php @@ -0,0 +1,122 @@ +content = $content; + //$this->bag = new AssetBag(); + } + + /** + * Adds another asset bag to those already contained in this fragment. + * + * @param AssetBagInterface $bag + */ + public function addAssetBag(AssetBagInterface $bag) { + $this->bag->addAssetBag($bag); + } + + /** + * Returns the AssetBag representing the collected assets in this fragment. + * + * @return AssetBag + */ + public function getAssets() { + return $this->bag; + } + + /** + * Sets the response content. + * + * This should be the bulk of the page content, and will ultimately be placed + * within the tag in final HTML output. + * + * Valid types are strings, numbers, and objects that implement a __toString() + * method. + * + * @param mixed $content + * + * @return \Drupal\Core\Page\HtmlFragment + */ + public function setContent($content) { + $this->content = $content; + return $this; + } + + /** + * Gets the main content of this HtmlFragment. + * + * @return string + */ + public function getContent() { + return $this->content; + } + + /** + * Sets the title of this HtmlFragment. + * + * Handling of this title varies depending on what is consuming this + * HtmlFragment object. If it's a block, it may only be used as the block's + * title; if it's at the page level, it will be used in a number of places, + * including the html title. + */ + public function setTitle($title, $output = Title::CHECK_PLAIN) { + $this->title = ($output == PASS_THROUGH) ? $title : check_plain($title); + } + + /** + * Indicates whether or not this HtmlFragment has a title. + * + * @return bool + */ + public function hasTitle() { + return !empty($this->title); + } + + /** + * Gets the title for this HtmlFragment, if any. + * + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * Clones the current HtmlFragment instance. + */ + public function __clone() { + $this->bag = clone $this->bag; + } +} diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php new file mode 100644 index 0000000..635ef11 --- /dev/null +++ b/core/lib/Drupal/Core/Page/HtmlPage.php @@ -0,0 +1,212 @@ +title = $title; + + $this->htmlAttributes = new Attribute(); + $this->bodyAttributes = new Attribute(); + } + + /** + * Adds a Link to the page. + * + * @param \Drupal\Core\Page\Link $link + * A link element to enqueue. + * @return \Drupal\Core\Page\HtmlPage + * The invoked object. + */ + public function addLink(Link $link) { + $this->links[] = $link; + return $this; + } + + /** + * Returns an array of all enqueued links. + * + * @return array + */ + public function links() { + return $this->links; + } + + /** + * Adds a Meta tag to the page. + * + * @param \Drupal\Core\Page\Metatag $tag + * A meta element to add. + * @return \Drupal\Core\Page\HtmlPage + * The invoked object. + */ + public function addMetatag(Metatag $tag) { + $this->metatags[] = $tag; + return $this; + } + + /** + * Returns an array of all enqueued meta tags. + * @return array + */ + public function metatags() { + return $this->metatags; + } + + /** + * Returns the HTML attributes for this HTML page. + * + * @return \Drupal\Core\Template\Attribute + */ + public function getHtmlAttributes() { + return $this->htmlAttributes; + } + + /** + * Returns the HTML attributes for the body element of this page. + * + * @return \Drupal\Core\Template\Attribute + */ + public function getAttributes() { + return $this->bodyAttributes; + } + + /** + * Sets the top-content of this page. + * + * @param string $content + * The top-content to set. + * @return \Drupal\Core\Page\HtmlPage + * The called object. + */ + public function setContentTop($content) { + $this->contentTop = $content; + return $this; + } + + /** + * Returns the top-content of this page. + * + * @return string + * The top-content of this page. + */ + public function getContentTop() { + return $this->contentTop; + } + + /** + * Sets the bottom-content of this page. + * + * @param string $content + * The bottom-content to set. + * @return \Drupal\Core\Page\HtmlPage + * The called object. + */ + public function setContentBottom($content) { + $this->contentBottom = $content; + return $this; + } + + /** + * Returns the bottom-content of this page. + * + * @return string + * The bottom-content of this page. + */ + public function getContentBottom() { + return $this->contentBottom; + } + + /** + * Sets the HTTP status of this page. + * + * @param int $status + * The status code to set. + * @return \Drupal\Core\Page\HtmlPage + * The called object. + */ + public function setStatusCode($status) { + $this->statusCode = $status; + return $this; + } + + /** + * Returns the status code of this response. + * + * @return int + * The status code of thise page. + */ + public function getStatusCode() { + return $this->statusCode; + } +} + diff --git a/core/lib/Drupal/Core/Page/Link.php b/core/lib/Drupal/Core/Page/Link.php new file mode 100644 index 0000000..6b772b4 --- /dev/null +++ b/core/lib/Drupal/Core/Page/Link.php @@ -0,0 +1,31 @@ +attributes = $attributes + array( + 'href' => $href, + 'rel' => $rel, + ); + } +} diff --git a/core/lib/Drupal/Core/Page/Metatag.php b/core/lib/Drupal/Core/Page/Metatag.php new file mode 100644 index 0000000..c4822d8 --- /dev/null +++ b/core/lib/Drupal/Core/Page/Metatag.php @@ -0,0 +1,28 @@ +attributes = $attributes + array( + 'name' => $name, + 'content' => $content, + ); + } +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php index 47c5179..7c4a1df 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php @@ -39,7 +39,7 @@ public function __construct(ContentNegotiation $negotiation) { public function enhance(array $defaults, Request $request) { if (empty($defaults['_content']) && $this->negotiation->getContentType($request) == 'drupal_ajax') { $defaults['_content'] = isset($defaults['_controller']) ? $defaults['_controller'] : NULL; - $defaults['_controller'] = '\Drupal\Core\Controller\AjaxController::content'; + $defaults['_controller'] = 'controller.ajax:content'; } return $defaults; } diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php index e088d44..3555044 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php @@ -32,6 +32,7 @@ class ContentControllerEnhancer implements RouteEnhancerInterface { 'drupal_dialog' => 'controller.dialog:dialog', 'drupal_modal' => 'controller.dialog:modal', 'html' => 'controller.page:content', + 'drupal_ajax' => 'controller.ajax:content', ); /** @@ -50,19 +51,17 @@ public function __construct(ContentNegotiation $negotiation) { public function enhance(array $defaults, Request $request) { // If no controller is set and either _content is set or the request is // for a dialog or modal, then enhance. - if (empty($defaults['_controller']) && - ($type = $this->negotiation->getContentType($request)) && - (!empty($defaults['_content']) || - in_array($type, array('drupal_dialog', 'drupal_modal')))) { + if (empty($defaults['_controller']) && ($type = $this->negotiation->getContentType($request))) { if (isset($this->types[$type])) { $defaults['_controller'] = $this->types[$type]; } } + // When the dialog attribute is TRUE this is a DialogController sub-request. // Route the sub-request to the _content callable. - if ($request->attributes->get('dialog') && !empty($defaults['_content'])) { - $defaults['_controller'] = $defaults['_content']; - } + //if ($request->attributes->get('dialog') && !empty($defaults['_content'])) { + // $defaults['_controller'] = $defaults['_content']; + //} return $defaults; } } diff --git a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php index f84bb32..4c9d327 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php @@ -9,7 +9,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface; -use Drupal\Core\ContentNegotiation; +use Drupal\Core\Controller\FormController; +use \Drupal\Core\Controller\ControllerResolverInterface; /** * Enhances a form route with the appropriate controller. @@ -17,28 +18,28 @@ class FormEnhancer implements RouteEnhancerInterface { /** - * Content negotiation library. + * The form controller wrapper. * - * @var \Drupal\CoreContentNegotiation + * @var Drupal\Core\Controller\FormController */ - protected $negotiation; + protected $formController; /** * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. - * - * @param \Drupal\Core\ContentNegotiation $negotiation - * The Content Negotiation service. */ - public function __construct(ContentNegotiation $negotiation) { - $this->negotiation = $negotiation; + public function __construct(FormController $form_controller) { + $this->formController = $form_controller; } /** * {@inhertdoc} */ public function enhance(array $defaults, Request $request) { - if (empty($defaults['_controller']) && !empty($defaults['_form']) && $this->negotiation->getContentType($request) === 'html') { - $defaults['_controller'] = '\Drupal\Core\Controller\HtmlFormController::content'; + if (!empty($defaults['_form'])) { + $controller_wrapper = $this->formController; + $defaults['_content'] = function() use ($controller_wrapper, $defaults, $request) { + return $controller_wrapper->getContentResult($request, $defaults['_form']); + }; } return $defaults; } diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml index cadbb14..af88ec3 100644 --- a/core/modules/aggregator/aggregator.routing.yml +++ b/core/modules/aggregator/aggregator.routing.yml @@ -50,7 +50,7 @@ aggregator_opml_add: aggregator_page_last: pattern: '/aggregator' defaults: - _controller: '\Drupal\aggregator\Controller\AggregatorController::pageLast' + _content: '\Drupal\aggregator\Controller\AggregatorController::pageLast' requirements: _permission: 'access news feeds' diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module index 4427a3a..39048df 100644 --- a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module +++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module @@ -2,6 +2,7 @@ use Drupal\Component\Utility\Crypt; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; /** * Implements hook_menu(). @@ -64,7 +65,7 @@ function aggregator_test_feed($use_last_modified = FALSE, $use_etag = FALSE) { $feed = fread($handle, filesize($file_name)); fclose($handle); - print $feed; + return new Response($feed); } /** diff --git a/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php b/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php index 576ea80..4df0e25 100644 --- a/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php +++ b/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php @@ -42,7 +42,7 @@ public function routes(RouteBuildEvent $event) { $route = new Route( "admin/structure/block/list/$key", array( - '_controller' => '\Drupal\block\Controller\BlockListController::listing', + '_content' => '\Drupal\block\Controller\BlockListController::listing', 'theme' => $key, ), array( diff --git a/core/modules/config/tests/config_test/config_test.routing.yml b/core/modules/config/tests/config_test/config_test.routing.yml index c26a438..90a9d49 100644 --- a/core/modules/config/tests/config_test/config_test.routing.yml +++ b/core/modules/config/tests/config_test/config_test.routing.yml @@ -15,7 +15,7 @@ config_test_entity_add: config_test_entity: pattern: 'admin/structure/config_test/manage/{config_test}' defaults: - _controller: '\Drupal\config_test\ConfigTestController::edit' + _content: '\Drupal\config_test\ConfigTestController::edit' entity_type: 'config_test' requirements: _access: 'TRUE' @@ -23,7 +23,7 @@ config_test_entity: config_test_entity_edit: pattern: 'admin/structure/config_test/manage/{config_test}/edit' defaults: - _controller: '\Drupal\config_test\ConfigTestController::edit' + _content: '\Drupal\config_test\ConfigTestController::edit' entity_type: 'config_test' requirements: _access: 'TRUE' diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 050cdd7..167b2d0 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -5,6 +5,7 @@ * Admin page callbacks for the system module. */ +use Drupal\Core\Page\HtmlPage; use Drupal\system\DateFormatInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; @@ -365,6 +366,8 @@ function system_batch_page() { elseif (isset($output)) { // Force a page without blocks or messages to // display a list of collected messages later. + return new HtmlPage($output); + // @todo Figure out how to remove blocks but still render the page. drupal_set_page_content($output); $page = element_info('page'); $page['#show_messages'] = FALSE; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index bd58f93..887f069 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -263,6 +263,10 @@ function system_element_info() { '#theme' => 'page', '#theme_wrappers' => array('html'), ); + $types['page_body'] = array( + '#show_messages' => TRUE, + '#theme' => 'page', + ); // By default, we don't want Ajax commands being rendered in the context of an // HTML page, so we don't provide defaults for #theme or #theme_wrappers. // However, modules can set these properties (for example, to provide an HTML diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index f12ba8a..ff996aa 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -162,7 +162,7 @@ system_theme_enable: system_status: pattern: '/admin/reports/status' defaults: - _controller: 'Drupal\system\Controller\SystemInfoController::status' + _content: 'Drupal\system\Controller\SystemInfoController::status' requirements: _permission: 'administer site configuration' diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml index 2eda2c8..55c56a8 100644 --- a/core/modules/system/tests/modules/router_test/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml @@ -30,7 +30,7 @@ router_test_4: router_test_6: pattern: '/router_test/test6' defaults: - _controller: '\Drupal\router_test\TestControllers::test1' + _content: '\Drupal\router_test\TestControllers::test1' requirements: _access: 'TRUE' diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php index 2bbdb0f..bcbbf0d 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php @@ -8,9 +8,13 @@ namespace Drupal\views\Plugin\views\display; use Drupal\Component\Annotation\Plugin; +use Drupal\Component\Utility\Xss; +use Drupal\views\Views; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Drupal\Core\Annotation\Translation; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * The plugin that handles a full page. @@ -76,6 +80,26 @@ protected function defineOptions() { } /** + * {@inheritdoc} + */ + public function collectRoutes(RouteCollection $collection) { + parent::collectRoutes($collection); + + $view_id = $this->view->storage->id(); + $display_id = $this->display['id']; + + // Move _controller to _content for page displays, which will return a + // normal Drupal HTML page. + $view_route = $collection->get("view.$view_id.$display_id"); + $defaults = $view_route->getDefaults(); + $defaults['_content'] = $defaults['_controller']; + unset($defaults['_controller']); + $view_route->setDefaults($defaults); + + return $collection; + } + + /** * Overrides \Drupal\views\Plugin\views\display\PathPluginBase::execute(). */ public function execute() { @@ -91,12 +115,16 @@ public function execute() { // First execute the view so it's possible to get tokens for the title. // And the title, which is much easier. - drupal_set_title(filter_xss_admin($this->view->getTitle()), PASS_THROUGH); - - $response = $this->view->getResponse(); - $response->setContent(drupal_render_page($render)); - - return $response; + // @todo Figure out how to support custom response objects. Maybe for pages + // it should be dropped. + if (is_array($render)) { + return $render + array( + '#title' => Xss::filterAdmin($this->view->getTitle()), + ); + } + else { + return $render; + } } /** diff --git a/core/modules/views_ui/views_ui.routing.yml b/core/modules/views_ui/views_ui.routing.yml index fca076b..d0a04b4 100644 --- a/core/modules/views_ui/views_ui.routing.yml +++ b/core/modules/views_ui/views_ui.routing.yml @@ -118,7 +118,7 @@ views_ui.form.addItem: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\AddItem::getForm' + _content: '\Drupal\views_ui\Form\Ajax\AddItem::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -130,7 +130,7 @@ views_ui.form.editDetails: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\EditDetails::getForm' + _content: '\Drupal\views_ui\Form\Ajax\EditDetails::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -142,7 +142,7 @@ views_ui.form.reorderDisplays: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\ReorderDisplays::getForm' + _content: '\Drupal\views_ui\Form\Ajax\ReorderDisplays::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -154,7 +154,7 @@ views_ui.form.analyze: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\Analyze::getForm' + _content: '\Drupal\views_ui\Form\Ajax\Analyze::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -166,7 +166,7 @@ views_ui.form.rearrange: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\Rearrange::getForm' + _content: '\Drupal\views_ui\Form\Ajax\Rearrange::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -178,7 +178,7 @@ views_ui.form.rearrangeFilter: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\RearrangeFilter::getForm' + _content: '\Drupal\views_ui\Form\Ajax\RearrangeFilter::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -190,7 +190,7 @@ views_ui.form.display: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\Display::getForm' + _content: '\Drupal\views_ui\Form\Ajax\Display::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -202,7 +202,7 @@ views_ui.form.configItem: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\ConfigItem::getForm' + _content: '\Drupal\views_ui\Form\Ajax\ConfigItem::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -214,7 +214,7 @@ views_ui.form.configItemExtra: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\ConfigItemExtra::getForm' + _content: '\Drupal\views_ui\Form\Ajax\ConfigItemExtra::getForm' requirements: _permission: 'administer views' js: 'nojs|ajax' @@ -226,7 +226,7 @@ views_ui.form.configItemGroup: view: tempstore: TRUE defaults: - _controller: '\Drupal\views_ui\Form\Ajax\ConfigItemGroup::getForm' + _content: '\Drupal\views_ui\Form\Ajax\ConfigItemGroup::getForm' form_state: NULL requirements: _permission: 'administer views' diff --git a/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php b/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php index 17f454e..f0a0a5e 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php @@ -34,12 +34,12 @@ public static function getInfo() { * @see \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer::enhancer() */ public function testEnhancer() { - $negotation = $this->getMock('Drupal\core\ContentNegotiation', array('getContentType')); - $negotation->expects($this->any()) - ->method('getContentType') - ->will($this->returnValue('html')); + $form_controller = $this->getMockBuilder('Drupal\Core\Entity\HtmlEntityFormController') + ->disableOriginalConstructor() + ->getMock(); + - $route_enhancer = new EntityRouteEnhancer($negotation); + $route_enhancer = new EntityRouteEnhancer($form_controller); // Set a controller to ensure it is not overridden. $request = new Request(); @@ -47,13 +47,14 @@ public function testEnhancer() { $defaults['_controller'] = 'Drupal\Tests\Core\Controller\TestController::content'; $defaults['_entity_form'] = 'entity_test.default'; $new_defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals($defaults, $new_defaults, '_controller got overridden.'); + $this->assertInstanceOf('\Closure', $new_defaults['_content']); + $this->assertEquals($defaults['_controller'], $new_defaults['_controller'], '_controller got overridden.'); // Set _entity_form and ensure that the form controller is set. $defaults = array(); $defaults['_entity_form'] = 'entity_test.default'; $defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals('\Drupal\Core\Entity\HtmlEntityFormController::content', $defaults['_controller'], 'The entity form controller was not set.'); + $this->assertInstanceOf('\Closure', $new_defaults['_content']); // Set _entity_list and ensure that the entity list controller is set. $defaults = array();