core/authorize.php | 3 +- core/core.services.yml | 56 ++-- core/includes/batch.inc | 3 +- core/includes/common.inc | 141 ---------- core/includes/errors.inc | 3 +- core/includes/install.core.inc | 3 +- core/includes/menu.inc | 10 +- core/includes/theme.inc | 91 +++---- core/lib/Drupal/Core/ContentNegotiation.php | 1 + .../Drupal/Core/Controller/DialogController.php | 157 ------------ .../Drupal/Core/Controller/HtmlControllerBase.php | 82 ------ .../Drupal/Core/Controller/HtmlPageController.php | 81 ------ core/lib/Drupal/Core/CoreServiceProvider.php | 3 + .../Core/Display/Annotation/DisplayVariant.php | 6 +- .../ContentControllerSubscriber.php | 37 ++- .../ContentFormControllerSubscriber.php | 4 +- .../EventSubscriber/DefaultExceptionSubscriber.php | 59 +---- .../Core/EventSubscriber/HtmlViewSubscriber.php | 110 -------- .../EventSubscriber/MainContentViewSubscriber.php | 109 ++++++++ .../EventSubscriber/MaintenanceModeSubscriber.php | 21 +- .../Drupal/Core/EventSubscriber/ViewSubscriber.php | 40 +-- .../Core/Page/DefaultHtmlFragmentRenderer.php | 155 ----------- .../Drupal/Core/Page/DefaultHtmlPageRenderer.php | 125 --------- core/lib/Drupal/Core/Page/FeedLinkElement.php | 32 --- core/lib/Drupal/Core/Page/HeadElement.php | 100 -------- core/lib/Drupal/Core/Page/HtmlFragment.php | 229 ----------------- .../lib/Drupal/Core/Page/HtmlFragmentInterface.php | 66 ----- .../Core/Page/HtmlFragmentRendererInterface.php | 38 --- core/lib/Drupal/Core/Page/HtmlPage.php | 234 ----------------- .../Drupal/Core/Page/HtmlPageRendererInterface.php | 29 --- core/lib/Drupal/Core/Page/LinkElement.php | 40 --- core/lib/Drupal/Core/Page/MetaElement.php | 81 ------ core/lib/Drupal/Core/Page/RenderHtmlRenderer.php | 88 ------- .../Core/Page/RenderHtmlRendererInterface.php | 31 --- .../Drupal/Core/Render/BareHtmlPageRenderer.php | 78 ++++++ .../Core/Render/BareHtmlPageRendererInterface.php | 73 ++++++ core/lib/Drupal/Core/Render/Element/Html.php | 107 +++++++- core/lib/Drupal/Core/Render/Element/Page.php | 6 +- .../MainContent/AjaxRenderer.php} | 85 +----- .../Core/Render/MainContent/DialogRenderer.php | 92 +++++++ .../Core/Render/MainContent/HtmlRenderer.php | 284 +++++++++++++++++++++ .../MainContent/MainContentRendererInterface.php | 38 +++ .../MainContent/MainContentRenderersPass.php | 33 +++ .../Core/Render/MainContent/ModalRenderer.php | 42 +++ .../src/Authentication/Provider/BasicAuth.php | 2 +- core/modules/block/block.module | 36 +-- core/modules/block/block.services.yml | 5 + .../BlockPageDisplayVariantSubscriber.php | 64 +++++ .../{FullPageVariant.php => BlockPageVariant.php} | 12 +- .../Plugin/DisplayVariant/DemoBlockPageVariant.php | 134 ++++++++++ ...ageVariantTest.php => BlockPageVariantTest.php} | 10 +- .../modules/node/src/Tests/Views/FrontPageTest.php | 2 +- core/modules/rest/src/Tests/ReadTest.php | 8 +- core/modules/rest/src/Tests/ResourceTest.php | 12 +- core/modules/simpletest/simpletest.module | 1 - .../system/src/Controller/BatchController.php | 92 +------ .../system/src/Controller/DbUpdateController.php | 19 +- .../src/Event/PageDisplayVariantSelectionEvent.php | 55 ++++ core/modules/system/src/Event/SystemEvents.php | 23 ++ .../Plugin/DisplayVariant/SimplePageVariant.php | 47 ++++ .../system/src/Tests/Common/AddFeedTest.php | 18 +- .../system/src/Tests/Common/PageRenderTest.php | 7 +- .../src/Tests/System/MainContentFallbackTest.php | 85 ------ core/modules/system/system.routing.yml | 4 +- core/modules/system/templates/html.html.twig | 14 +- .../ajax_test/src/Form/AjaxTestDialogForm.php | 2 +- .../modules/ajax_test/src/Form/AjaxTestForm.php | 2 +- .../src/EventSubscriber/HtmlPageSubscriber.php | 45 ---- .../system_module_test/system_module_test.module | 20 ++ .../system_module_test.services.yml | 5 - .../tests/modules/system_test/system_test.module | 13 - core/modules/system/theme.api.php | 74 ++++++ core/modules/views/views.module | 24 +- ...AjaxControllerTest.php => AjaxRendererTest.php} | 58 +---- core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php | 62 ----- core/themes/bartik/bartik.theme | 56 ++-- core/themes/seven/css/theme/install-page.css | 2 +- core/themes/seven/css/theme/maintenance-page.css | 2 +- core/themes/seven/seven.theme | 55 ++-- 79 files changed, 1574 insertions(+), 2602 deletions(-) diff --git a/core/authorize.php b/core/authorize.php index 4b0f249..22bede6 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -25,7 +25,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Drupal\Core\Site\Settings; -use Drupal\Core\Page\DefaultHtmlPageRenderer; // Change the directory to the Drupal root. chdir('..'); @@ -150,7 +149,7 @@ function authorize_access_allowed() { if (!empty($output)) { $response->headers->set('Content-Type', 'text/html; charset=utf-8'); - $response->setContent(DefaultHtmlPageRenderer::renderPage($output, $page_title, 'maintenance', array( + $response->setContent(\Drupal::service('bare_html_page_renderer')->renderMaintenancePage($output, $page_title, array( '#show_messages' => $show_messages, ))); $response->send(); diff --git a/core/core.services.yml b/core/core.services.yml index 384659b..52050cd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -650,15 +650,34 @@ services: class: Drupal\Core\EventSubscriber\RouteMethodSubscriber tags: - { name: event_subscriber } - controller.page: - class: Drupal\Core\Controller\HtmlPageController - arguments: ['@controller_resolver', '@title_resolver', '@render_html_renderer'] - controller.ajax: - class: Drupal\Core\Controller\AjaxController - arguments: ['@controller_resolver', '@element_info'] - controller.dialog: - class: Drupal\Core\Controller\DialogController - arguments: ['@controller_resolver', '@title_resolver'] + + # Main content view subscriber plus the renderers it uses. + main_content_view_subscriber: + class: Drupal\Core\EventSubscriber\MainContentViewSubscriber + arguments: ['@class_resolver', '@current_route_match', '%main_content_renderers%'] + tags: + - { name: event_subscriber } + main_content_renderer.html: + class: Drupal\Core\Render\MainContent\HtmlRenderer + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher'] + tags: + - { name: render.main_content_renderer, format: html } + main_content_renderer.ajax: + class: Drupal\Core\Render\MainContent\AjaxRenderer + arguments: ['@element_info'] + tags: + - { name: render.main_content_renderer, format: drupal_ajax } + main_content_renderer.dialog: + class: Drupal\Core\Render\MainContent\DialogRenderer + arguments: ['@title_resolver'] + tags: + - { name: render.main_content_renderer, format: drupal_dialog } + main_content_renderer.modal: + class: Drupal\Core\Render\MainContent\ModalRenderer + arguments: ['@title_resolver'] + tags: + - { name: render.main_content_renderer, format: drupal_modal } + router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener tags: @@ -671,19 +690,8 @@ services: tags: - { name: event_subscriber } arguments: ['@content_negotiation', '@title_resolver'] - html_view_subscriber: - class: Drupal\Core\EventSubscriber\HtmlViewSubscriber - tags: - - { name: event_subscriber } - arguments: ['@html_fragment_renderer', '@html_page_renderer'] - render_html_renderer: - class: Drupal\Core\Page\RenderHtmlRenderer - arguments: ['@url_generator'] - html_fragment_renderer: - class: Drupal\Core\Page\DefaultHtmlFragmentRenderer - arguments: ['@language_manager'] - html_page_renderer: - class: Drupal\Core\Page\DefaultHtmlPageRenderer + bare_html_page_renderer: + class: Drupal\Core\Render\BareHtmlPageRenderer private_key: class: Drupal\Core\PrivateKey arguments: ['@state'] @@ -732,7 +740,7 @@ services: arguments: ['@state', '@current_user'] maintenance_mode_subscriber: class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber - arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user'] + arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer'] tags: - { name: event_subscriber } path_subscriber: @@ -777,7 +785,7 @@ services: class: Drupal\Core\EventSubscriber\DefaultExceptionSubscriber tags: - { name: event_subscriber } - arguments: ['@html_fragment_renderer', '@html_page_renderer', '@config.factory'] + arguments: ['@config.factory', '@bare_html_page_renderer'] exception.logger: class: Drupal\Core\EventSubscriber\ExceptionLoggingSubscriber tags: diff --git a/core/includes/batch.inc b/core/includes/batch.inc index 68fc14b..1ba845f 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -19,7 +19,6 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Batch\Percentage; use Drupal\Core\Form\FormState; -use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -136,7 +135,7 @@ function _batch_progress_page() { // additional HTML output by PHP shows up inside the page rather than below // it. While this causes invalid HTML, the same would be true if we didn't, // as content is not allowed to appear after anyway. - $fallback = DefaultHtmlPageRenderer::renderPage($fallback, $current_set['title'], 'maintenance', array( + $fallback = \Drupal::service('bare_html_page_renderer')->renderMaintenancePage($fallback, $current_set['title'], array( '#show_messages' => FALSE, )); list($fallback) = explode('', $fallback); diff --git a/core/includes/common.inc b/core/includes/common.inc index 77e270b..e2d395f 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1117,9 +1117,6 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE, $theme_add_css = TRUE) '#type' => 'styles', '#items' => $css, ); - if (!empty($setting)) { - $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); - } return drupal_render($styles); } @@ -2262,39 +2259,6 @@ function drupal_page_set_cache(Response $response, Request $request) { } /** - * Sets the main page content value for later use. - * - * Given the nature of the Drupal page handling, this will be called once with - * a string or array. We store that and return it later as the block is being - * displayed. - * - * @param $content - * A string or renderable array representing the body of the page. - * - * @return - * If called without $content, a renderable array representing the body of - * the page. - */ -function drupal_set_page_content($content = NULL) { - $content_block = &drupal_static(__FUNCTION__, NULL); - $main_content_display = &drupal_static('system_main_content_added', FALSE); - - // Filter out each empty value, though allow '0' and 0, which would be - // filtered out by empty(). - if ($content !== NULL && $content !== '') { - $content_block = (is_array($content) ? $content : array('main' => array('#markup' => $content))); - } - else { - // Indicate that the main content has been requested. We assume that - // the module requesting the content will be adding it to the page. - // A module can indicate that it does not handle the content by setting - // the static variable back to FALSE after calling this function. - $main_content_display = TRUE; - return $content_block; - } -} - -/** * Pre-render callback: Renders a link into #markup. * * @deprecated Use \Drupal\Core\Render\Element\Link::preRenderLink(). @@ -2399,111 +2363,6 @@ function drupal_pre_render_links($element) { } /** - * 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 - * 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 array - * The processed render array for the page. - * - * @see hook_page_attachments() - * @see hook_page_attachments_alter() - * @see hook_page_top() - * @see hook_page_bottom() - * @see element_info() - */ -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. - if (is_array($page) && isset($page['#title'])) { - $title = $page['#title']; - } - - // Allow menu callbacks to return strings or arbitrary arrays to render. - // If the array returned is not of #type page directly, we need to fill - // in the page with defaults. - if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) { - drupal_set_page_content($page); - $page = element_info('page'); - } - - // Modules can add attachments. - $attachments = []; - foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) { - $function = $module . '_page_attachments'; - $function($attachments); - } - if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) { - throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments().'); - } - // Modules and themes can alter page attachments. - \Drupal::moduleHandler()->alter('page_attachments', $attachments); - \Drupal::theme()->alter('page_attachments', $attachments); - if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) { - throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments_alter().'); - } - if (isset($attachments['#attached'])) { - $page['#attached'] = $attachments['#attached']; - } - if (isset($attachments['#post_render_cache'])) { - $page['#post_render_cache'] = $attachments['#post_render_cache']; - } - - // Modules can add renderable arrays to the top and bottom of the page. - $pseudo_page_top = []; - $pseudo_page_bottom = []; - foreach (\Drupal::moduleHandler()->getImplementations('page_top') as $module) { - $function = $module . '_page_top'; - $function($pseudo_page_top); - } - foreach (\Drupal::moduleHandler()->getImplementations('page_bottom') as $module) { - $function = $module . '_page_bottom'; - $function($pseudo_page_bottom); - } - if (!empty($pseudo_page_top)) { - $page['page_top'] = $pseudo_page_top; - } - if (!empty($pseudo_page_bottom)) { - $page['page_bottom'] = $pseudo_page_bottom; - } - - // @todo Clean this up as part of https://www.drupal.org/node/2352155. - if (\Drupal::moduleHandler()->moduleExists('block')) { - _block_page_build($page); - // Find all non-empty page regions, and add a theme wrapper function that - // allows them to be consistently themed. - $regions = system_region_list(\Drupal::theme()->getActiveTheme()->getName()); - foreach (array_keys($regions) as $region) { - if (!empty($page[$region])) { - $page[$region]['#theme_wrappers'][] = 'region'; - $page[$region]['#region'] = $region; - } - } - } - - // If no module has taken care of the main content, add it to the page now. - // This allows the site to still be usable even if no modules that - // control page regions (for example, the Block module) are enabled. - if (!$main_content_display) { - $page['content']['system_main'] = drupal_set_page_content(); - } - - // Set back the previously stored title. - if (isset($title)) { - $page['#title'] = $title; - } - - return $page; -} - -/** * Renders final HTML given a structured array tree. * * Calls drupal_render() in such a way that #post_render_cache callbacks are diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 4065408..be30937 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -8,7 +8,6 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; use Drupal\Core\Logger\RfcLogLevel; -use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Utility\Error; use Symfony\Component\HttpFoundation\Response; @@ -236,7 +235,7 @@ function _drupal_log_error($error, $fatal = FALSE) { install_display_output($output, $GLOBALS['install_state']); } else { - $output = DefaultHtmlPageRenderer::renderPage($message, 'Error'); + $output = \Drupal::service('bare_html_page_renderer')->renderMaintenancePage($message, 'Error'); } $response = new Response($output, 500); diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index e494862..7bdf888 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -14,7 +14,6 @@ use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; -use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Site\Settings; use Drupal\Core\StringTranslation\Translator\FileTranslation; use Drupal\Core\Extension\ExtensionDiscovery; @@ -934,7 +933,7 @@ function install_display_output($output, $install_state) { 'ETag' => '"' . REQUEST_TIME . '"', ); $response->headers->add($default_headers); - $response->setContent(DefaultHtmlPageRenderer::renderPage($output, $output['#title'], 'install', $regions)); + $response->setContent(\Drupal::service('bare_html_page_renderer')->renderInstallPage($output, $output['#title'], $regions)); $response->send(); exit; } diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 76aafd0..b42ff1c 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -110,13 +110,15 @@ * class and method. Page controller classes do not necessarily need to * implement any particular interface or extend any particular base class. The * only requirement is that the method specified in your *.routing.yml file - * return one of the following, depending on whether you specified _content or - * _controller in the routing file defaults section: + * returns: * - A render array (see the * @link theme_render Theme and render topic @endlink for more information), * if _content is used in the routing file. - * - A \Drupal\Core\Page\HtmlFragmentInterface object (fragment or page), if - * _content is used in the routing file. + * This render array is then rendered in the requested format (HTML, dialog, + * modal, AJAX are supported by default). In the case of HTML, it will be + * surrounded by blocks by default: the Block module is enabled by default, + * and hence its Page Display Variant that surrounds the main content with + * blocks is also used by default. * - A \Symfony\Component\HttpFoundation\Response object, if _controller is * used in the routing file. * As a note, if your module registers multiple simple routes, it is usual diff --git a/core/includes/theme.inc b/core/includes/theme.inc index e772289..24d7f42 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -18,8 +18,6 @@ use Drupal\Core\Config\StorageException; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionNameLengthException; -use Drupal\Core\Page\LinkElement; -use Drupal\Core\Page\MetaElement; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; @@ -1646,22 +1644,24 @@ function drupal_pre_render_html(array $element) { * - page: A render element representing the page. */ function template_preprocess_html(&$variables) { - /** @var $page \Drupal\Core\Page\HtmlPage */ - $page = $variables['page_object']; + $variables['html_attributes'] = new Attribute(); - $variables['html_attributes'] = $page->getHtmlAttributes(); - $variables['attributes'] = $page->getBodyAttributes(); - $variables['page'] = $page; + // HTML element attributes. + $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + $variables['html_attributes']['lang'] = $language_interface->getId(); + $variables['html_attributes']['dir'] = $language_interface->getDirection(); // Compile a list of classes that are going to be applied to the body element. // This allows advanced theming based on context (home page, node of certain // type, etc.). - $body_classes = $variables['attributes']['class']; + if (isset($variables['db_is_active']) && !$variables['db_is_active']) { + $variables['attributes']['class'][] = 'db-offline'; + } // Add a class that tells us whether the page is viewed by an authenticated // user. if ($variables['logged_in']) { - $body_classes[] = 'user-logged-in'; + $variables['attributes']['class'][] = 'user-logged-in'; } // Add a class that tells us what path the page is located make it possible // to theme the page depending on the current path (e.g. node, admin, user, @@ -1669,20 +1669,18 @@ function template_preprocess_html(&$variables) { $path = \Drupal::request()->getPathInfo(); if (drupal_is_front_page()) { - $body_classes[] = 'path-frontpage'; + $variables['attributes']['class'][] = 'path-frontpage'; } else { $segment = explode('/', $path); - $body_classes[] = 'path-' . drupal_html_class($segment[1]); + $variables['attributes']['class'][] = 'path-' . drupal_html_class($segment[1]); } - $variables['attributes']['class'] = $body_classes; - $site_config = \Drupal::config('system.site'); // Construct page title. - if ($page->hasTitle()) { + if (!empty($variables['html']['page']['#title'])) { $head_title = array( - 'title' => SafeMarkup::set(trim(strip_tags($page->getTitle()))), + 'title' => SafeMarkup::set(trim(strip_tags($variables['html']['page']['#title']))), 'name' => String::checkPlain($site_config->get('name')), ); } @@ -1710,35 +1708,27 @@ function template_preprocess_html(&$variables) { } $variables['head_title'] = SafeMarkup::set($output); - // @todo Remove drupal_*_html_head() and refactor accordingly. - $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') { - $link = new LinkElement($name, isset($tag['#attributes']['content']) ? $tag['#attributes']['content'] : NULL, $tag['#attributes']); - if (!empty($tag['#noscript'])) { - $link->setNoScript(); - } - $page->addLinkElement($link); - } - elseif ($tag['#tag'] == 'meta') { - $metatag = new MetaElement(NULL, $tag['#attributes']); - if (!empty($tag['#noscript'])) { - $metatag->setNoScript(); - } - $page->addMetaElement($metatag); - } + // Collect all attachments. This must happen in the preprocess function for + // #type => html, to ensure that attachments added in #pre_render callbacks + // for #type => html are included. + $attached = $variables['html']['#attached']; + $attached = drupal_merge_attached($attached, $variables['html']['page']['#attached']); + if (isset($variables['html']['page_top'])) { + $attached = drupal_merge_attached($attached, $variables['html']['page_top']['#attached']); } - - // Add favicon. - if (theme_get_setting('features.favicon')) { - $url = UrlHelper::stripDangerousProtocols(theme_get_setting('favicon.url')); - $link = new LinkElement($url, 'shortcut icon', ['type' => theme_get_setting('favicon.mimetype')]); - $page->addLinkElement($link); + if (isset($variables['html']['page_bottom'])) { + $attached = drupal_merge_attached($attached, $variables['html']['page_bottom']['#attached']); } - $variables['page_top'][] = array('#markup' => $page->getBodyTop()); - $variables['page_bottom'][] = array('#markup' => $page->getBodyBottom()); + // Render the attachments into HTML markup to be used directly in the template + // for #type => html: html.html.twig. + $all_attached = ['#attached' => $attached]; + drupal_process_attached($all_attached); + + $variables['html']['styles'] = drupal_get_css(); + $variables['html']['scripts'] = drupal_get_js(); + $variables['html']['scripts_bottom'] = drupal_get_js('footer'); + $variables['html']['head'] = drupal_get_html_head(FALSE); } /** @@ -1749,8 +1739,6 @@ function template_preprocess_html(&$variables) { * Most themes use their own copy of page.html.twig. The default is located * inside "modules/system/page.html.twig". Look in there for the full list of * variables. - * - * @see DefaultHtmlFragmentRenderer::render() */ function template_preprocess_page(&$variables) { $language_interface = \Drupal::languageManager()->getCurrentLanguage(); @@ -1890,15 +1878,6 @@ function template_preprocess_maintenance_page(&$variables) { // @todo Rename the templates to page--maintenance + page--install. template_preprocess_page($variables); - $page_object = $variables['page']['#page']; - $attributes = $page_object->getBodyAttributes(); - $classes = $attributes['class']; - $classes[] = 'maintenance-page'; - if (isset($variables['db_is_active']) && !$variables['db_is_active']) { - $classes[] = 'db-offline'; - } - $attributes['class'] = $classes; - // @see system_page_attachments() $variables['#attached']['library'][] = 'core/normalize'; $variables['#attached']['library'][] = 'system/maintenance'; @@ -1918,12 +1897,6 @@ function template_preprocess_maintenance_page(&$variables) { function template_preprocess_install_page(&$variables) { template_preprocess_maintenance_page($variables); - $page_object = $variables['page']['#page']; - $attributes = $page_object->getBodyAttributes(); - $classes = $attributes['class']; - $classes[] = 'install-page'; - $attributes['class'] = $classes; - // Override the site name that is displayed on the page, since Drupal is // still in the process of being installed. $distribution_name = String::checkPlain(drupal_install_profile_distribution_name()); @@ -2161,7 +2134,7 @@ function drupal_common_theme() { return array( // From theme.inc. 'html' => array( - 'variables' => array('page_object' => NULL), + 'render element' => 'html', ), 'page' => array( 'render element' => 'page', diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php index 857f9d5..e98fb67 100644 --- a/core/lib/Drupal/Core/ContentNegotiation.php +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -14,6 +14,7 @@ * * @todo Replace this class with a real content negotiation library based on * mod_negotiation. Development of that is a work in progress. + * https://www.drupal.org/node/1505080 */ class ContentNegotiation { diff --git a/core/lib/Drupal/Core/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php deleted file mode 100644 index 6aef7fa..0000000 diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php deleted file mode 100644 index 88e0efe..0000000 diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php deleted file mode 100644 index efc11ac..0000000 diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index db54f91..3767c9f 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -20,6 +20,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; use Drupal\Core\Plugin\PluginManagerPass; +use Drupal\Core\Render\MainContent\MainContentRenderersPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; /** @@ -54,6 +55,8 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new StackedKernelPass()); + $container->addCompilerPass(new MainContentRenderersPass()); + // Collect tagged handler services as method calls on consumer services. $container->addCompilerPass(new TaggedHandlersPass()); $container->addCompilerPass(new RegisterStreamWrappersPass()); diff --git a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php index 3c05cf3..df1b52a 100644 --- a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php +++ b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php @@ -25,8 +25,10 @@ * * Plugin namespace: Plugin\DisplayVariant * - * For a working example, see - * \Drupal\block\Plugin\DisplayVariant\FullPageVariant + * For working examples, see + * - \Drupal\system\Plugin\DisplayVariant\SimplePageVariant + * - \Drupal\block\Plugin\DisplayVariant\BlockPageVariant + * - \Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant * * @see \Drupal\Core\Display\VariantInterface * @see \Drupal\Core\Display\VariantBase diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php index 377ae93..0d928705 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php @@ -13,7 +13,13 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * Defines a subscriber for setting the format of the request. + * Defines a subscriber to negotiate a _controller to use for a _content route. + * + * @todo Remove this event subscriber after both + * https://www.drupal.org/node/2092647 and https://www.drupal.org/node/2331919 + * have landed. + * + * @see \Drupal\Core\EventSubscriber\MainContentViewSubscriber */ class ContentControllerSubscriber implements EventSubscriberInterface { @@ -35,20 +41,10 @@ public function __construct(ContentNegotiation $negotiation) { } /** - * Associative array of supported mime types and their appropriate controller. - * - * @var array - */ - protected $types = array( - 'drupal_dialog' => 'controller.dialog:dialog', - 'drupal_modal' => 'controller.dialog:modal', - 'html' => 'controller.page:content', - 'drupal_ajax' => 'controller.ajax:content', - ); - - /** * Sets the derived request format on the request. * + * @todo Remove when https://www.drupal.org/node/2331919 lands. + * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The event to process. */ @@ -61,19 +57,20 @@ public function onRequestDeriveFormat(GetResponseEvent $event) { } /** - * Sets the _controller on a request based on the request format. + * Sets _content (if it exists) as the _controller. + * + * @todo Remove when https://www.drupal.org/node/2092647 lands. * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The event to process. */ - public function onRequestDeriveContentWrapper(GetResponseEvent $event) { + public function onRequestDeriveController(GetResponseEvent $event) { $request = $event->getRequest(); $controller = $request->attributes->get('_controller'); - if (empty($controller) && ($type = $request->getRequestFormat())) { - if (isset($this->types[$type])) { - $request->attributes->set('_controller', $this->types[$type]); - } + $content = $request->attributes->get('_content'); + if (empty($controller) && !empty($content)) { + $request->attributes->set('_controller', $content); } } @@ -85,7 +82,7 @@ public function onRequestDeriveContentWrapper(GetResponseEvent $event) { */ static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormat', 31); - $events[KernelEvents::REQUEST][] = array('onRequestDeriveContentWrapper', 30); + $events[KernelEvents::REQUEST][] = array('onRequestDeriveController', 30); return $events; } diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php index 43bdd8b..b1d8411 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php @@ -57,6 +57,8 @@ public function __construct(ClassResolverInterface $class_resolver, ControllerRe /** * Sets the _controller on a request based on the request format. * + * @todo Remove when https://www.drupal.org/node/2092647 lands. + * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The event to process. */ @@ -65,7 +67,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) { if ($form = $request->attributes->get('_form')) { $wrapper = new HtmlFormController($this->classResolver, $this->controllerResolver, $this->container, $form, $this->formBuilder); - $request->attributes->set('_content', array($wrapper, 'getContentResult')); + $request->attributes->set('_controller', array($wrapper, 'getContentResult')); } } diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php index e35eff4..b8d23c4 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php @@ -11,10 +11,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\ContentNegotiation; -use Drupal\Core\Page\DefaultHtmlPageRenderer; -use Drupal\Core\Page\HtmlFragment; -use Drupal\Core\Page\HtmlFragmentRendererInterface; -use Drupal\Core\Page\HtmlPageRendererInterface; +use Drupal\Core\Render\BareHtmlPageRendererInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Utility\Error; use Symfony\Component\Debug\Exception\FlattenException; @@ -36,20 +33,6 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface { use StringTranslationTrait; /** - * The fragment renderer. - * - * @var \Drupal\Core\Page\HtmlFragmentRendererInterface - */ - protected $fragmentRenderer; - - /** - * The page renderer. - * - * @var \Drupal\Core\Page\HtmlPageRendererInterface - */ - protected $htmlPageRenderer; - - /** * @var string * * One of the error level constants defined in bootstrap.inc. @@ -64,19 +47,23 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface { protected $configFactory; /** + * The bare HTML page renderer. + * + * @var \Drupal\Core\Render\BareHtmlPageRendererInterface + */ + protected $bareHtmlPageRenderer; + + /** * Constructs a new DefaultExceptionHtmlSubscriber. * - * @param \Drupal\Core\Page\HtmlFragmentRendererInterface $fragment_renderer - * The fragment renderer. - * @param \Drupal\Core\Page\HtmlPageRendererInterface $page_renderer - * The page renderer. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. + * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer + * The bare HTML page renderer. */ - public function __construct(HtmlFragmentRendererInterface $fragment_renderer, HtmlPageRendererInterface $page_renderer, ConfigFactoryInterface $config_factory) { - $this->fragmentRenderer = $fragment_renderer; - $this->htmlPageRenderer = $page_renderer; + public function __construct(ConfigFactoryInterface $config_factory, BareHtmlPageRendererInterface $bare_html_page_renderer) { $this->configFactory = $config_factory; + $this->bareHtmlPageRenderer = $bare_html_page_renderer; } /** @@ -145,7 +132,7 @@ protected function onHtml(GetResponseForExceptionEvent $event) { } $content = $this->t('The website has encountered an error. Please try again later.'); - $output = DefaultHtmlPageRenderer::renderPage($content, $this->t('Error')); + $output = $this->bareHtmlPageRenderer->renderMaintenancePage($content, $this->t('Error')); $response = new Response($output); if ($exception instanceof HttpExceptionInterface) { @@ -186,26 +173,6 @@ protected function onJson(GetResponseForExceptionEvent $event) { } /** - * Creates an Html response for the provided criteria. - * - * @param $title - * The page title of the response. - * @param $body - * The body of the error page. - * @param $response_code - * The HTTP response code of the response. - * @return \Symfony\Component\HttpFoundation\Response - * An error Response object ready to return to the browser. - */ - protected function createHtmlResponse($title, $body, $response_code) { - $fragment = new HtmlFragment($body); - $fragment->setTitle($title); - - $page = $this->fragmentRenderer->render($fragment, $response_code); - return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode()); - } - - /** * Handles errors for this subscriber. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php deleted file mode 100644 index b51e491..0000000 diff --git a/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php new file mode 100644 index 0000000..0c386bf --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php @@ -0,0 +1,109 @@ +classResolver = $class_resolver; + $this->routeMatch = $route_match; + $this->mainContentRenderers = $main_content_renderers; + } + + /** + * Sets a response given a (main content) render array. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The event to process. + * + * @throws \LogicException + * Thrown when the _content controller doesn't return a render array. + */ + public function onViewRenderArray(GetResponseForControllerResultEvent $event) { + $request = $event->getRequest(); + $result = $event->getControllerResult(); + + $format = $request->getRequestFormat(); + + // Render the controller result into a response if it's a render array. + if (is_array($result)) { + if (isset($this->mainContentRenderers[$format])) { + $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$format]); + $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch)); + } + else { + $supported_formats = array_keys($this->mainContentRenderers); + $supported_mimetypes = array_map([$request, 'getMimeType'], $supported_formats); + $event->setResponse(new Response('Not Acceptable. Supported MIME types: ' . implode(', ', $supported_mimetypes) . '.', 406)); + } + } + } + + /** + * 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('onViewRenderArray', 30); + + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php index f09e3e3..164dec0 100644 --- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php @@ -10,7 +10,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Page\DefaultHtmlPageRenderer; +use Drupal\Core\Render\BareHtmlPageRendererInterface; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; @@ -58,6 +58,13 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface { protected $urlGenerator; /** + * The bare HTML page renderer. + * + * @var \Drupal\Core\Render\BareHtmlPageRendererInterface + */ + protected $bareHtmlPageRenderer; + + /** * Constructs a new MaintenanceModeSubscriber. * * @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode @@ -70,13 +77,16 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface { * The url generator. * @param \Drupal\Core\Session\AccountInterface $account * The current user. + * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer + * The bare HTML page renderer. */ - public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account) { + public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) { $this->maintenanceMode = $maintenance_mode; $this->config = $config_factory; $this->stringTranslation = $translation; $this->urlGenerator = $url_generator; $this->account = $account; + $this->bareHtmlPageRenderer = $bare_html_page_renderer; } /** @@ -95,11 +105,8 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { $content = Xss::filterAdmin(String::format($this->config->get('system.maintenance')->get('message'), array( '@site' => $this->config->get('system.site')->get('name'), ))); - // @todo Break the dependency on DefaultHtmlPageRenderer, see: - // https://www.drupal.org/node/2295609 - $content = DefaultHtmlPageRenderer::renderPage($content, $this->t('Site under maintenance')); - $response = new Response('Service unavailable', 503); - $response->setContent($content); + $output = $this->bareHtmlPageRenderer->renderMaintenancePage($content, $this->t('Site under maintenance')); + $response = new Response($output, 503); $event->setResponse($response); } else { diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php index 7f13698..0b0c274 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -7,9 +7,7 @@ namespace Drupal\Core\EventSubscriber; -use Drupal\Core\Ajax\AjaxResponseRenderer; use Drupal\Core\Controller\TitleResolverInterface; -use Drupal\Core\Page\HtmlPage; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; @@ -43,12 +41,6 @@ class ViewSubscriber implements EventSubscriberInterface { */ protected $titleResolver; - /** - * The Ajax response renderer. - * - * @var \Drupal\Core\Ajax\AjaxResponseRenderer - */ - protected $ajaxRenderer; /** * Constructs a new ViewSubscriber. @@ -57,13 +49,10 @@ class ViewSubscriber implements EventSubscriberInterface { * The content negotiation. * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver * The title resolver. - * @param \Drupal\Core\Ajax\AjaxResponseRenderer $ajax_renderer - * The ajax response renderer. */ - public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver, AjaxResponseRenderer $ajax_renderer) { + public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver) { $this->negotiation = $negotiation; $this->titleResolver = $title_resolver; - $this->ajaxRenderer = $ajax_renderer; } /** @@ -79,14 +68,8 @@ public function __construct(ContentNegotiation $negotiation, TitleResolverInterf * The Event to process. */ public function onView(GetResponseForControllerResultEvent $event) { - $request = $event->getRequest(); - // For a master request, we process the result and wrap it as needed. - // For a subrequest, all we want is the string value. We assume that - // is just an HTML string from a controller, so wrap that into a response - // object. The subrequest's response will get dissected and placed into - // the larger page as needed. if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { $method = 'on' . $this->negotiation->getContentType($request); @@ -97,27 +80,6 @@ public function onView(GetResponseForControllerResultEvent $event) { $event->setResponse(new Response('Not Acceptable', 406)); } } - 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, - ); - } - - // If no title was returned fall back to one defined in the route. - if (!isset($page_result['#title'])) { - $page_result['#title'] = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); - } - - $event->setResponse(new Response(drupal_render_root($page_result))); - } } public function onJson(GetResponseForControllerResultEvent $event) { diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php deleted file mode 100644 index 0cdd613..0000000 diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php deleted file mode 100644 index eea3e16..0000000 diff --git a/core/lib/Drupal/Core/Page/FeedLinkElement.php b/core/lib/Drupal/Core/Page/FeedLinkElement.php deleted file mode 100644 index 648963b..0000000 diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php deleted file mode 100644 index 49c421d..0000000 diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php deleted file mode 100644 index a4e0e29..0000000 diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentInterface.php b/core/lib/Drupal/Core/Page/HtmlFragmentInterface.php deleted file mode 100644 index f9c481b..0000000 diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php b/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php deleted file mode 100644 index 32e0076..0000000 diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php deleted file mode 100644 index e6c75f2..0000000 diff --git a/core/lib/Drupal/Core/Page/HtmlPageRendererInterface.php b/core/lib/Drupal/Core/Page/HtmlPageRendererInterface.php deleted file mode 100644 index dea7f64..0000000 diff --git a/core/lib/Drupal/Core/Page/LinkElement.php b/core/lib/Drupal/Core/Page/LinkElement.php deleted file mode 100644 index 04cb2a0..0000000 diff --git a/core/lib/Drupal/Core/Page/MetaElement.php b/core/lib/Drupal/Core/Page/MetaElement.php deleted file mode 100644 index d849ed9..0000000 diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php b/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php deleted file mode 100644 index bc88bc3..0000000 diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php b/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php deleted file mode 100644 index 4807be9..0000000 diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php new file mode 100644 index 0000000..cd5adaa --- /dev/null +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php @@ -0,0 +1,78 @@ + $content]; + } + $attributes = [ + 'class' => [ + 'maintenance-page', + ], + ]; + return $this->renderBarePage($content, $title, $page_additions, $attributes, 'maintenance_page'); + } + + /** + * {@inheritdoc} + */ + public function renderInstallPage($content, $title, array $page_additions = []) { + $attributes = [ + 'class' => [ + 'install-page', + ], + ]; + return $this->renderBarePage($content, $title, $page_additions, $attributes, 'install_page'); + } + + /** + * Renders a bare page. + * + * @param string|array $content + * The main content to render in the 'content' region. + * @param string $title + * The title for this maintenance page. + * @param array $page_additions + * Additional regions to add to the page. May also be used to pass the + * #show_messages property for #type 'page'. + * @param array $attributes + * Attributes to set on #type 'html'. + * @param string $page_theme_property + * The #theme property to set on #type 'page'. + * + * @return string + * The rendered HTML page. + */ + protected function renderBarePage(array $content, $title, array $page_additions, array $attributes, $page_theme_property) { + $html = [ + '#type' => 'html', + '#attributes' => $attributes, + 'page' => [ + '#type' => 'page', + '#theme' => $page_theme_property, + '#title' => $title, + 'content' => $content, + ] + $page_additions, + ]; + + // We must first render the contents of the html.html.twig template, see + // \Drupal\Core\Render\MainContent\HtmlRenderer::renderPage() for details. + drupal_render_root($html['page']); + return drupal_render($html); + } + +} diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php new file mode 100644 index 0000000..4bba7c8 --- /dev/null +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php @@ -0,0 +1,73 @@ +. + * Provides a render element for an entire HTML page: plus its children. * * @RenderElement("html") */ @@ -18,8 +20,12 @@ class Html extends RenderElement { * {@inheritdoc} */ public function getInfo() { + $class = get_class($this); return array( '#theme' => 'html', + '#pre_render' => array( + array($class, 'preRenderHtml'), + ), // HTML5 Shiv '#attached' => array( 'library' => array('core/html5shiv'), @@ -27,4 +33,103 @@ public function getInfo() { ); } + /** + * #pre_render callback for the html element type. + * + * @param array $element + * A structured array containing the html element type build properties. + * + * @return array + * The processed element. + */ + public static function preRenderHtml(array $element) { + // Attach libraries and CSS used by this theme. + $active_theme = \Drupal::theme()->getActiveTheme(); + foreach ($active_theme->getLibraries() as $library) { + $element['#attached']['library'][] = $library; + } + foreach ($active_theme->getStyleSheets() as $media => $stylesheets) { + foreach ($stylesheets as $stylesheet) { + $element['#attached']['css'][$stylesheet] = array( + 'group' => CSS_AGGREGATE_THEME, + 'every_page' => TRUE, + 'media' => $media + ); + } + } + + // Attach favicon. + if (static::themeGetSetting('features.favicon')) { + $favicon = static::themeGetSetting('favicon.url'); + $type = static::themeGetSetting('favicon.mimetype'); + $element['#attached']['html_head_link'][][] = array( + 'rel' => 'shortcut icon', + 'href' => UrlHelper::stripDangerousProtocols($favicon), + 'type' => $type, + ); + } + + // Get the major Drupal version. + list($version, ) = explode('.', \Drupal::VERSION); + + // Attach default meta tags. + $meta_default = array( + // Make sure the Content-Type comes first because the IE browser may be + // vulnerable to XSS via encoding attacks from any content that comes + // before this META tag, such as a TITLE tag. + 'system_meta_content_type' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'charset', + 'charset' => 'utf-8', + ), + // Security: This always has to be output first. + '#weight' => -1000, + ), + // Show Drupal and the major version number in the META GENERATOR tag. + 'system_meta_generator' => array( + '#type' => 'html_tag', + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'Generator', + 'content' => 'Drupal ' . $version . ' (http://drupal.org)', + ), + ), + // Attach default mobile meta tags for responsive design. + 'MobileOptimized' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'MobileOptimized', + 'content' => 'width', + ), + ), + 'HandheldFriendly' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'HandheldFriendly', + 'content' => 'true', + ), + ), + 'viewport' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'viewport', + 'content' => 'width=device-width, initial-scale=1.0', + ), + ), + ); + foreach ($meta_default as $key => $value) { + $element['#attached']['html_head'][] = [$value, $key]; + } + + return $element; + } + + /** + * Wraps theme_get_setting(). + */ + protected static function themeGetSetting($setting_name) { + return theme_get_setting($setting_name); + } + } diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php index cf133af..0d0393e 100644 --- a/core/lib/Drupal/Core/Render/Element/Page.php +++ b/core/lib/Drupal/Core/Render/Element/Page.php @@ -8,7 +8,11 @@ namespace Drupal\Core\Render\Element; /** - * Provides a render element for an entire HTML page. + * Provides a render element for the content of an HTML page. + * + * This represents the "main part" of the HTML page's body; see html.html.twig. + * + * @todo Rename this to 'body'? * * @RenderElement("page") */ diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php similarity index 42% rename from core/lib/Drupal/Core/Controller/AjaxController.php rename to core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php index 79ee2c0..728f615 100644 --- a/core/lib/Drupal/Core/Controller/AjaxController.php +++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php @@ -2,28 +2,23 @@ /** * @file - * Contains \Drupal\Core\Controller\AjaxController. + * Contains \Drupal\Core\Render\MainContent\AjaxRenderer. */ -namespace Drupal\Core\Controller; +namespace Drupal\Core\Render\MainContent; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\AlertCommand; use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Ajax\PrependCommand; -use Drupal\Core\Page\HtmlFragment; use Drupal\Core\Render\ElementInfoManagerInterface; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; /** - * Default controller for Ajax requests. + * Default main content renderer for Ajax requests. */ -class AjaxController implements ContainerAwareInterface { - - use ContainerAwareTrait; +class AjaxRenderer { /** * The controller resolver. @@ -40,55 +35,26 @@ class AjaxController implements ContainerAwareInterface { protected $elementInfoManager; /** - * Constructs a new AjaxController instance. + * Constructs a new AjaxRenderer instance. * - * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver - * The controller resolver. * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager * The element info manager. */ - public function __construct(ControllerResolverInterface $controller_resolver, ElementInfoManagerInterface $element_info_manager) { - $this->controllerResolver = $controller_resolver; + public function __construct(ElementInfoManagerInterface $element_info_manager) { $this->elementInfoManager = $element_info_manager; } /** - * Controller method for Ajax content. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - * @param callable $_content - * The callable that returns the content of the Ajax response. - * - * @return \Drupal\Core\Ajax\AjaxResponse - * A response object. + * {@inheritdoc} */ - public function content(Request $request, $_content) { - $content = $this->getContentResult($request, $_content); - - // If there is already a Response object, return it without manipulation. - if ($content instanceof Response && $content->isOk()) { - return $content; - } - - // Allow controllers to return an HtmlFragment directly. - if ($content instanceof HtmlFragment) { - $content = $content->getContent(); - } - // Most controllers return a render array, but some return a string. - if (!is_array($content)) { - $content = array( - '#markup' => $content, - ); - } - + public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { $response = new AjaxResponse(); - if (isset($content['#type']) && ($content['#type'] == 'ajax')) { + if (isset($main_content['#type']) && ($main_content['#type'] == 'ajax')) { // Complex Ajax callbacks can return a result that contains an error // message or a specific set of commands to send to the browser. - $content += $this->elementInfoManager->getInfo('ajax'); - $error = $content['#error']; + $main_content += $this->elementInfoManager->getInfo('ajax'); + $error = $main_content['#error']; if (!empty($error)) { // Fall back to some default message otherwise use the specific one. if (!is_string($error)) { @@ -98,7 +64,7 @@ public function content(Request $request, $_content) { } } - $html = $this->drupalRenderRoot($content); + $html = $this->drupalRenderRoot($main_content); // The selector for the insert command is NULL as the new content will // replace the element making the Ajax call. The default 'replaceWith' @@ -113,31 +79,6 @@ public function content(Request $request, $_content) { } /** - * 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 mixed - * The result of invoking the controller. Render arrays, strings, HtmlPage, - * and HtmlFragment objects are possible. - */ - 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; - } - - /** * Wraps drupal_render_root(). * * @todo: Remove as part of https://drupal.org/node/2182149 diff --git a/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php new file mode 100644 index 0000000..24462cb --- /dev/null +++ b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php @@ -0,0 +1,92 @@ +titleResolver = $title_resolver; + } + + /** + * {@inheritdoc} + */ + public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { + $response = new AjaxResponse(); + + // First render the main content, because it might provide a title. + $content = drupal_render_root($main_content); + drupal_process_attached($main_content); + + // Determine the title: use the title provided by the main content if any, + // otherwise get it from the routing information. + $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject()); + + // Determine the dialog options and the target for the OpenDialogCommand. + $options = $request->request->get('dialogOptions', array()); + $target = $this->determineTargetSelector($options, $route_match); + + $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); + return $response; + } + + /** + * Determine the target selector for the OpenDialogCommand. + * + * @param array &$options + * The 'target' option, if set, is used, and then removed from $options. + * @param RouteMatchInterface $route_match + * When no 'target' option is set in $options, $route_match is used instead + * to determine the target. + * + * @return string + * The target selector. + */ + protected function determineTargetSelector(array &$options, RouteMatchInterface $route_match) { + // 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 = $route_match->getRouteName(); + $target = '#' . drupal_html_id("drupal-dialog-$route_name"); + } + return $target; + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php new file mode 100644 index 0000000..47e33bb --- /dev/null +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -0,0 +1,284 @@ +titleResolver = $title_resolver; + $this->displayVariantManager = $display_variant_manager; + $this->eventDispatcher = $event_dispatcher; + } + + /** + * Prepares the HTML body: wraps the main content in #type 'page'. + * + * @param array $main_content + * The render array representing the main content. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object, for context. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route match, for context. + * + * @return array + * An array with two values: + * 0. A #type 'page' render array. + * 1. The page title. + * + * @throws \LogicException + * If the selected display variant does not implement PageVariantInterface. + */ + protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) { + // If the _content result already is #type => page, we have no work to do: + // the "main content" already is an entire "page" (see html.html.twig). + if (isset($main_content['#type']) && $main_content['#type'] === 'page') { + $page = $main_content; + } + // Otherwise, render it as the main content of a #type => page, by selecting + // page display variant to do that and building that page display variant. + else { + // Select the page display variant to be used to render this main content, + // default to the built-in "simple page". + $event = new PageDisplayVariantSelectionEvent('simple_page'); + $this->eventDispatcher->dispatch(SystemEvents::SELECT_PAGE_DISPLAY_VARIANT, $event); + $variant_id = $event->getPluginId(); + + // We must render the main content now already, because it might provide a + // title. We set its $is_root_call parameter to FALSE, to ensure + // #post_render_cache callbacks are not yet applied. This is essentially + // "pre-rendering" the main content, the "full rendering" will happen in + // ::renderContentIntoResponse(). + // @todo Remove this once https://www.drupal.org/node/2359901 lands. + if (!empty($main_content)) { + drupal_render($main_content, FALSE); + $main_content = [ + '#markup' => SafeMarkup::set($main_content['#markup']), + '#attached' => $main_content['#attached'], + '#cache' => ['tags' => $main_content['#cache']['tags']], + '#post_render_cache' => $main_content['#post_render_cache'], + '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL, + ]; + } + + // Instantiate the page display, and give it the main content. + $page_display = $this->displayVariantManager->createInstance($variant_id); + if (!$page_display instanceof PageVariantInterface) { + throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.'); + } + $page_display->setMainContent($main_content); + + // Generate a #type => page render array using the page display variant, + // the page display will build the content for the various page regions. + $page = array( + '#type' => 'page', + ); + $page += $page_display->build(); + } + + // $page is now fully built. Find all non-empty page regions, and add a + // theme wrapper function that allows them to be consistently themed. + $regions = system_region_list(\Drupal::theme()->getActiveTheme()->getName()); + foreach (array_keys($regions) as $region) { + if (!empty($page[$region])) { + $page[$region]['#theme_wrappers'][] = 'region'; + $page[$region]['#region'] = $region; + } + } + + // Allow hooks to add attachments to $page['#attached']. + static::invokePageAttachmentHooks($page); + + // Determine the title: use the title provided by the main content if any, + // otherwise get it from the routing information. + $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject()); + + return [$page, $title]; + } + + /** + * {@inheritdoc} + * + * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'. + */ + public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { + list($page, $title) = $this->prepare($main_content, $request, $route_match); + + if (!isset($page['#type']) || $page['#type'] !== 'page') { + throw new \LogicException('Must be #type page'); + } + + $page['#title'] = $title; + + // Now render the rendered page.html.twig template inside the html.html.twig + // template, and use the bubbled #attached metadata from $page to ensure we + // load all attached assets. + $html = [ + '#type' => 'html', + 'page' => $page, + ]; + $html += element_info('html'); + + // The special page regions will appear directly in html.html.twig, not in + // page.html.twig, hence add them here, just before rendering html.html.twig. + static::buildPageTopAndBottom($html); + + // The three parts of rendered markup in html.html.twig (page_top, page and + // page_bottom) must be rendered with drupal_render_root(), so that their + // #post_render_cache callbacks are executed (which may attach additional + // assets). + // html.html.twig must be able to render the final list of attached assets, + // and hence may not execute any #post_render_cache_callbacks (because they + // might add yet more assets to be attached), and therefore it must be + // rendered with drupal_render(), not drupal_render_root(). + drupal_render_root($html['page']); + if (isset($html['page_top'])) { + drupal_render_root($html['page_top']); + } + if (isset($html['page_bottom'])) { + drupal_render_root($html['page_bottom']); + } + $content = drupal_render_root($html); + + // Store the cache tags associated with this page in a X-Drupal-Cache-Tags + // header. Also associate the "rendered" cache tag. This allows us to + // invalidate the entire render cache, regardless of the cache bin. + $cache_tags = Cache::mergeTags( + isset($html['page_top']) ? $html['page_top']['#cache']['tags'] : [], + $html['page']['#cache']['tags'], + isset($html['page_bottom']) ? $html['page_bottom']['#cache']['tags'] : [], + ['rendered'] + ); + + // Set the generator in the HTTP header. + list($version) = explode('.', \Drupal::VERSION, 2); + + return new Response($content, 200,[ + 'X-Drupal-Cache-Tags' => implode(' ', $cache_tags), + 'X-Generator' => 'Drupal ' . $version . ' (http://drupal.org)' + ]); + } + + /** + * Invokes the page attachment hooks. + * + * @param array &$page + * A #type 'page' render array, for which the page attachment hooks will be + * invoked and to which the results will be added. + * + * @throws \LogicException + * + * @see hook_page_attachments() + * @see hook_page_attachments_alter() + */ + public static function invokePageAttachmentHooks(array &$page) { + // Modules can add attachments. + $attachments = []; + foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) { + $function = $module . '_page_attachments'; + $function($attachments); + } + if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) { + throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments().'); + } + + // Modules and themes can alter page attachments. + \Drupal::moduleHandler()->alter('page_attachments', $attachments); + \Drupal::theme()->alter('page_attachments', $attachments); + if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) { + throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments_alter().'); + } + if (isset($attachments['#attached'])) { + $page['#attached'] = $attachments['#attached']; + } + if (isset($attachments['#post_render_cache'])) { + $page['#post_render_cache'] = $attachments['#post_render_cache']; + } + } + + /** + * Invokes the page top and bottom hooks. + * + * @param array &$html + * A #type 'html' render array, for which the page top and bottom hooks will + * be invoked, and to which the 'page_top' and 'page_bottom' children (also + * render arrays) will be added (if non-empty). + * + * @throws \LogicException + * + * @see hook_page_top() + * @see hook_page_bottom() + * @see html.html.twig + */ + public static function buildPageTopAndBottom(array &$html) { + // Modules can add render arrays to the top and bottom of the page. + $page_top = []; + $page_bottom = []; + foreach (\Drupal::moduleHandler()->getImplementations('page_top') as $module) { + $function = $module . '_page_top'; + $function($page_top); + } + foreach (\Drupal::moduleHandler()->getImplementations('page_bottom') as $module) { + $function = $module . '_page_bottom'; + $function($page_bottom); + } + if (!empty($page_top)) { + $html['page_top'] = $page_top; + } + if (!empty($page_bottom)) { + $html['page_bottom'] = $page_bottom; + } + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/MainContentRendererInterface.php b/core/lib/Drupal/Core/Render/MainContent/MainContentRendererInterface.php new file mode 100644 index 0000000..ca957c8 --- /dev/null +++ b/core/lib/Drupal/Core/Render/MainContent/MainContentRendererInterface.php @@ -0,0 +1,38 @@ +findTaggedServiceIds('render.main_content_renderer') as $id => $attributes) { + $format = $attributes[0]['format']; + $main_content_renderers[$format] = $id; + } + $container->setParameter('main_content_renderers', $main_content_renderers); + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php b/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php new file mode 100644 index 0000000..c971eb3 --- /dev/null +++ b/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php @@ -0,0 +1,42 @@ +titleResolver->getTitle($request, $route_match->getRouteObject()); + + // Determine the title: use the title provided by the main content if any, + // otherwise get it from the routing information. + $options = $request->request->get('dialogOptions', array()); + + $response->addCommand(new OpenModalDialogCommand($title, $content, $options)); + return $response; + } + +} diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php index 4545c19..de41c44 100644 --- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php @@ -125,7 +125,7 @@ public function authenticate(Request $request) { } // Always register an IP-based failed login event. $this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window')); - return NULL; + return []; } /** diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 83f4a40..a294dc8 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -62,44 +62,22 @@ function block_theme() { } /** - * Renders blocks into their regions. - * - * @todo Clean this up as part of https://www.drupal.org/node/2352155. + * Implements hook_page_top(). */ -function _block_page_build(&$page) { - $theme = \Drupal::theme()->getActiveTheme()->getName(); - - // Fetch a list of regions for the current theme. - $all_regions = system_region_list($theme); - if (\Drupal::routeMatch()->getRouteName() != 'block.admin_demo') { - // Create a full page display variant, which will load blocks into their - // regions. - $page += \Drupal::service('plugin.manager.display_variant') - ->createInstance('full_page') - ->setMainContent(drupal_set_page_content()) - ->build(); - } - else { - // Append region description if we are rendering the regions demo page. - $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE)); - foreach ($visible_regions as $region) { - $description = '
' . $all_regions[$region] . '
'; - $page[$region]['block_description'] = array( - '#markup' => $description, - '#weight' => 15, - ); - } - $page['page_top']['backlink'] = array( +function block_page_top(array &$page_top) { + if (\Drupal::routeMatch()->getRouteName() === 'block.admin_demo') { + $theme = \Drupal::theme()->getActiveTheme()->getName(); + $page_top['backlink'] = array( '#type' => 'link', '#title' => t('Exit block region demonstration'), '#options' => array('attributes' => array('class' => array('block-demo-backlink'))), '#weight' => -10, ); if (\Drupal::config('system.theme')->get('default') == $theme) { - $page['page_top']['backlink']['#url'] = Url::fromRoute('block.admin_display'); + $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display'); } else { - $page['page_top']['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]); + $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]); } } } diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml index dd38a43..9c7be32 100644 --- a/core/modules/block/block.services.yml +++ b/core/modules/block/block.services.yml @@ -3,6 +3,11 @@ services: class: Drupal\block\Theme\AdminDemoNegotiator tags: - { name: theme_negotiator, priority: 1000 } + block.page_display_variant_subscriber: + class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber + arguments: ['@current_route_match'] + tags: + - { name: event_subscriber } block.current_user_context: class: Drupal\block\EventSubscriber\CurrentUserContext arguments: ['@current_user', '@entity.manager'] diff --git a/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php new file mode 100644 index 0000000..b6127fb --- /dev/null +++ b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php @@ -0,0 +1,64 @@ +routeMatch = $route_match; + } + + /** + * Selects the block page display variant. + * + * @param \Drupal\system\Event\PageDisplayVariantSelectionEvent $event + * The event to process. + */ + public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) { + if ($this->routeMatch->getRouteName() != 'block.admin_demo') { + $event->setPluginId('block_page'); + } + else { + $event->setPluginId('block_demo_page'); + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[SystemEvents::SELECT_PAGE_DISPLAY_VARIANT][] = array('onSelectPageDisplayVariant'); + return $events; + } + +} diff --git a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php similarity index 94% rename from core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php rename to core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php index 9ae99ed..1b58773 100644 --- a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\block\Plugin\DisplayVariant\FullPageVariant. + * Contains \Drupal\block\Plugin\DisplayVariant\BlockPageVariant. */ namespace Drupal\block\Plugin\DisplayVariant; @@ -18,14 +18,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Provides a display variant that represents the full page. + * Provides a page display variant that decorates the main content with blocks. * * @PageDisplayVariant( - * id = "full_page", - * admin_label = @Translation("Full page") + * id = "block_page", + * admin_label = @Translation("Page with blocks") * ) */ -class FullPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface { +class BlockPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface { /** * The block storage. @@ -70,7 +70,7 @@ class FullPageVariant extends VariantBase implements PageVariantInterface, Conta protected $mainContent; /** - * Constructs a new FullPageVariant. + * Constructs a new BlockPageVariant. * * @param array $configuration * A configuration array containing information about the plugin instance. diff --git a/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php new file mode 100644 index 0000000..b1a450d --- /dev/null +++ b/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php @@ -0,0 +1,134 @@ +routeMatch = $route_match; + $this->themeNegotiator = $theme_negotiator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match'), + $container->get('theme.negotiator') + ); + } + + /** + * Gets the current theme for this route. + * + * @return string + * The current theme. + */ + protected function getTheme() { + return $this->themeNegotiator->determineActiveTheme($this->routeMatch); + } + + /** + * {@inheritdoc} + */ + public function setMainContent(array $main_content) { + $this->mainContent = $main_content; + } + + /** + * {@inheritdoc} + */ + public function build() { + // @see \Drupal\block\Controller::demo() + $build = ['content' => $this->mainContent]; + + // Show descriptions in each visible region, nothing else. + $visible_regions = $this->getVisibleRegionNames(); + foreach (array_keys($visible_regions) as $region) { + $description = '
' . $visible_regions[$region] . '
'; + $build[$region]['block_description'] = array( + '#markup' => $description, + '#weight' => 15, + ); + } + return $build; + } + + /** + * Returns the human-readable list of regions keyed by machine name. + * + * @return array + * An array of human-readable region names keyed by machine name. + */ + protected function getVisibleRegionNames() { + return system_region_list($this->getTheme(), REGIONS_VISIBLE); + } + +} diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php similarity index 94% rename from core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php rename to core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php index 783c2fa..557d996b 100644 --- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\Tests\block\Unit\Plugin\DisplayVariant\FullPageVariantTest. + * Contains \Drupal\Tests\block\Unit\Plugin\DisplayVariant\BlockPageVariantTest. */ namespace Drupal\Tests\block\Unit\Plugin\DisplayVariant; @@ -10,10 +10,10 @@ use Drupal\Tests\UnitTestCase; /** - * @coversDefaultClass \Drupal\block\Plugin\DisplayVariant\FullPageVariant + * @coversDefaultClass \Drupal\block\Plugin\DisplayVariant\BlockPageVariant * @group block */ -class FullPageVariantTest extends UnitTestCase { +class BlockPageVariantTest extends UnitTestCase { /** * The block storage. @@ -51,7 +51,7 @@ class FullPageVariantTest extends UnitTestCase { * @param array $definition * The plugin definition array. * - * @return \Drupal\block\Plugin\DisplayVariant\FullPageVariant|\PHPUnit_Framework_MockObject_MockObject + * @return \Drupal\block\Plugin\DisplayVariant\BlockPageVariant|\PHPUnit_Framework_MockObject_MockObject * A mocked display variant plugin. */ public function setUpDisplayVariant($configuration = array(), $definition = array()) { @@ -59,7 +59,7 @@ public function setUpDisplayVariant($configuration = array(), $definition = arra $this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface'); $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); $this->themeNegotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); - return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\FullPageVariant') + return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\BlockPageVariant') ->setConstructorArgs(array($configuration, 'test', $definition, $this->blockStorage, $this->blockViewBuilder, $this->routeMatch, $this->themeNegotiator)) ->setMethods(array('getRegionNames')) ->getMock(); diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php index 13c93e3..ebb7347 100644 --- a/core/modules/node/src/Tests/Views/FrontPageTest.php +++ b/core/modules/node/src/Tests/Views/FrontPageTest.php @@ -30,7 +30,7 @@ class FrontPageTest extends ViewTestBase { * * @var array */ - public static $modules = array('node'); + public static $modules = array('node', 'contextual'); protected function setUp() { parent::setUp(); diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php index e1aae7f..062529c 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/src/Tests/ReadTest.php @@ -87,8 +87,12 @@ public function testRead() { $account = $this->drupalCreateUser(); $this->drupalLogin($account); $response = $this->httpRequest($account->getSystemPath(), 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(404); - $expected_message = Json::encode(['error' => 'A fatal error occurred: Unable to find the controller for path "/user/4". Maybe you forgot to add the matching route in your routing configuration?']); + // AcceptHeaderMatcher considers the canonical, non-REST route a match, but + // a lower quality one: no format restrictions means there's always a match, + // and hence when there is no matching REST route, the non-REST route is + // used, but it can't render into application/hal+json, so it returns a 406. + $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); + $expected_message = 'Not Acceptable. Supported MIME types: text/html, application/vnd.drupal-ajax, application/vnd.drupal-dialog, application/vnd.drupal-modal.'; $this->assertIdentical($expected_message, $response); } diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php index 729af5c..35e1807 100644 --- a/core/modules/rest/src/Tests/ResourceTest.php +++ b/core/modules/rest/src/Tests/ResourceTest.php @@ -56,7 +56,11 @@ public function testFormats() { // Verify that accessing the resource returns 401. $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType); - $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define formats.'); + // AcceptHeaderMatcher considers the canonical, non-REST route a match, but + // a lower quality one: no format restrictions means there's always a match, + // and hence when there is no matching REST route, the non-REST route is + // used, but it can't render into application/hal+json, so it returns a 406. + $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); $this->curlClose(); } @@ -81,7 +85,11 @@ public function testAuthentication() { // Verify that accessing the resource returns 401. $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType); - $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define authentication.'); + // AcceptHeaderMatcher considers the canonical, non-REST route a match, but + // a lower quality one: no format restrictions means there's always a match, + // and hence when there is no matching REST route, the non-REST route is + // used, but it can't render into application/hal+json, so it returns a 406. + $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); $this->curlClose(); } diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 3768b90..5471d7e 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -1,7 +1,6 @@ fragmentRenderer = $html_fragment_renderer; - $this->titleResolver = $title_resolver; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('html_fragment_renderer'), - $container->get('title_resolver') - ); - } +class BatchController { /** * Returns a system batch page. @@ -64,9 +22,8 @@ public static function create(ContainerInterface $container) { * @param \Symfony\Component\HttpFoundation\Request $request * The current request object. * - * @return mixed - * A \Symfony\Component\HttpFoundation\Response object or page element or - * NULL. + * @return \Symfony\Component\HttpFoundation\Response|array + * A \Symfony\Component\HttpFoundation\Response object or render array. * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ @@ -81,46 +38,13 @@ public function batchPage(Request $request) { return $output; } elseif (isset($output)) { - // Force a page without blocks or messages to - // display a list of collected messages later. - drupal_set_page_content($output); - $page = element_info('page'); - $page['#show_messages'] = FALSE; - - $page = $this->render($page); - + $page = [ + '#type' => 'page', + '#show_messages' => FALSE, + 'content' => $output, + ]; return $page; } } - /** - * {@inheritdoc} - */ - public function render(array $output, $status_code = 200) { - if (!isset($output['#title'])) { - $output['#title'] = $this->titleResolver->getTitle(\Drupal::request(), \Drupal::routeMatch()->getRouteObject()); - } - $page = new HtmlPage('', isset($output['#cache']) ? $output['#cache'] : array(), $output['#title']); - - $page_array = drupal_prepare_page($output); - - $page = $this->fragmentRenderer->preparePage($page, $page_array); - - $page->setBodyTop(drupal_render_root($page_array['page_top'])); - $page->setBodyBottom(drupal_render_root($page_array['page_bottom'])); - $page->setContent(drupal_render_root($page_array)); - - drupal_process_attached($page_array); - if (isset($page_array['page_top'])) { - drupal_process_attached($page_array['page_top']); - } - if (isset($page_array['page_bottom'])) { - drupal_process_attached($page_array['page_bottom']); - } - - $page->setStatusCode($status_code); - - return $page; - } - } diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 7dff04a..2534301 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -12,7 +12,7 @@ use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; -use Drupal\Core\Page\DefaultHtmlPageRenderer; +use Drupal\Core\Render\BareHtmlPageRendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\State\StateInterface; @@ -69,6 +69,13 @@ class DbUpdateController extends ControllerBase { protected $entityDefinitionUpdateManager; /** + * The bare HTML page renderer. + * + * @var \Drupal\Core\Render\BareHtmlPageRendererInterface + */ + protected $bareHtmlPageRenderer; + + /** * Constructs a new UpdateController. * * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory @@ -83,14 +90,17 @@ class DbUpdateController extends ControllerBase { * The current user. * @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager * The entity definition update manager. + * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer + * The bare HTML page renderer. */ - public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager) { + public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, BareHtmlPageRendererInterface $bare_html_page_renderer) { $this->keyValueExpirableFactory = $key_value_expirable_factory; $this->cache = $cache; $this->state = $state; $this->moduleHandler = $module_handler; $this->account = $account; $this->entityDefinitionUpdateManager = $entity_definition_update_manager; + $this->bareHtmlPageRenderer = $bare_html_page_renderer; } /** @@ -103,7 +113,8 @@ public static function create(ContainerInterface $container) { $container->get('state'), $container->get('module_handler'), $container->get('current_user'), - $container->get('entity.definition_update_manager') + $container->get('entity.definition_update_manager'), + $container->get('bare_html_page_renderer') ); } @@ -176,7 +187,7 @@ public function handle($op, Request $request) { } $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update'); - return new Response(DefaultHtmlPageRenderer::renderPage($output, $title, 'maintenance', $regions)); + return new Response($this->bareHtmlPageRenderer->renderMaintenancePage($output, $title, $regions)); } /** diff --git a/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php b/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php new file mode 100644 index 0000000..9177a18 --- /dev/null +++ b/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php @@ -0,0 +1,55 @@ +pluginId = $plugin_id; + } + + /** + * The selected page display variant plugin ID. + * + * @param string $plugin_id + * The ID of the page display variant plugin to use. + */ + public function setPluginId($plugin_id) { + $this->pluginId = $plugin_id; + } + + /** + * The selected page display variant plugin ID. + * + * @return string; + */ + public function getPluginId() { + return $this->pluginId; + } + +} diff --git a/core/modules/system/src/Event/SystemEvents.php b/core/modules/system/src/Event/SystemEvents.php new file mode 100644 index 0000000..a99eb59 --- /dev/null +++ b/core/modules/system/src/Event/SystemEvents.php @@ -0,0 +1,23 @@ +mainContent = $main_content; + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = [ + 'content' => $this->mainContent, + ]; + return $build; + } + +} diff --git a/core/modules/system/src/Tests/Common/AddFeedTest.php b/core/modules/system/src/Tests/Common/AddFeedTest.php index ba55937..0f5a560 100644 --- a/core/modules/system/src/Tests/Common/AddFeedTest.php +++ b/core/modules/system/src/Tests/Common/AddFeedTest.php @@ -7,8 +7,6 @@ namespace Drupal\system\Tests\Common; -use Drupal\Core\Page\FeedLinkElement; -use Drupal\Core\Page\HtmlPage; use Drupal\simpletest\WebTestBase; /** @@ -30,10 +28,6 @@ function testBasicFeedAddNoTitle() { $external_for_title = 'http://' . $this->randomMachineName(12) . '/' . $this->randomMachineName(12); $fully_qualified_for_title = _url($this->randomMachineName(12), array('absolute' => TRUE)); - // Possible permutations of _drupal_add_feed() to test. - // - 'input_url': the path passed to _drupal_add_feed(), - // - 'output_url': the expected URL to be found in the header. - // - 'title' == the title of the feed as passed into _drupal_add_feed(). $urls = array( 'path without title' => array( 'url' => _url($path, array('absolute' => TRUE)), @@ -61,14 +55,14 @@ function testBasicFeedAddNoTitle() { ), ); - $html_page = new HtmlPage(); - + $build = []; foreach ($urls as $feed_info) { - $feed_link = new FeedLinkElement($feed_info['title'], $feed_info['url']); - $html_page->addLinkElement($feed_link); + $build['#attached']['feed'][] = [$feed_info['url'], $feed_info['title']]; } - $this->drupalSetContent(\Drupal::service('html_page_renderer')->render($html_page)); + drupal_process_attached($build); + + $this->drupalSetContent(drupal_get_html_head()); foreach ($urls as $description => $feed_info) { $this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description))); } @@ -80,7 +74,7 @@ function testBasicFeedAddNoTitle() { function urlToRSSLinkPattern($url, $title = '') { // Escape any regular expression characters in the URL ('?' is the worst). $url = preg_replace('/([+?.*])/', '[$0]', $url); - $generated_pattern = '%%'; + $generated_pattern = '%%'; return $generated_pattern; } diff --git a/core/modules/system/src/Tests/Common/PageRenderTest.php b/core/modules/system/src/Tests/Common/PageRenderTest.php index 09dd9b3..ad8a087 100644 --- a/core/modules/system/src/Tests/Common/PageRenderTest.php +++ b/core/modules/system/src/Tests/Common/PageRenderTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Common; +use Drupal\Core\Render\MainContent\HtmlRenderer; use Drupal\simpletest\KernelTestBase; /** @@ -61,14 +62,14 @@ function testHookPageAttachmentsAlter() { function assertPageRenderHookExceptions($module, $hook) { // Assert a valid hook implementation doesn't trigger an exception. $page = []; - drupal_prepare_page($page); + HtmlRenderer::invokePageAttachmentHooks($page); // Assert an invalid hook implementation doesn't trigger an exception. \Drupal::state()->set($module . '.' . $hook . '.descendant_attached', TRUE); $assertion = $hook . '() implementation that sets #attached on a descendant triggers an exception'; $page = []; try { - drupal_prepare_page($page); + HtmlRenderer::invokePageAttachmentHooks($page); $this->error($assertion); } catch (\LogicException $e) { @@ -82,7 +83,7 @@ function assertPageRenderHookExceptions($module, $hook) { $assertion = $hook . '() implementation that sets a child render array triggers an exception'; $page = []; try { - drupal_prepare_page($page); + HtmlRenderer::invokePageAttachmentHooks($page); $this->error($assertion); } catch (\LogicException $e) { diff --git a/core/modules/system/src/Tests/System/MainContentFallbackTest.php b/core/modules/system/src/Tests/System/MainContentFallbackTest.php deleted file mode 100644 index 288e681..0000000 diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 4d54d00..26317f6 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -419,13 +419,13 @@ system.admin_config: requirements: _permission: 'access administration pages' -# @todo system.batch_page.html does not work due to some ordering issues. -system.batch_page.normal: +system.batch_page.html: path: '/batch' defaults: _content: '\Drupal\system\Controller\BatchController::batchPage' requirements: _access: 'TRUE' + _format: 'html' options: _admin_route: TRUE diff --git a/core/modules/system/templates/html.html.twig b/core/modules/system/templates/html.html.twig index d605532..8e9cf40 100644 --- a/core/modules/system/templates/html.html.twig +++ b/core/modules/system/templates/html.html.twig @@ -29,18 +29,18 @@ - {{ page.head }} + {{ html.head }} {{ head_title }} - {{ page.styles }} - {{ page.scripts }} + {{ html.styles }} + {{ html.scripts }} - {{ page_top }} - {{ page.content }} - {{ page_bottom }} - {{ page.scripts('footer') }} + {{ html.page_top }} + {{ html.page }} + {{ html.page_bottom }} + {{ html.scripts_bottom }} diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php index 21b3ac6..946504d 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php +++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php @@ -16,7 +16,7 @@ use Drupal\Core\Form\FormStateInterface; /** - * Dummy form for testing DialogController with _form routes. + * Dummy form for testing DialogRenderer with _form routes. */ class AjaxTestDialogForm extends FormBase { diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php index 4155b32..677532f 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php +++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php @@ -11,7 +11,7 @@ use Drupal\Core\Form\FormStateInterface; /** - * Dummy form for testing DialogController with _form routes. + * Dummy form for testing DialogRenderer with _form routes. */ class AjaxTestForm implements FormInterface { diff --git a/core/modules/system/tests/modules/system_module_test/src/EventSubscriber/HtmlPageSubscriber.php b/core/modules/system/tests/modules/system_module_test/src/EventSubscriber/HtmlPageSubscriber.php deleted file mode 100644 index f64cd54..0000000 diff --git a/core/modules/system/tests/modules/system_module_test/system_module_test.module b/core/modules/system/tests/modules/system_module_test/system_module_test.module index 8d67e7a..4d5e073 100644 --- a/core/modules/system/tests/modules/system_module_test/system_module_test.module +++ b/core/modules/system/tests/modules/system_module_test/system_module_test.module @@ -5,3 +5,23 @@ * Provides System module hook implementations for testing purposes. */ +/** + * Implements hook_element_info_alter(). + */ +function system_module_test_element_info_alter(&$types) { + $types['html']['#pre_render'][] = 'system_module_test_html_pre_render'; +} + +/** + * Additional #pre_render callback for 'html' elements. + */ +function system_module_test_html_pre_render(array $element) { + // Remove the HTML5 mobile meta-tags. + $meta_tags_to_remove = ['MobileOptimized', 'HandheldFriendly', 'viewport', 'cleartype']; + foreach ($element['#attached']['html_head'] as $index => $parts) { + if (in_array($parts[1], $meta_tags_to_remove)) { + unset($element['#attached']['html_head'][$index]); + } + } + return $element; +} diff --git a/core/modules/system/tests/modules/system_module_test/system_module_test.services.yml b/core/modules/system/tests/modules/system_module_test/system_module_test.services.yml deleted file mode 100644 index 3abeb5e..0000000 diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module index 8024680..b44e0a0 100644 --- a/core/modules/system/tests/modules/system_test/system_test.module +++ b/core/modules/system/tests/modules/system_test/system_test.module @@ -99,19 +99,6 @@ function system_test_lock_exit() { * Implements hook_page_attachments(). */ function system_test_page_attachments(array &$page) { - $menu_item['path'] = current_path(); - $main_content_display = &drupal_static('system_main_content_added', FALSE); - - if ($menu_item['path'] == 'system-test/main-content-fallback') { - // Get the main content, to e.g. dynamically attach an asset. - drupal_set_page_content(); - // Indicate we don't want to override the main content. - $main_content_display = FALSE; - } - elseif ($menu_item['path'] == 'system-test/main-content-handling') { - // Set the main content. - drupal_set_page_content('
Overridden!
'); - } // Used by FrontPageTestCase to get the results of drupal_is_front_page(). $frontpage = \Drupal::state()->get('system_test.front_page_output') ?: 0; if ($frontpage && drupal_is_front_page()) { diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 3087c5f..37b769e 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -302,6 +302,80 @@ * * See drupal_process_attached() for additional information. * + * @section render_pipeline The Render Pipeline (or: how Drupal renders pages) + * + * First, you need to know the general routing concepts: please read + * @ref sec_controller first. + * + * Any route that returns the "main content" as a render array automatically has + * the ability to be requested in multiple ways: it can be rendered in a certain + * format (HTML, JSON …) and/or in a certain decorated manner (e.g. with blocks + * around the main content). + * + * After the controller returned a render array, the @code VIEW @endcode event + * (\Symfony\Component\HttpKernel\KernelEvents::VIEW) will be triggered, because + * the controller result is not a Response, but a render array. + * + * \Drupal\Core\EventSubscriber\MainContentViewSubscriber is subscribed to the + * @code VIEW @endcode event. It checks whether the controller result is an + * array, and if so, guarantees to generate a Response. + * + * Next, it checks whether the negotiated request format is supported. Any + * format for which a main content renderer service exists (an implementation of + * \Drupal\Core\Render\MainContent\MainContentRendererInterface) is supported. + * + * If the negotiated request format is not supported, a 406 response is + * generated, including a helpful message that lists the supported formats (as + * per RFC 2616, section 10.4.7). + * + * Otherwise, when the negotiated request format is supported, the corresponding + * main content renderer service is initialized. A response is generated by + * calling \Drupal\Core\Render\MainContent\MainContentRendererInterface::renderResponse() + * on the service. That's it! + * + * Each main content renderer service can choose how to implement its + * renderResponse() method. It may of course choose to add protected helper + * methods to provide more structure, if it's a complex main content renderer. + * + * The above is the general flow. But let's take a look at the HTML main content + * renderer (\Drupal\Core\Render\MainContent\HtmlRenderer), because that will be + * used most often. + * + * \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() first calls a + * helper method, @code prepare() @endcode, which takes the main content render + * array and returns a #type 'page' render array. A #type 'page' render array + * represents the final for the HTML document (page.html.twig). The + * remaining task for @code renderResponse() @endcode is to wrap the #type + * 'page' render array in a #type 'html' render array, which then represents the + * entire HTML document (html.html.twig). + * Hence the steps are: + * 1. \Drupal\Core\Render\MainContent\HtmlRenderer::prepare() takes the main + * content render array; if it already is #type 'page', then most of the work + * it must do is already done. In the other case, we need to build that #type + * 'page' render array still. The SystemEvents::SELECT_PAGE_DISPLAY_VARIANT + * event is dispatched, to select a page display variant. By default, + * \Drupal\system\Plugin\DisplayVariant\SimplePageVariant is used, which + * doesn't apply any decorations. But, when Block module is enabled, + * \Drupal\block\Plugin\DisplayVariant\BlockPageVariant is used, which allows + * the site builder to place blocks in any of the page regions, and hence + * "decorate" the main content. + * 2. \Drupal\Core\Render\MainContent\HtmlRenderer::prepare() now is guaranteed + * to be working on a #type 'page' render array. hook_page_attachments() and + * hook_page_attachments_alter() are invoked. + * 3. \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() uses the + * #type 'page' render array returned by the previous step and wraps it in + * #type 'html'. hook_page_top() and hook_page_bottom() are invoked. + * 4. drupal_render() is called on the #type 'html' render array, which uses + * the html.html.twig template and the return value is a HTML document as a + * string. + * 5. This string of HTML is returned as the Response. + * + * For HTML pages to be rendered in limited environments, such as when you are + * installing or updating Drupal, or when you put it in maintenance mode, or + * when an error occurs, a simpler HTML page renderer is used for rendering + * these bare pages: \Drupal\Core\Render\BareHtmlPageRenderer + * + * * @see themeable * * @} diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 02e9f52..ecd2cf2 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -306,7 +306,7 @@ function views_page_display_pre_render(array $element) { /** * Implements MODULE_preprocess_HOOK(). */ -function views_preprocess_page(&$variables) { +function views_preprocess_html(&$variables) { // Early-return to prevent adding unnecessary JavaScript. if (!\Drupal::currentUser()->hasPermission('access contextual links')) { return; @@ -325,20 +325,8 @@ function views_preprocess_page(&$variables) { // page.html.twig, so we can only find it using JavaScript. We therefore // remove 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']) && isset($variables['attributes']['class'])) { - /** @var \Drupal\Core\Page\HtmlPage $page_object */ - $page_object = $variables['page']['#page']; - $attributes = $page_object->getBodyAttributes(); - $class = $attributes['class'] ?: array(); - - $key = array_search('contextual-region', $variables['attributes']['class'] instanceof AttributeArray ? $variables['attributes']['class']->value() : $variables['attributes']['class']); - if ($key !== FALSE) { - /** @var \Drupal\Core\Page\HtmlPage $page_object */ - unset($class[$key]); - $attributes['class'] = $class; - $attributes['data-views-page-contextual-id'] = $variables['title_suffix']['contextual_links']['#id']; - $variables['#attached']['library'][] = 'views/views.contextual-links'; - } + if (!empty($variables['html']['page']['#views_contextual_links'])) { + $variables['attributes']['data-views-page-contextual-id'] = _contextual_links_to_id($variables['html']['page']['#contextual_links']); } } @@ -460,6 +448,12 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable 'display_id' => $display_id, ), ); + // If we're setting contextual links on a page, for a page view, for a + // user that may use contextual links, attach Views' contextual links + // JavaScript. + if ($location === 'page' && $render_element['#type'] === 'page' && \Drupal::currentUser()->hasPermission('access contextual links')) { + $render_element['#attached']['library'][] = 'views/views.contextual-links'; + } } } } diff --git a/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php b/core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php similarity index 44% rename from core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php rename to core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php index a987082..c0651df 100644 --- a/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php +++ b/core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php @@ -2,38 +2,32 @@ /** * @file - * Contains \Drupal\Tests\Core\Controller\AjaxControllerTest. + * Contains \Drupal\Tests\Core\Controller\AjaxRendererTest. */ namespace Drupal\Tests\Core\Controller; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Controller\AjaxController; +use Drupal\Core\Render\MainContent\AjaxRenderer; use Drupal\Tests\UnitTestCase; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; /** - * @coversDefaultClass \Drupal\Core\Controller\AjaxControllerTest + * @coversDefaultClass \Drupal\Core\Render\MainContent\AjaxRenderer * @group Ajax */ -class AjaxControllerTest extends UnitTestCase { +class AjaxRendererTest extends UnitTestCase { /** * The tested ajax controller. * - * @var \Drupal\Tests\Core\Controller\TestAjaxController + * @var \Drupal\Core\Render\MainContent\AjaxRenderer */ - protected $ajaxController; + protected $ajaxRenderer; /** * {@inheritdoc} */ protected function setUp() { - $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); - $controller_resolver->expects($this->any()) - ->method('getArguments') - ->willReturn([]); $element_info_manager = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface'); $element_info_manager->expects($this->any()) ->method('getInfo') @@ -43,22 +37,20 @@ protected function setUp() { '#commands' => array(), '#error' => NULL, ]); - $this->ajaxController = new TestAjaxController($controller_resolver, $element_info_manager); + $this->ajaxRenderer = new TestAjaxRenderer($element_info_manager); } /** * Tests the renderMainContent method. * - * @covers \Drupal\Core\Controller\AjaxController::renderContentIntoResponse + * @covers \Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse */ public function testRenderWithFragmentObject() { $main_content = ['#markup' => 'example content']; $request = new Request(); - $_content = function() use ($main_content) { - return $main_content; - }; + $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); /** @var \Drupal\Core\Ajax\AjaxResponse $result */ - $result = $this->ajaxController->content($request, $_content); + $result = $this->ajaxRenderer->renderResponse($main_content, $request, $route_match); $this->assertInstanceOf('Drupal\Core\Ajax\AjaxResponse', $result); @@ -70,37 +62,9 @@ public function testRenderWithFragmentObject() { $this->assertEquals('status_messages', $commands[1]['data']); } - /** - * Tests the handle method with a Json response object. - * - * @covers \Drupal\Core\Controller\AjaxController::handle - */ - public function testRenderWithResponseObject() { - $json_response = new JsonResponse(array('foo' => 'bar')); - $request = new Request(); - $_content = function() use ($json_response) { - return $json_response; - }; - $this->assertSame($json_response, $this->ajaxController->content($request, $_content)); - } - - /** - * Tests the handle method with an Ajax response object. - * - * @covers \Drupal\Core\Controller\AjaxController::handle - */ - public function testRenderWithAjaxResponseObject() { - $ajax_response = new AjaxResponse(array('foo' => 'bar')); - $request = new Request(); - $_content = function() use ($ajax_response) { - return $ajax_response; - }; - $this->assertSame($ajax_response, $this->ajaxController->content($request, $_content)); - } - } -class TestAjaxController extends AjaxController { +class TestAjaxRenderer extends AjaxRenderer { /** * {@inheritdoc} diff --git a/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php b/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php deleted file mode 100644 index f2305db..0000000 diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme index 60f9a34..7588a8c 100644 --- a/core/themes/bartik/bartik.theme +++ b/core/themes/bartik/bartik.theme @@ -10,51 +10,49 @@ use Drupal\Core\Template\Attribute; /** - * Implements hook_preprocess_HOOK() for page.html.twig. + * Implements hook_preprocess_HOOK() for HTML document templates. * * Adds body classes if certain regions have content. */ -function bartik_preprocess_page(&$variables) { +function bartik_preprocess_html(&$variables) { // Add information about the number of sidebars. - /** @var \Drupal\Core\Page\HtmlPage $page_object */ - $page_object = $variables['page']['#page']; - $attributes = $page_object->getBodyAttributes(); - $classes = $attributes['class']; - if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) { - $classes[] = 'layout-two-sidebars'; + if (!empty($variables['html']['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) { + $variables['attributes']['class'][] = 'layout-two-sidebars'; } - elseif (!empty($variables['page']['sidebar_first'])) { - $classes[] = 'layout-one-sidebar'; - $classes[] = 'layout-sidebar-first'; + elseif (!empty($variables['html']['page']['sidebar_first'])) { + $variables['attributes']['class'][] = 'layout-one-sidebar'; + $variables['attributes']['class'][] = 'layout-sidebar-first'; } - elseif (!empty($variables['page']['sidebar_second'])) { - $classes[] = 'layout-one-sidebar'; - $classes[] = 'layout-sidebar-second'; + elseif (!empty($variables['html']['page']['sidebar_second'])) { + $variables['attributes']['class'][] = 'layout-one-sidebar'; + $variables['attributes']['class'][] = 'layout-sidebar-second'; } else { - $classes[] = 'layout-no-sidebars'; + $variables['attributes']['class'][] = 'layout-no-sidebars'; } - if (!empty($variables['page']['featured'])) { - $classes[] = 'featured'; + if (!empty($variables['html']['page']['featured'])) { + $variables['attributes']['class'][] = 'featured'; } - if (!empty($variables['page']['triptych_first']) - || !empty($variables['page']['triptych_middle']) - || !empty($variables['page']['triptych_last'])) { - $classes[] = 'triptych'; + if (!empty($variables['html']['page']['triptych_first']) + || !empty($variables['html']['page']['triptych_middle']) + || !empty($variables['html']['page']['triptych_last'])) { + $variables['attributes']['class'][] = 'triptych'; } - if (!empty($variables['page']['footer_firstcolumn']) - || !empty($variables['page']['footer_secondcolumn']) - || !empty($variables['page']['footer_thirdcolumn']) - || !empty($variables['page']['footer_fourthcolumn'])) { - $classes[] = 'footer-columns'; + if (!empty($variables['html']['page']['footer_firstcolumn']) + || !empty($variables['html']['page']['footer_secondcolumn']) + || !empty($variables['html']['page']['footer_thirdcolumn']) + || !empty($variables['html']['page']['footer_fourthcolumn'])) { + $variables['attributes']['class'][] = 'footer-columns'; } +} - // Store back the classes to the htmlpage object. - $attributes['class'] = $classes; - +/** + * Implements hook_preprocess_HOOK() for page templates. + */ +function bartik_preprocess_page(&$variables) { // Set the options that apply to both page and maintenance page. _bartik_process_page($variables); diff --git a/core/themes/seven/css/theme/install-page.css b/core/themes/seven/css/theme/install-page.css index c9d124d..395fb9b 100644 --- a/core/themes/seven/css/theme/install-page.css +++ b/core/themes/seven/css/theme/install-page.css @@ -5,7 +5,7 @@ * Unfortunately we have to make our styling quite strong, to override the * .maintenance-page styling. */ -.install-background { +.install-page { background-color: #1275b2; background-image: url(../../images/noise-low.png), diff --git a/core/themes/seven/css/theme/maintenance-page.css b/core/themes/seven/css/theme/maintenance-page.css index edf9018..f2f5a0e 100644 --- a/core/themes/seven/css/theme/maintenance-page.css +++ b/core/themes/seven/css/theme/maintenance-page.css @@ -2,7 +2,7 @@ * @file * Maintenance theming. */ -.maintenance-background { +body.maintenance-page { background-color: #e0e0d8; background-image: -webkit-radial-gradient(hsl(203, 2%, 90%), hsl(203, 2%, 95%)); background-image: radial-gradient(hsl(203, 2%, 90%), hsl(203, 2%, 95%)); diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme index 4863525..76ddfcc 100644 --- a/core/themes/seven/seven.theme +++ b/core/themes/seven/seven.theme @@ -10,24 +10,33 @@ use Drupal\Core\Form\FormStateInterface; /** - * Implements hook_preprocess_HOOK() for page templates. + * Implements hook_preprocess_HOOK() for HTML document templates. */ -function seven_preprocess_page(&$variables) { - /** @var \Drupal\Core\Page\HtmlPage $page_object */ - $page_object = $variables['page']['#page']; - $attributes = $page_object->getBodyAttributes(); - $classes = $attributes['class']; +function seven_preprocess_html(&$variables) { // Add information about the number of sidebars. - if (!empty($variables['page']['sidebar_first'])) { - $classes[] = 'layout-one-sidebar'; - $classes[] = 'layout-sidebar-first'; + $variables['attributes']['class'][] = 'one-sidebar'; + $variables['attributes']['class'][] = 'sidebar-first'; } else { - $classes[] = 'layout-no-sidebars'; + $variables['attributes']['class'][] = 'no-sidebars'; } - $attributes['class'] = $classes; + // If on a node add or edit page, add a node-layout class. + $path_args = explode('/', \Drupal::request()->getPathInfo()); + if ($suggestions = theme_get_suggestions($path_args, 'page', '-')) { + foreach ($suggestions as $suggestion) { + if ($suggestion === 'page-node-edit' || strpos($suggestion, 'page-node-add') !== FALSE) { + $variables['attributes']['class'][] = drupal_html_class('node-form-layout'); + } + } + } +} + +/** + * Implements hook_preprocess_HOOK() for page templates. + */ +function seven_preprocess_page(&$variables) { $variables['primary_local_tasks'] = $variables['tabs']; unset($variables['primary_local_tasks']['#secondary']); $variables['secondary_local_tasks'] = array( @@ -145,12 +154,6 @@ function seven_element_info_alter(&$type) { * Implements hook_preprocess_install_page(). */ function seven_preprocess_install_page(&$variables) { - $page_object = $variables['page']['#page']; - $attributes = $page_object->getHtmlAttributes(); - $classes = $attributes['class']; - $classes[] = 'install-background'; - $attributes['class'] = $classes; - // Seven has custom styling for the install page. $variables['#attached']['library'][] = 'seven/install-page'; } @@ -159,12 +162,6 @@ function seven_preprocess_install_page(&$variables) { * Implements hook_preprocess_maintenance_page(). */ function seven_preprocess_maintenance_page(&$variables) { - $page_object = $variables['page']['#page']; - $attributes = $page_object->getHtmlAttributes(); - $classes = $attributes['class']; - $classes[] = 'maintenance-background'; - $attributes['class'] = $classes; - // Seven has custom styling for the maintenance page. $variables['#attached']['library'][] = 'seven/maintenance-page'; } @@ -208,15 +205,3 @@ function seven_form_node_form_alter(&$form, FormStateInterface $form_state) { $form['revision_information']['#type'] = 'container'; $form['revision_information']['#group'] = 'meta'; } - -function seven_preprocess_html(&$variables) { - // If on a node add or edit page, add a node-layout class. - $path_args = explode('/', \Drupal::request()->getPathInfo()); - if ($suggestions = theme_get_suggestions($path_args, 'page', '-')) { - foreach ($suggestions as $suggestion) { - if ($suggestion === 'page-node-edit' || strpos($suggestion, 'page-node-add') !== FALSE) { - $variables['attributes']['class'][] = drupal_html_class('node-form-layout'); - } - } - } -}