diff --git a/core/core.services.yml b/core/core.services.yml index cd3ac50..4855c56 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -343,16 +343,16 @@ services: class: Drupal\Core\Routing\Enhancer\AjaxEnhancer arguments: ['@content_negotiation'] tags: - - { name: route_enhancer, priority: 20 } - - { name: legacy_route_enhancer, priority: 20 } + - { name: route_enhancer, priority: 15 } + - { name: legacy_route_enhancer, priority: 15 } route_enhancer.entity: class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer - arguments: ['@content_negotiation'] + arguments: ['@controller_resolver', '@plugin.manager.entity'] tags: - - { name: route_enhancer, priority: 15 } + - { name: route_enhancer, priority: 20 } route_enhancer.form: class: Drupal\Core\Routing\Enhancer\FormEnhancer - arguments: ['@content_negotiation'] + arguments: ['@service_container', '@controller_resolver'] tags: - { name: route_enhancer, priority: 10 } route_special_attributes_subscriber: @@ -361,10 +361,19 @@ services: - { name: event_subscriber } controller.page: class: Drupal\Core\Controller\HtmlPageController - arguments: ['@http_kernel', '@controller_resolver', '@string_translation', '@title_resolver'] + arguments: ['@http_kernel', '@controller_resolver', '@string_translation', '@title_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', '@entity.manager'] controller.dialog: class: Drupal\Core\Controller\DialogController - arguments: ['@http_kernel'] + arguments: ['@controller_resolver'] router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener tags: @@ -471,7 +480,7 @@ services: arguments: ['@language_manager', '@string_translation'] exception_controller: class: Drupal\Core\Controller\ExceptionController - arguments: ['@content_negotiation'] + arguments: ['@content_negotiation', '@language_manager', '@string_translation'] calls: - [setContainer, ['@service_container']] exception_listener: diff --git a/core/includes/common.inc b/core/includes/common.inc index df552ed..46550a4 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -376,12 +376,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; + } } /** @@ -3641,7 +3652,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 @@ -3651,10 +3662,13 @@ function drupal_pre_render_dropbutton($element) { * - #show_messages: Suppress drupal_get_message() items. Used by Batch * API (optional). * + * @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) { $main_content_display = &drupal_static('system_main_content_added', FALSE); // Pull out the page title to set it back later. @@ -3691,6 +3705,28 @@ function drupal_render_page($page) { $page['#title'] = $title; } + return $page; +} + +/** + * Renders the page, including all theming. + * + * @param string|array $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). + * + * @return string + * Returns the rendered string. + * + * @see hook_page_alter() + * @see element_info() + */ +function drupal_render_page($page) { + $page = drupal_prepare_page($page); return drupal_render($page); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index b65b71e..fbce68b 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,10 @@ use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Page\Link; +use Drupal\Core\Page\Metatag; +use Drupal\node\NodeInterface; + /** * @defgroup content_flags Content markers * @{ @@ -2484,31 +2489,23 @@ function _template_preprocess_default_variables() { * @see system_elements() */ function template_preprocess_html(&$variables) { - $language_interface = language(Language::TYPE_INTERFACE); + + /** @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.). - $variables['attributes']['class'][] = 'html'; + $body_classes = $variables['attributes']['class']; + $body_classes[] = 'html'; // Add a class that tells us whether we're on the front page or not. - $variables['attributes']['class'][] = $variables['is_front'] ? 'front' : 'not-front'; + $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. - $variables['attributes']['class'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; - - // Add information about the number of sidebars. - if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) { - $variables['attributes']['class'][] = 'two-sidebars'; - } - elseif (!empty($variables['page']['sidebar_first'])) { - $variables['attributes']['class'][] = 'one-sidebar'; - $variables['attributes']['class'][] = 'sidebar-first'; - } - elseif (!empty($variables['page']['sidebar_second'])) { - $variables['attributes']['class'][] = 'one-sidebar'; - $variables['attributes']['class'][] = 'sidebar-second'; - } - else { - $variables['attributes']['class'][] = 'no-sidebars'; - } + $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', '-')) { @@ -2523,42 +2520,30 @@ function template_preprocess_html(&$variables) { } // If on an individual node page, add the node type to body classes. - if ($node = menu_get_object()) { + if (($node = menu_get_object()) && $node instanceof NodeInterface) { $variables['attributes']['class'][] = drupal_html_class('node-type-' . $node->getType()); } - // Initializes attributes which are specific to the html and body elements. - $variables['html_attributes'] = new Attribute; - - // HTML element attributes. - $variables['html_attributes']['lang'] = $language_interface->id; - $variables['html_attributes']['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; - // Add favicon. if (theme_get_setting('features.favicon')) { $favicon = theme_get_setting('favicon.url'); $type = theme_get_setting('favicon.mimetype'); - drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type)); + $link = new Link(drupal_strip_dangerous_protocols($favicon), 'shortcut icon'); + $link->setAttribute('type', $type); + $page->addLink($link); } $site_config = \Drupal::config('system.site'); - // Construct page title. - if (isset($variables['page']['#title'])) { + if ($page->hasTitle()) { $head_title = array( - 'title' => strip_tags($variables['page']['#title']), + 'title' => strip_tags($page->getTitle()), 'name' => String::checkPlain($site_config->get('name')), ); } - elseif (drupal_get_title()) { - $head_title = array( - 'title' => strip_tags(drupal_get_title()), - 'name' => check_plain($site_config->get('name')), - ); - } else { - $head_title = array('name' => check_plain($site_config->get('name'))); + $head_title = array('name' => String::checkPlain($site_config->get('name'))); if ($site_config->get('slogan')) { - $head_title['slogan'] = strip_tags(filter_xss_admin($site_config->get('slogan'))); + $head_title['slogan'] = strip_tags(Xss::filterAdmin($site_config->get('slogan'))); } } @@ -2566,6 +2551,12 @@ function template_preprocess_html(&$variables) { $variables['head_title'] = implode(' | ', $head_title); // Display the html.tpl.php's default mobile metatags for responsive design. + // @todo Add the metatags via drupal_add_html_head for now, so the alter hook + // in drupal_get_html_head() still works. + +// $page->addMetatag(new Metatag('MobileOptimized', 'width')); +// $page->addMetatag(new Metatag('HandheldFriendly', 'true')); +// $page->addMetatag(new Metatag('viewport', 'width=device-width')); $elements = array( 'MobileOptimized' => array( '#tag' => 'meta', @@ -2593,6 +2584,20 @@ function template_preprocess_html(&$variables) { drupal_add_html_head($element, $name); } + // @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(isset($tag['attributes']['name']) ? $tag['attributes']['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; @@ -2600,15 +2605,8 @@ function template_preprocess_html(&$variables) { drupal_add_library('system', 'html5shiv', TRUE); - // Render page_top and page_bottom into top level variables. - $variables['page_top'] = array(); - if (isset($variables['page']['page_top'])) { - $variables['page_top'] = drupal_render($variables['page']['page_top']); - } - $variables['page_bottom'] = array(); - if (isset($variables['page']['page_bottom'])) { - $variables['page_bottom'][]['#markup'] = drupal_render($variables['page']['page_bottom']); - } + $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. @@ -2616,7 +2614,9 @@ function template_preprocess_html(&$variables) { $variables['page_bottom'][] = array('#markup' => $footer_scripts); // Wrap function calls in an object so they can be called when printed. - $variables['head'] = new RenderWrapper('drupal_get_html_head'); + $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'); } @@ -2992,12 +2992,13 @@ function drupal_common_theme() { return array( // From theme.inc. 'html' => array( - 'render element' => 'page', + 'variables' => array('page_object' => NULL), 'template' => 'html', ), 'page' => array( 'render element' => 'page', 'template' => 'page', + 'theme wrapper' => 'html', ), 'region' => array( 'render element' => 'elements', @@ -3066,7 +3067,7 @@ function drupal_common_theme() { ), // From theme.maintenance.inc. 'maintenance_page' => array( - 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'variables' => array('content' => NULL, 'show_messages' => TRUE, 'page' => array()), 'template' => 'maintenance-page', ), 'install_page' => array( diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php index 7b2cece..410b1d0 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,64 @@ 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..36c13e7 100644 --- a/core/lib/Drupal/Core/Controller/DialogController.php +++ b/core/lib/Drupal/Core/Controller/DialogController.php @@ -7,11 +7,13 @@ 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. @@ -19,20 +21,20 @@ 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. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel - * The kernel. + * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver + * The controller resolver service. */ - public function __construct(HttpKernelInterface $kernel) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolverInterface $controller_resolver) { + $this->controllerResolver = $controller_resolver; } /** @@ -75,8 +77,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 +92,81 @@ 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 b03cc8e..cec8c30 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -7,19 +7,24 @@ namespace Drupal\Core\Controller; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\StringTranslation\TranslationInterface; 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 +34,36 @@ 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. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager + * The translation manager. */ - public function __construct(ContentNegotiation $negotiation) { + public function __construct(ContentNegotiation $negotiation, LanguageManager $language_manager, TranslationInterface $translation_manager) { + parent::__construct($language_manager, $translation_manager); $this->negotiation = $negotiation; } @@ -84,14 +112,15 @@ 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 (!$request->query->has('destination')) { $request->query->set('destination', $system_path); } - $subrequest = Request::create('/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); + $subrequest = Request::create('/' . $path, 'get', array('destination' => $system_path, '_exception_statuscode' => 403), $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 @@ -113,15 +142,18 @@ public function on403Html(FlattenException $exception, Request $request) { $response->setStatusCode(403, 'Access denied'); } else { + $page_content = array( + '#markup' => t('You are not authorized to access this page.'), + '#title' => t('Access denied'), + ); + + $page = $this->createHtmlPage($page_content, $request); + $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', array('page_object' => $page)); + $response = new Response($output, $page->getStatusCode()); - $response = new Response($content, 403); + return $response; } return $response; @@ -164,7 +196,7 @@ public function on404Html(FlattenException $exception, Request $request) { // 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()); + $subrequest = Request::create('/' . $path, 'get', array('_exception_statuscode' => 403), $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 @@ -186,14 +218,16 @@ 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())), + '#title' => t('Page not found'), + ); + + $page = $this->createHtmlPage($page_content, $request); + $page->setStatusCode(404); + + $output = theme('html', array('page_object' => $page)); + $response = new Response($output, $page->getStatusCode()); } return $response; @@ -252,16 +286,16 @@ 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.'), + '#page' => array( + '#title' => t('Error'), + ), ); - $output = drupal_render($maintenance_page); - $response = new Response($output, 500); + $output = drupal_render($page_content); + $response = new Response($output); $response->setStatusCode(500, '500 Service unavailable (with message)'); return $response; diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php new file mode 100644 index 0000000..c99e8fc --- /dev/null +++ b/core/lib/Drupal/Core/Controller/FormController.php @@ -0,0 +1,87 @@ +resolver = $resolver; + } + + /** + * Returns the result of invoking the form. + * + * @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) { + $form_object = $this->getFormObject($request, $this->formDefinition); + + // Using reflection, find all of the parameters needed by the form in the + // request attributes, skipping $form and $form_state. + + // At the form and form_state to trick the getArguments method of the + // controller resolver. + $form_state = array(); + $request->attributes->set('form', array()); + $request->attributes->set('form_state', $form_state); + $args = $this->resolver->getArguments($request, array($form_object, 'buildForm')); + $request->attributes->remove('form'); + $request->attributes->remove('form_state'); + + // Remove $form and $form_state from the arguments, and re-index them. + unset($args[0], $args[1]); + $form_state['build_info']['args'] = array_values($args); + + $form_id = _drupal_form_id($form_object, $form_state); + return drupal_build_form($form_id, $form_state); + } + + /** + * Returns the object used to build the form. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request using this form. + * @param string $form_arg + * Either a class name or a service ID. + * + * @return \Drupal\Core\Form\FormInterface + * The form object to use. + */ + abstract protected function getFormObject(Request $request, $form_arg); + +} diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php new file mode 100644 index 0000000..297e2c6 --- /dev/null +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php @@ -0,0 +1,152 @@ +languageManager = $language_manager; + $this->translationManager = $translation_manager; + $this->titleResolver = $title_resolver; + } + + /** + * 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, + ); + } + + $route_title = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); + + $page = new HtmlPage('', $route_title); + + // If no title was returned fall back to one defined in the route. + if (!isset($page_content['#title']) && $route_title) { + $page_content['#title'] = $route_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'], PASS_THROUGH); + unset($page_array['#title']); + } + elseif (!$page->hasTitle()) { + $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/HtmlFormController.php b/core/lib/Drupal/Core/Controller/HtmlFormController.php index 8b3e70a..246611a 100644 --- a/core/lib/Drupal/Core/Controller/HtmlFormController.php +++ b/core/lib/Drupal/Core/Controller/HtmlFormController.php @@ -2,70 +2,39 @@ /** * @file - * Contains \Drupal\Core\Controller\HtmlFormController. + * Contains \Drupal\Core\Controler\HtmlFormController. */ 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 HtmlFormController extends FormController { /** - * The injection container for this object. + * The service container. * * @var \Symfony\Component\DependencyInjection\ContainerInterface */ protected $container; /** - * Injects the service container used by this object. + * The name of a class implementing FormInterface that defines a form. * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * The service container this object should use. + * @var string */ - public function setContainer(ContainerInterface $container = NULL) { - $this->container = $container; - } + protected $formClass; /** - * 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. + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. */ - public function content(Request $request, $_form) { - $form_object = $this->getFormObject($request, $_form); - - // Using reflection, find all of the parameters needed by the form in the - // request attributes, skipping $form and $form_state. - - // At the form and form_state to trick the getArguments method of the - // controller resolver. - $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')); - $request->attributes->remove('form'); - $request->attributes->remove('form_state'); - - // Remove $form and $form_state from the arguments, and re-index them. - unset($args[0], $args[1]); - $form_state['build_info']['args'] = array_values($args); - - $form_id = _drupal_form_id($form_object, $form_state); - return drupal_build_form($form_id, $form_state); + public function __construct(ControllerResolverInterface $resolver, ContainerInterface $container, $class) { + parent::__construct($resolver); + $this->container = $container; + $this->formDefinition = $class; } /** diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php index 4525328..ebec111 100644 --- a/core/lib/Drupal/Core/Controller/HtmlPageController.php +++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Controller; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\StringTranslation\TranslationInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; @@ -16,14 +17,7 @@ /** * 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,20 +27,6 @@ class HtmlPageController { protected $controllerResolver; /** - * The translation manager service. - * - * @var \Drupal\Core\StringTranslation\TranslationInterface - */ - protected $translationManager; - - /** - * The title resolver. - * - * @var \Drupal\Core\Controller\TitleResolver - */ - protected $titleResolver; - - /** * Constructs a new HtmlPageController. * * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel @@ -56,12 +36,12 @@ class HtmlPageController { * The translation manager. * @param \Drupal\Core\Controller\TitleResolver $title_resolver * The title resolver. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. */ - public function __construct(HttpKernelInterface $kernel, ControllerResolverInterface $controller_resolver, TranslationInterface $translation_manager, TitleResolver $title_resolver) { - $this->httpKernel = $kernel; + public function __construct(HttpKernelInterface $kernel, ControllerResolverInterface $controller_resolver, TranslationInterface $translation_manager, TitleResolver $title_resolver, LanguageManager $language_manager) { + parent::__construct($language_manager, $translation_manager, $title_resolver); $this->controllerResolver = $controller_resolver; - $this->translationManager = $translation_manager; - $this->titleResolver = $title_resolver; } /** @@ -76,36 +56,32 @@ 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, - ); - } - if (!isset($page_content['#title'])) { - $title = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); - // Ensure that #title will not be set if no title was returned. - if (isset($title)) { - $page_content['#title'] = $title; - } - } - - $response = new Response(drupal_render_page($page_content)); - return $response; + $page_content = $this->getContentResult($request, $_content); + return $this->createHtmlPage($page_content, $request); } /** - * Translates a string to the current language or to a given language. + * 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. * - * See the t() documentation for details. + * @return array + * The render array that results from invoking the controller. */ - protected function t($string, array $args = array(), array $options = array()) { - return $this->translationManager->translate($string, $args, $options); + 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/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php index 2cf8d47..a3512c7 100644 --- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php +++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php @@ -10,7 +10,9 @@ 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; +use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Entity\EntityManager; /** * Enhances an entity form route with the appropriate controller. @@ -18,38 +20,42 @@ class EntityRouteEnhancer implements RouteEnhancerInterface { /** - * Content negotiation library. + * The controller resolver. * - * @var \Drupal\Core\ContentNegotiation + * @var \Drupal\Core\Controller\ControllerResolverInterface */ - protected $negotiation; + protected $resolver; /** - * Constructs a new \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer. + * The entity manager service. * - * @param \Drupal\Core\ContentNegotiation $negotiation - * The content negotiation library. + * @var \Drupal\Core\Entity\EntityManager */ - public function __construct(ContentNegotiation $negotiation) { - $this->negotiation = $negotiation; + protected $manager; + + /** + * Constructs a new EntityRouteEnhancer object. + */ + public function __construct(ControllerResolverInterface $resolver, EntityManager $manager) { + $this->resolver = $resolver; + $this->manager = $manager; } /** * {@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'; + $wrapper = new HtmlEntityFormController($this->resolver, $this->manager, $defaults['_entity_form']); + $defaults['_content'] = array($wrapper, 'getContentResult'); } elseif (!empty($defaults['_entity_list'])) { - $defaults['_controller'] = 'controller.page:content'; $defaults['_content'] = '\Drupal\Core\Entity\Controller\EntityListController::listing'; $defaults['entity_type'] = $defaults['_entity_list']; unset($defaults['_entity_list']); } elseif (!empty($defaults['_entity_view'])) { - $defaults['_controller'] = 'controller.page:content'; $defaults['_content'] = '\Drupal\Core\Entity\Controller\EntityViewController::view'; if (strpos($defaults['_entity_view'], '.') !== FALSE) { // The _entity_view entry is of the form entity_type.view_mode. diff --git a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php index 4aa7925..d3ac1fb 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 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, EntityManager $manager, $form_definition) { + parent::__construct($resolver); + $this->manager = $manager; + $this->formDefinition = $form_definition; } /** @@ -46,8 +55,6 @@ public function content(Request $request, $_entity_form) { * @endcode */ protected function getFormObject(Request $request, $form_arg) { - $manager = $this->container->get('entity.manager'); - // If no operation is provided, use 'default'. $form_arg .= '.default'; list ($entity_type, $operation) = explode('.', $form_arg); @@ -56,10 +63,10 @@ protected function getFormObject(Request $request, $form_arg) { $entity = $request->attributes->get($entity_type); } else { - $entity = $manager->getStorageController($entity_type)->create(array()); + $entity = $this->manager->getStorageController($entity_type)->create(array()); } - return $manager->getFormController($entity_type, $operation)->setEntity($entity); + return $this->manager->getFormController($entity_type, $operation)->setEntity($entity); } } 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/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php index 39828ef..603174c 100644 --- a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php @@ -46,7 +46,7 @@ public function onKernelRequestLegacy(GetResponseEvent $event) { * An array of event listener definitions. */ static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 90); + $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 31); return $events; } diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php index ae1d083..afcfa95 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', array('page_object' => $page)); + + $response = new Response((string) $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,28 +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, - ); - } - - // If no title was returned fall back to one defined in the route. - if (!isset($page_result['#title']) && $request->attributes->has('_title')) { - $page_result['#title'] = $request->attributes->get('_title'); - } - - $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,29 +145,13 @@ public function onIframeUpload(GetResponseForControllerResultEvent $event) { } /** - * 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\GetResponseForControllerResultEvent $event - * The Event to process. - */ - public function onHtml(GetResponseForControllerResultEvent $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('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..130eb00 --- /dev/null +++ b/core/lib/Drupal/Core/Page/HeadElement.php @@ -0,0 +1,58 @@ +attributes); + $rendered = (string) $attributes; + + 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..73e7bcf --- /dev/null +++ b/core/lib/Drupal/Core/Page/HtmlFragment.php @@ -0,0 +1,124 @@ +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 : String::checkPlain($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..9b4a794 --- /dev/null +++ b/core/lib/Drupal/Core/Page/Link.php @@ -0,0 +1,40 @@ +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..58e3562 --- /dev/null +++ b/core/lib/Drupal/Core/Page/Metatag.php @@ -0,0 +1,37 @@ +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..4fcd9ea 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php @@ -37,9 +37,9 @@ public function __construct(ContentNegotiation $negotiation) { * {@inheritdoc} */ public function enhance(array $defaults, Request $request) { - if (empty($defaults['_content']) && $this->negotiation->getContentType($request) == 'drupal_ajax') { + if (empty($defaults['_content']) && $defaults['_controller'] != 'controller.ajax: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..cdb9b4e 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface; -use Drupal\Core\ContentNegotiation; +use Drupal\Core\Controller\HtmlFormController; +use Drupal\Core\Controller\ControllerResolverInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Enhances a form route with the appropriate controller. @@ -17,28 +19,34 @@ class FormEnhancer implements RouteEnhancerInterface { /** - * Content negotiation library. + * The service container. * - * @var \Drupal\CoreContentNegotiation + * @var \Symfony\Component\DependencyInjection\ContainerInterface */ - protected $negotiation; + protected $container; /** - * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. + * The controller resolver. * - * @param \Drupal\Core\ContentNegotiation $negotiation - * The Content Negotiation service. + * @var \Drupal\Core\Controller\ControllerResolverInterface + */ + protected $resolver; + + /** + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. */ - public function __construct(ContentNegotiation $negotiation) { - $this->negotiation = $negotiation; + public function __construct(ContainerInterface $container, ControllerResolverInterface $resolver) { + $this->container = $container; + $this->resolver = $resolver; } /** * {@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'])) { + $wrapper = new HtmlFormController($this->resolver, $this->container, $defaults['_form']); + $defaults['_content'] = array($wrapper, 'getContentResult'); } return $defaults; } diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml index e05c4d9..7764f4b 100644 --- a/core/modules/aggregator/aggregator.routing.yml +++ b/core/modules/aggregator/aggregator.routing.yml @@ -72,7 +72,7 @@ aggregator.feed_view: aggregator.page_last: path: '/aggregator' defaults: - _controller: '\Drupal\aggregator\Controller\AggregatorController::pageLast' + _content: '\Drupal\aggregator\Controller\AggregatorController::pageLast' requirements: _permission: 'access news feeds' diff --git a/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php b/core/modules/block/lib/Drupal/block/Routing/RouteSubscriber.php index f60e51a..9a66413 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 7a2689c..6de86a7 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: path: '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' diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index b6194a8..dd6749b 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -195,7 +195,12 @@ function overlay_page_alter(&$page) { $mode = overlay_get_mode(); if ($mode == 'child') { // Add the overlay wrapper before the html wrapper. - array_unshift($page['#theme_wrappers'], 'overlay'); + if (isset($page['#theme_wrappers'])) { + array_unshift($page['#theme_wrappers'], 'overlay'); + } + else { + $page['#theme_wrappers'] = array('overlay'); + } } elseif ($mode == 'parent' && ($message = overlay_disable_message())) { $page['page_top']['disable_overlay'] = $message; diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php index ab3f913..328d385 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php @@ -42,6 +42,7 @@ function setUp() { } function testSearchPageHook() { + $this->container->get('router.builder')->rebuild(); $keys = 'bike shed ' . $this->randomName(); $this->drupalGet("search/dummy_path/{$keys}"); $this->assertText('Dummy search snippet', 'Dummy search snippet is shown'); diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ExceptionControllerTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ExceptionControllerTest.php index 939432d..32359cc 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/ExceptionControllerTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/ExceptionControllerTest.php @@ -8,6 +8,9 @@ use \Drupal\Core\ContentNegotiation; use \Drupal\Core\Controller\ExceptionController; +use Drupal\Core\KeyValueStore\MemoryStorage; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\StringTranslation\TranslationManager; use \Drupal\simpletest\UnitTestBase; use \Symfony\Component\HttpFoundation\Request; use \Symfony\Component\HttpKernel\Exception\FlattenException; @@ -31,7 +34,7 @@ public static function getInfo() { public function test405HTML() { $exception = new \Exception('Test exception'); $flat_exception = FlattenException::create($exception, 405); - $exception_controller = new ExceptionController(new ContentNegotiation()); + $exception_controller = new ExceptionController(new ContentNegotiation(), new LanguageManager(new MemoryStorage('state')), new TranslationManager()); $response = $exception_controller->execute($flat_exception, new Request()); $this->assertEqual($response->getStatusCode(), 405, 'HTTP status of response is correct.'); $this->assertEqual($response->getContent(), 'Method Not Allowed', 'HTTP response body is correct.'); diff --git a/core/modules/system/lib/Drupal/system/Tests/System/PageTitleTest.php b/core/modules/system/lib/Drupal/system/Tests/System/PageTitleTest.php index b22fd90..8fd3594 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/PageTitleTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/PageTitleTest.php @@ -135,13 +135,6 @@ public function testRoutingTitle() { $result = $this->xpath('//h1'); $this->assertEqual('Foo', (string) $result[0]); - // Test a controller using _controller instead of _content. - $this->drupalGet('test-render-title-controller'); - - $this->assertTitle('Foo | Drupal'); - $result = $this->xpath('//h1'); - $this->assertEqual('Foo', (string) $result[0]); - // Test forms $this->drupalGet('form-test/object-builder'); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index af72abe..bbb4411 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; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 96d6e70..449cb9f 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -260,7 +260,6 @@ function system_element_info() { $types['page'] = array( '#show_messages' => TRUE, '#theme' => 'page', - '#theme_wrappers' => array('html'), ); // 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. diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 7b284ae..f336b9b 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -205,7 +205,7 @@ system.date_format_edit: system.date_format_language_overview: path: '/admin/config/regional/date-time/locale' defaults: - _controller: '\Drupal\system\Controller\DateFormatLanguageController::overviewPage' + _content: '\Drupal\system\Controller\DateFormatLanguageController::overviewPage' requirements: _permission: 'administer site configuration' @@ -255,7 +255,7 @@ system.theme_enable: system.status: path: '/admin/reports/status' defaults: - _controller: 'Drupal\system\Controller\SystemInfoController::status' + _content: 'Drupal\system\Controller\SystemInfoController::status' requirements: _permission: 'administer site configuration' @@ -348,7 +348,7 @@ system.admin_config: system.batch_page: path: '/batch' defaults: - _controller: '\Drupal\system\Controller\BatchController::batchPage' + _content: '\Drupal\system\Controller\BatchController::batchPage' requirements: _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php index cc63d39..3009600 100644 --- a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php @@ -55,7 +55,7 @@ public function alterRoutes(RouteBuildEvent $event) { $collection = $event->getRouteCollection(); $route = $collection->get('router_test.6'); // Change controller method from test1 to test5. - $route->setDefault('_controller', '\Drupal\router_test\TestControllers::test5'); + $route->setDefault('_content', '\Drupal\router_test\TestControllers::test5'); } } } 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 0aab01a..817da71 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: path: '/router_test/test6' defaults: - _controller: '\Drupal\router_test\TestControllers::test1' + _content: '\Drupal\router_test\TestControllers::test1' requirements: _access: 'TRUE' diff --git a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php index 44e2882..f27fe0d 100644 --- a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php +++ b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php @@ -49,7 +49,7 @@ public function onKernelResponseSessionTest(FilterResponseEvent $event) { // https.php because form submissions would otherwise redirect to a // non-existent HTTPS site. if (!empty($is_https_mock)) { - $path = $base_insecure_url . '/' . $event->getTargetUrl(); + $path = $base_insecure_url . '/' . $response->getTargetUrl(); $response->setTargetUrl($path); } } diff --git a/core/modules/system/tests/modules/session_test/session_test.routing.yml b/core/modules/system/tests/modules/session_test/session_test.routing.yml index 1e7b127..17eafeb 100644 --- a/core/modules/system/tests/modules/session_test/session_test.routing.yml +++ b/core/modules/system/tests/modules/session_test/session_test.routing.yml @@ -2,7 +2,7 @@ session_test.get: path: '/session-test/get' defaults: _title: 'Session value' - _controller: '\Drupal\session_test\Controller\SessionTestController::get' + _content: '\Drupal\session_test\Controller\SessionTestController::get' requirements: _permission: 'access content' @@ -10,7 +10,7 @@ session_test.id: path: '/session-test/id' defaults: _title: 'Session ID' - _controller: '\Drupal\session_test\Controller\SessionTestController::getId' + _content: '\Drupal\session_test\Controller\SessionTestController::getId' requirements: _permission: 'access content' @@ -18,7 +18,7 @@ session_test.id_from_cookie: path: '/session-test/id-from-cookie' defaults: _title: 'Session ID from cookie' - _controller: '\Drupal\session_test\Controller\SessionTestController::getIdFromCookie' + _content: '\Drupal\session_test\Controller\SessionTestController::getIdFromCookie' requirements: _permission: 'access content' @@ -26,7 +26,7 @@ session_test.set: path: '/session-test/set/{test_value}' defaults: _title: 'Set session value' - _controller: '\Drupal\session_test\Controller\SessionTestController::set' + _content: '\Drupal\session_test\Controller\SessionTestController::set' options: converters: test_value: '\s+' @@ -37,7 +37,7 @@ session_test.no_set: path: '/session-test/no-set/{test_value}' defaults: _title: 'Set session value but do not save session' - _controller: '\Drupal\session_test\Controller\SessionTestController::noSet' + _content: '\Drupal\session_test\Controller\SessionTestController::noSet' options: converters: test_value: '\s+' @@ -48,7 +48,7 @@ session_test.set_message: path: '/session-test/set-message' defaults: _title: 'Set message' - _controller: '\Drupal\session_test\Controller\SessionTestController::setMessage' + _content: '\Drupal\session_test\Controller\SessionTestController::setMessage' requirements: _permission: 'access content' @@ -56,7 +56,7 @@ session_test.set_message_but_dont_save: path: '/session-test/set-message-but-dont-save' defaults: _title: 'Set message but do not save session' - _controller: '\Drupal\session_test\Controller\SessionTestController::setMessageButDontSave' + _content: '\Drupal\session_test\Controller\SessionTestController::setMessageButDontSave' requirements: _permission: 'access content' @@ -64,7 +64,7 @@ session_test.set_not_started: path: '/session-test/set-not-started' defaults: _title: 'Set message when session is not started' - _controller: '\Drupal\session_test\Controller\SessionTestController::setNotStarted' + _content: '\Drupal\session_test\Controller\SessionTestController::setNotStarted' requirements: _permission: 'access content' @@ -72,6 +72,6 @@ session_test.is_logged_in: path: '/session-test/is-logged-in' defaults: _title: 'Check if user is logged in' - _controller: '\Drupal\session_test\Controller\SessionTestController::isLoggedIn' + _content: '\Drupal\session_test\Controller\SessionTestController::isLoggedIn' requirements: _user_is_logged_in: 'TRUE' diff --git a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/PageCacheAcceptHeaderController.php b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/PageCacheAcceptHeaderController.php index cc3b231..43cded1 100644 --- a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/PageCacheAcceptHeaderController.php +++ b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/PageCacheAcceptHeaderController.php @@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; /** * Defines a controller to respond the page cache accept header test. @@ -28,7 +29,7 @@ public function content(Request $request) { return new JsonResponse(array('content' => 'oh hai this is json')); } else { - return "

oh hai this is html.

"; + return new Response("

oh hai this is html.

"); } } } diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml index 1391481..60e88b8 100644 --- a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml +++ b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml @@ -27,10 +27,3 @@ test_page_test.admin_render_title: _content: 'Drupal\test_page_test\Controller\Test::renderTitle' requirements: _access: 'TRUE' - -test_page_test.render_title_controller: - path: "/test-render-title-controller" - defaults: - _controller: 'Drupal\test_page_test\Controller\Test::renderTitle' - requirements: - _access: 'TRUE' diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 08e9c01..8983733 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -83,10 +83,7 @@ function theme_test_preprocess_html(&$variables) { $variables['html_attributes']['theme_test_html_attribute'] = 'theme test html attribute value'; $variables['attributes']['theme_test_body_attribute'] = 'theme test body attribute value'; - // Check that the page variable is not flattened yet. - if (is_array($variables['page'])) { - $variables['attributes']['theme_test_page_variable'] = 'Page variable is an array.'; - } + $variables['attributes']['theme_test_page_variable'] = 'Page variable is an array.'; } /** diff --git a/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php b/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php index e3dbde3..0bda4e8 100644 --- a/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php +++ b/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php @@ -7,13 +7,16 @@ namespace Drupal\views\EventSubscriber; +use Drupal\Core\Page\HtmlPage; use Drupal\Core\Routing\RouteBuildEvent; use Drupal\Core\Routing\RoutingEvents; use Drupal\views\Plugin\views\display\DisplayRouterInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; +use Symfony\Component\HttpKernel\KernelEvents; /** - * Builds up the routes of all views. + * Builds up the routes of all views and changes the htmlpage response code. */ class RouteSubscriber implements EventSubscriberInterface { @@ -22,10 +25,28 @@ class RouteSubscriber implements EventSubscriberInterface { */ public static function getSubscribedEvents() { $events[RoutingEvents::DYNAMIC] = 'dynamicRoutes'; + $events[KernelEvents::VIEW][] = array('onHtmlPage', 20); return $events; } /** + * Sets the proper response code coming from the http status area handler. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The Event to process. + * + * @see \Drupal\views\Plugin\views\area\HTTPStatusCode + */ + public function onHtmlPage(GetResponseForControllerResultEvent $event) { + $page = $event->getControllerResult(); + if ($page instanceof HtmlPage) { + if (($request = $event->getRequest()) && $request->attributes->has('view_id')) { + $page->setStatusCode($request->attributes->get('_http_statuscode', 200)); + }; + } + } + + /** * Adds routes defined by all views. * * @param \Drupal\Core\Routing\RouteBuildEvent $event diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/HTTPStatusCode.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/HTTPStatusCode.php index d448876..fbeadbf 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/area/HTTPStatusCode.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/HTTPStatusCode.php @@ -65,6 +65,7 @@ public function buildOptionsForm(&$form, &$form_state) { function render($empty = FALSE) { if (!$empty || !empty($this->options['empty'])) { $this->view->getResponse()->setStatusCode($this->options['status_code']); + $this->view->getRequest()->attributes->set('_http_statuscode', $this->options['status_code']); } } 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 75758e7..45cfd6d 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\views\Annotation\ViewsDisplay; +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/lib/Drupal/views/Routing/ViewPageController.php b/core/modules/views/lib/Drupal/views/Routing/ViewPageController.php index 8da43f0..b14bd3c 100644 --- a/core/modules/views/lib/Drupal/views/Routing/ViewPageController.php +++ b/core/modules/views/lib/Drupal/views/Routing/ViewPageController.php @@ -68,6 +68,7 @@ public function handle(Request $request) { throw new NotFoundHttpException(format_string('Page controller for view %id requested, but view was not found.', array('%id' => $view_id))); } $view = $this->executableFactory->get($entity); + $view->setRequest($request); $view->setDisplay($display_id); $view->initHandlers(); diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php index d4806f0..49e3205 100644 --- a/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php @@ -25,7 +25,7 @@ class ViewPageControllerTest extends ViewUnitTestBase { * * @var array */ - public static $modules = array('user'); + public static $modules = array('user', 'menu_link'); /** * Views used by this test. @@ -55,7 +55,8 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('system', array('router', 'menu_router')); + $this->installSchema('system', 'menu_router'); + $this->installSchema('menu_link', 'menu_links'); $this->pageController = new ViewPageController($this->container->get('entity.manager')->getStorageController('view'), new ViewExecutableFactory()); } @@ -88,7 +89,8 @@ public function testPageController() { $request->attributes->set('display_id', 'page_1'); $output = $this->pageController->handle($request); - $this->assertTrue($output instanceof Response, 'Ensure the page display returns a response object.'); + $this->assertTrue(is_array($output)); + $this->assertEqual($output['#view']->storage->id, 'test_page_view', 'The right view was executed.'); } } diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php index d8236cf..3ee7d61 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -10,6 +10,7 @@ use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\ViewStorageInterface; use Drupal\Component\Utility\Tags; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -334,6 +335,13 @@ class ViewExecutable { protected $response = NULL; /** + * Stores the current request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** * Does this view already have loaded it's handlers. * * @todo Group with other static properties. @@ -1580,7 +1588,7 @@ public function setResponse(Response $response) { /** * Gets the response object used by the view. * - * @return Symfony\Component\HttpFoundation\Response + * @return \Symfony\Component\HttpFoundation\Response * The response object of the view. */ public function getResponse() { @@ -1591,6 +1599,26 @@ public function getResponse() { } /** + * Sets the request object. + * + * @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 + * Returns the request object. + */ + public function getRequest() { + return $this->request; + } + + /** * Get the view's current title. This can change depending upon how it * was built. */ diff --git a/core/modules/views/views.module b/core/modules/views/views.module index d8e0712..60a80fc 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -12,6 +12,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Template\AttributeArray; use Drupal\views\ViewExecutable; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\views\Entity\View; @@ -489,7 +490,7 @@ function views_preprocess_html(&$variables) { // the "contextual-region" class from the tag here and add // JavaScript that will insert it back in the correct place. if (!empty($variables['page']['#views_contextual_links'])) { - $key = array_search('contextual-region', $variables['attributes']['class']); + $key = array_search('contextual-region', $variables['attributes']['class'] instanceof AttributeArray ? $variables['attributes']['class']->value() : $variables['attributes']['class']); if ($key !== FALSE) { unset($variables['attributes']['class'][$key]); $variables['attributes']['data-views-page-contextual-id'] = $variables['title_suffix']['contextual_links']['#id']; diff --git a/core/modules/views_ui/views_ui.routing.yml b/core/modules/views_ui/views_ui.routing.yml index 3efc9ea..7dd7f66 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_add_item: 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_edit_details: 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_reorder_displays: 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_rearrange_filter: 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_config_item: 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_config_item_extra: 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_config_item_group: 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..e880af9 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php @@ -34,12 +34,13 @@ 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')); + $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); + $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManager') + ->disableOriginalConstructor() + ->getMock(); - $route_enhancer = new EntityRouteEnhancer($negotation); + + $route_enhancer = new EntityRouteEnhancer($controller_resolver, $entity_manager); // Set a controller to ensure it is not overridden. $request = new Request(); @@ -47,29 +48,32 @@ 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->assertTrue(is_callable($new_defaults['_content'])); + $this->assertInstanceOf('\Drupal\Core\Entity\HtmlEntityFormController', $new_defaults['_content'][0]); + $this->assertEquals($new_defaults['_content'][1], 'getContentResult'); + $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.'); + $new_defaults = $route_enhancer->enhance($defaults, $request); + $this->assertTrue(is_callable($new_defaults['_content'])); + $this->assertInstanceOf('\Drupal\Core\Entity\HtmlEntityFormController', $new_defaults['_content'][0]); + $this->assertEquals($new_defaults['_content'][1], 'getContentResult'); // Set _entity_list and ensure that the entity list controller is set. $defaults = array(); $defaults['_entity_list'] = 'entity_test.default'; - $defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals('controller.page:content', $defaults['_controller']); - $this->assertEquals('\Drupal\Core\Entity\Controller\EntityListController::listing', $defaults['_content'], 'The entity list controller was not set.'); - $this->assertEquals('entity_test.default', $defaults['entity_type']); - $this->assertFalse(isset($defaults['_entity_list'])); + $new_defaults = $route_enhancer->enhance($defaults, $request); + $this->assertEquals('\Drupal\Core\Entity\Controller\EntityListController::listing', $new_defaults['_content'], 'The entity list controller was not set.'); + $this->assertEquals('entity_test.default', $new_defaults['entity_type']); + $this->assertFalse(isset($new_defaults['_entity_list'])); // Set _entity_view and ensure that the entity view controller is set. $defaults = array(); $defaults['_entity_view'] = 'entity_test.full'; $defaults['entity_test'] = 'Mock entity'; $defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals('controller.page:content', $defaults['_controller']); $this->assertEquals('\Drupal\Core\Entity\Controller\EntityViewController::view', $defaults['_content'], 'The entity view controller was not set.'); $this->assertEquals($defaults['_entity'], 'Mock entity'); $this->assertEquals($defaults['view_mode'], 'full'); @@ -93,7 +97,6 @@ public function testEnhancer() { $defaults[RouteObjectInterface::ROUTE_OBJECT] = $route; $defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals('controller.page:content', $defaults['_controller']); $this->assertEquals('\Drupal\Core\Entity\Controller\EntityViewController::view', $defaults['_content'], 'The entity view controller was not set.'); $this->assertEquals($defaults['_entity'], 'Mock entity'); $this->assertEquals($defaults['view_mode'], 'full'); @@ -104,7 +107,6 @@ public function testEnhancer() { $defaults['_entity_view'] = 'entity_test'; $defaults['entity_test'] = 'Mock entity'; $defaults = $route_enhancer->enhance($defaults, $request); - $this->assertEquals('controller.page:content', $defaults['_controller']); $this->assertEquals('\Drupal\Core\Entity\Controller\EntityViewController::view', $defaults['_content'], 'The entity view controller was not set.'); $this->assertEquals($defaults['_entity'], 'Mock entity'); $this->assertTrue(empty($defaults['view_mode']));