Commit 4c644028 authored by catch's avatar catch

Issue #2068471 by dawehner, Crell, tim.plunkett, jibran, fubhy, larowlan:...

Issue #2068471 by dawehner, Crell, tim.plunkett, jibran, fubhy, larowlan: Normalize Controller/View-listener behavior with a Page object.
parent c2e68a5d
...@@ -384,16 +384,16 @@ services: ...@@ -384,16 +384,16 @@ services:
class: Drupal\Core\Routing\Enhancer\AjaxEnhancer class: Drupal\Core\Routing\Enhancer\AjaxEnhancer
arguments: ['@content_negotiation'] arguments: ['@content_negotiation']
tags: tags:
- { name: route_enhancer, priority: 20 } - { name: route_enhancer, priority: 15 }
- { name: legacy_route_enhancer, priority: 20 } - { name: legacy_route_enhancer, priority: 15 }
route_enhancer.entity: route_enhancer.entity:
class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
arguments: ['@content_negotiation'] arguments: ['@controller_resolver', '@entity.manager', '@form_builder']
tags: tags:
- { name: route_enhancer, priority: 15 } - { name: route_enhancer, priority: 20 }
route_enhancer.form: route_enhancer.form:
class: Drupal\Core\Routing\Enhancer\FormEnhancer class: Drupal\Core\Routing\Enhancer\FormEnhancer
arguments: ['@content_negotiation'] arguments: ['@service_container', '@controller_resolver', '@form_builder']
tags: tags:
- { name: route_enhancer, priority: 10 } - { name: route_enhancer, priority: 10 }
route_special_attributes_subscriber: route_special_attributes_subscriber:
...@@ -402,10 +402,16 @@ services: ...@@ -402,10 +402,16 @@ services:
- { name: event_subscriber } - { name: event_subscriber }
controller.page: controller.page:
class: Drupal\Core\Controller\HtmlPageController class: Drupal\Core\Controller\HtmlPageController
arguments: ['@http_kernel', '@controller_resolver', '@string_translation', '@title_resolver'] arguments: ['@controller_resolver', '@string_translation', '@title_resolver']
controller.ajax:
class: Drupal\Core\Controller\AjaxController
arguments: ['@controller_resolver']
controller.entityform:
class: Drupal\Core\Entity\HtmlEntityFormController
arguments: ['@controller_resolver', '@service_container', '@entity.manager']
controller.dialog: controller.dialog:
class: Drupal\Core\Controller\DialogController class: Drupal\Core\Controller\DialogController
arguments: ['@http_kernel', '@title_resolver'] arguments: ['@controller_resolver', '@title_resolver']
router_listener: router_listener:
class: Symfony\Component\HttpKernel\EventListener\RouterListener class: Symfony\Component\HttpKernel\EventListener\RouterListener
tags: tags:
...@@ -418,6 +424,14 @@ services: ...@@ -418,6 +424,14 @@ services:
tags: tags:
- { name: event_subscriber } - { name: event_subscriber }
arguments: ['@content_negotiation', '@title_resolver'] arguments: ['@content_negotiation', '@title_resolver']
html_view_subscriber:
class: Drupal\Core\EventSubscriber\HtmlViewSubscriber
tags:
- { name: event_subscriber }
arguments: ['@html_page_renderer']
html_page_renderer:
class: Drupal\Core\Page\DefaultHtmlPageRenderer
arguments: ['@language_manager']
private_key: private_key:
class: Drupal\Core\PrivateKey class: Drupal\Core\PrivateKey
arguments: ['@state'] arguments: ['@state']
...@@ -521,7 +535,7 @@ services: ...@@ -521,7 +535,7 @@ services:
arguments: ['@language_manager', '@string_translation'] arguments: ['@language_manager', '@string_translation']
exception_controller: exception_controller:
class: Drupal\Core\Controller\ExceptionController class: Drupal\Core\Controller\ExceptionController
arguments: ['@content_negotiation'] arguments: ['@content_negotiation', '@string_translation', '@title_resolver', '@html_page_renderer']
calls: calls:
- [setContainer, ['@service_container']] - [setContainer, ['@service_container']]
exception_listener: exception_listener:
......
...@@ -150,30 +150,46 @@ function _batch_progress_page() { ...@@ -150,30 +150,46 @@ function _batch_progress_page() {
$batch['url_options']['query']['op'] = $new_op; $batch['url_options']['query']['op'] = $new_op;
$url = url($batch['url'], $batch['url_options']); $url = url($batch['url'], $batch['url_options']);
$element = array(
// Redirect through a 'Refresh' meta tag if JavaScript is disabled. $build = array(
'#prefix' => '<noscript>', '#theme' => 'progress_bar',
'#suffix' => '</noscript>', '#percent' => $percentage,
'#tag' => 'meta', '#message' => $message,
'#attributes' => array( '#label' => $label,
'http-equiv' => 'Refresh', '#attached' => array(
'content' => '0; URL=' . $url, 'drupal_add_html_head' => array(
), array(
); array(
drupal_add_html_head($element, 'batch_progress_meta_refresh'); // Redirect through a 'Refresh' meta tag if JavaScript is disabled.
'#tag' => 'meta',
// Adds JavaScript code and settings for clients where JavaScript is enabled. '#noscript' => TRUE,
$js_setting = array( '#attributes' => array(
'batch' => array( 'http-equiv' => 'Refresh',
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'], 'content' => '0; URL=' . $url,
'initMessage' => $current_set['init_message'], ),
'uri' => $url, ),
'batch_progress_meta_refresh',
),
),
// Adds JavaScript code and settings for clients where JavaScript is enabled.
'js' => array(
array(
'type' => 'setting',
'data' => array(
'batch' => array(
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
),
),
),
),
'library' => array(
array('system', 'drupal.batch'),
),
), ),
); );
drupal_add_js($js_setting, 'setting'); return drupal_render($build);
drupal_add_library('system', 'drupal.batch');
return theme('progress_bar', array('percent' => $percentage, 'message' => $message, 'label' => $label));
} }
/** /**
......
...@@ -354,7 +354,7 @@ function _drupal_default_html_head() { ...@@ -354,7 +354,7 @@ function _drupal_default_html_head() {
/** /**
* Retrieves output to be displayed in the HEAD tag of the HTML page. * Retrieves output to be displayed in the HEAD tag of the HTML page.
*/ */
function drupal_get_html_head() { function drupal_get_html_head() {
$elements = drupal_add_html_head(); $elements = drupal_add_html_head();
drupal_alter('html_head', $elements); drupal_alter('html_head', $elements);
...@@ -3408,7 +3408,12 @@ function drupal_pre_render_html_tag($element) { ...@@ -3408,7 +3408,12 @@ function drupal_pre_render_html_tag($element) {
} }
$markup .= '</' . $element['#tag'] . ">\n"; $markup .= '</' . $element['#tag'] . ">\n";
} }
$element['#markup'] = $markup; if (!empty($element['#noscript'])) {
$element['#markup'] = '<noscript>' . $markup . '</noscript>';
}
else {
$element['#markup'] = $markup;
}
return $element; return $element;
} }
...@@ -3592,7 +3597,7 @@ function drupal_pre_render_dropbutton($element) { ...@@ -3592,7 +3597,7 @@ function drupal_pre_render_dropbutton($element) {
} }
/** /**
* Renders the page, including all theming. * Processes the page render array, enhancing it as necessary.
* *
* @param $page * @param $page
* A string or array representing the content of a page. The array consists of * A string or array representing the content of a page. The array consists of
...@@ -3602,10 +3607,13 @@ function drupal_pre_render_dropbutton($element) { ...@@ -3602,10 +3607,13 @@ function drupal_pre_render_dropbutton($element) {
* - #show_messages: Suppress drupal_get_message() items. Used by Batch * - #show_messages: Suppress drupal_get_message() items. Used by Batch
* API (optional). * API (optional).
* *
* @return array
* The processed render array for the page.
*
* @see hook_page_alter() * @see hook_page_alter()
* @see element_info() * @see element_info()
*/ */
function drupal_render_page($page) { function drupal_prepare_page($page) {
$main_content_display = &drupal_static('system_main_content_added', FALSE); $main_content_display = &drupal_static('system_main_content_added', FALSE);
// Pull out the page title to set it back later. // Pull out the page title to set it back later.
...@@ -3642,6 +3650,28 @@ function drupal_render_page($page) { ...@@ -3642,6 +3650,28 @@ function drupal_render_page($page) {
$page['#title'] = $title; $page['#title'] = $title;
} }
return $page;
}
/**
* Renders the page, including all theming.
*
* @param string|array $page
* A string or array representing the content of a page. The array consists of
* the following keys:
* - #type: Value is always 'page'. This pushes the theming through
* the page template (required).
* - #show_messages: Suppress drupal_get_message() items. Used by Batch
* API (optional).
*
* @return string
* Returns the rendered string.
*
* @see hook_page_alter()
* @see element_info()
*/
function drupal_render_page($page) {
$page = drupal_prepare_page($page);
return drupal_render($page); return drupal_render($page);
} }
......
...@@ -2054,15 +2054,22 @@ function _template_preprocess_default_variables() { ...@@ -2054,15 +2054,22 @@ function _template_preprocess_default_variables() {
* @see system_elements() * @see system_elements()
*/ */
function template_preprocess_html(&$variables) { function template_preprocess_html(&$variables) {
$language_interface = language(Language::TYPE_INTERFACE); /** @var $page \Drupal\Core\Page\HtmlPage */
$page = $variables['page_object'];
$variables['html_attributes'] = $page->getHtmlAttributes();
$variables['attributes'] = $page->getBodyAttributes();
$variables['page'] = $page->getContent();
// Compile a list of classes that are going to be applied to the body element. // 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.). // This allows advanced theming based on context (home page, node of certain type, etc.).
$variables['attributes']['class'][] = 'html'; $body_classes = $variables['attributes']['class'];
$body_classes[] = 'html';
// Add a class that tells us whether we're on the front page or not. // Add a class that tells us whether we're on the front page or not.
$variables['attributes']['class'][] = $variables['is_front'] ? 'front' : 'not-front'; $body_classes[] = $variables['is_front'] ? 'front' : 'not-front';
// Add a class that tells us whether the page is viewed by an authenticated user or not. // Add a class that tells us whether the page is viewed by an authenticated user or not.
$variables['attributes']['class'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; $body_classes[] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in';
$variables['attributes']['class'] = $body_classes;
// Populate the body classes. // Populate the body classes.
if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) {
...@@ -2080,6 +2087,7 @@ function template_preprocess_html(&$variables) { ...@@ -2080,6 +2087,7 @@ function template_preprocess_html(&$variables) {
$variables['html_attributes'] = new Attribute; $variables['html_attributes'] = new Attribute;
// HTML element attributes. // HTML element attributes.
$language_interface = \Drupal::service('language_manager')->getLanguage();
$variables['html_attributes']['lang'] = $language_interface->id; $variables['html_attributes']['lang'] = $language_interface->id;
$variables['html_attributes']['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; $variables['html_attributes']['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
...@@ -2097,9 +2105,9 @@ function template_preprocess_html(&$variables) { ...@@ -2097,9 +2105,9 @@ function template_preprocess_html(&$variables) {
$site_config = \Drupal::config('system.site'); $site_config = \Drupal::config('system.site');
// Construct page title. // Construct page title.
if (!empty($variables['page']['#title'])) { if ($page->hasTitle()) {
$head_title = array( $head_title = array(
'title' => strip_tags($variables['page']['#title']), 'title' => strip_tags($page->getTitle()),
'name' => String::checkPlain($site_config->get('name')), 'name' => String::checkPlain($site_config->get('name')),
); );
} }
...@@ -2149,15 +2157,8 @@ function template_preprocess_html(&$variables) { ...@@ -2149,15 +2157,8 @@ function template_preprocess_html(&$variables) {
drupal_add_library('system', 'html5shiv', TRUE); drupal_add_library('system', 'html5shiv', TRUE);
// Render page_top and page_bottom into top level variables. $variables['page_top'][] = array('#markup' => $page->getBodyTop());
$variables['page_top'] = array(); $variables['page_bottom'][] = array('#markup' => $page->getBodyBottom());
if (isset($variables['page']['page_top'])) {
$variables['page_top'] = drupal_render($variables['page']['page_top']);
}
$variables['page_bottom'] = array();
if (isset($variables['page']['page_bottom'])) {
$variables['page_bottom'][]['#markup'] = drupal_render($variables['page']['page_bottom']);
}
// Add footer scripts as '#markup' so they can be rendered with other // Add footer scripts as '#markup' so they can be rendered with other
// elements in page_bottom. // elements in page_bottom.
...@@ -2557,7 +2558,7 @@ function drupal_common_theme() { ...@@ -2557,7 +2558,7 @@ function drupal_common_theme() {
return array( return array(
// From theme.inc. // From theme.inc.
'html' => array( 'html' => array(
'render element' => 'page', 'variables' => array('page_object' => NULL),
'template' => 'html', 'template' => 'html',
), ),
'page' => array( 'page' => array(
...@@ -2631,7 +2632,7 @@ function drupal_common_theme() { ...@@ -2631,7 +2632,7 @@ function drupal_common_theme() {
), ),
// From theme.maintenance.inc. // From theme.maintenance.inc.
'maintenance_page' => array( 'maintenance_page' => array(
'variables' => array('content' => NULL, 'show_messages' => TRUE), 'variables' => array('content' => NULL, 'show_messages' => TRUE, 'page' => array()),
'template' => 'maintenance-page', 'template' => 'maintenance-page',
), ),
'install_page' => array( 'install_page' => array(
......
...@@ -10,14 +10,34 @@ ...@@ -10,14 +10,34 @@
use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\PrependCommand; use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Page\HtmlFragment;
use Drupal\Core\Page\HtmlPage;
use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/** /**
* Default controller for ajax requests. * Default controller for ajax requests.
*/ */
class AjaxController extends ContainerAware { class AjaxController extends ContainerAware {
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* Constructs a new AjaxController instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
*/
public function __construct(ControllerResolverInterface $controller_resolver) {
$this->controllerResolver = $controller_resolver;
}
/** /**
* Controller method for AJAX content. * Controller method for AJAX content.
* *
...@@ -30,50 +50,66 @@ class AjaxController extends ContainerAware { ...@@ -30,50 +50,66 @@ class AjaxController extends ContainerAware {
* A response object. * A response object.
*/ */
public function content(Request $request, $_content) { public function content(Request $request, $_content) {
$content = $this->getContentResult($request, $_content);
// If there is already an AjaxResponse, then return it without
// manipulation.
if ($content instanceof AjaxResponse && $content->isOk()) {
return $content;
}
// @todo When we have a Generator, we can replace the forward() call with // Allow controllers to return a HtmlFragment or a Response object directly.
// a render() call, which would handle ESI and hInclude as well. That will if ($content instanceof HtmlFragment) {
// require an _internal route. For examples, see: $content = $content->getContent();
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml }
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php if ($content instanceof Response) {
$attributes = clone $request->attributes; $content = $content->getContent();
$controller = $_content; }
// We need to clean up the derived information and such so that the // Most controllers return a render array, but some return a string.
// subrequest can be processed properly without leaking data through. if (!is_array($content)) {
$attributes->remove('_system_path'); $content = array(
$attributes->remove('_content'); '#markup' => $content,
$attributes->remove('_legacy'); );
}
// Remove the accept header so the subrequest does not end up back in this $html = drupal_render($content);
// controller.
$request->headers->remove('accept');
// Remove the header in order to let the subrequest not think that it's an
// ajax request, see \Drupal\Core\ContentNegotiation.
$request->headers->remove('x-requested-with');
$response = $this->container->get('http_kernel')->forward($controller, $attributes->all(), $request->query->all()); $response = new AjaxResponse();
// For successful (HTTP status 200) responses. // The selector for the insert command is NULL as the new content will
if ($response->isOk()) { // replace the element making the ajax call. The default 'replaceWith'
// If there is already an AjaxResponse, then return it without // behavior can be changed with #ajax['method'].
// manipulation. $response->addCommand(new InsertCommand(NULL, $html));
if (!($response instanceof AjaxResponse)) { $status_messages = array('#theme' => 'status_messages');
// Pull the content out of the response. $output = drupal_render($status_messages);
$content = $response->getContent(); if (!empty($output)) {
// A page callback could return a render array or a string. $response->addCommand(new PrependCommand(NULL, $output));
$html = is_string($content) ? $content : drupal_render($content);
$response = new AjaxResponse();
// The selector for the insert command is NULL as the new content will
// replace the element making the ajax call. The default 'replaceWith'
// behavior can be changed with #ajax['method'].
$response->addCommand(new InsertCommand(NULL, $html));
$status_messages = array('#theme' => 'status_messages');
$output = drupal_render($status_messages);
if (!empty($output)) {
$response->addCommand(new PrependCommand(NULL, $output));
}
}
} }
return $response; return $response;
} }
/**
* Returns the result of invoking the sub-controller.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param mixed $controller_definition
* A controller definition string, or a callable object/closure.
*
* @return 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;
}
} }
...@@ -9,9 +9,10 @@ ...@@ -9,9 +9,10 @@
use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenDialogCommand; use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\Page\HtmlPage;
use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Response;
/** /**
* Defines a default controller for dialog requests. * Defines a default controller for dialog requests.
...@@ -19,11 +20,11 @@ ...@@ -19,11 +20,11 @@
class DialogController { class DialogController {
/** /**
* The HttpKernel object to use for subrequests. * The controller resolver service.
* *
* @var \Symfony\Component\HttpKernel\HttpKernelInterface * @var \Drupal\Core\Controller\ControllerResolverInterface
*/ */
protected $httpKernel; protected $controllerResolver;
/** /**
* The title resolver. * The title resolver.
...@@ -35,112 +36,126 @@ class DialogController { ...@@ -35,112 +36,126 @@ class DialogController {
/** /**
* Constructs a new DialogController. * Constructs a new DialogController.
* *
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The kernel. * The controller resolver service.
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver. * The title resolver.
*/ */
public function __construct(HttpKernelInterface $kernel, TitleResolverInterface $title_resolver) { public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver) {
$this->httpKernel = $kernel; $this->controllerResolver = $controller_resolver;
$this->titleResolver = $title_resolver;