From 4c6440288df411d896d73d8512df43cc30f98bae Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 1 Jan 2014 20:32:52 +0000 Subject: [PATCH] Issue #2068471 by dawehner, Crell, tim.plunkett, jibran, fubhy, larowlan: Normalize Controller/View-listener behavior with a Page object. --- core/core.services.yml | 30 ++- core/includes/batch.inc | 60 +++--- core/includes/common.inc | 38 +++- core/includes/theme.inc | 35 ++-- .../Drupal/Core/Controller/AjaxController.php | 114 +++++++---- .../Core/Controller/DialogController.php | 177 ++++++++++-------- .../Core/Controller/ExceptionController.php | 96 +++++++--- .../Drupal/Core/Controller/FormController.php | 96 ++++++++++ .../Core/Controller/HtmlControllerBase.php | 87 +++++++++ .../Core/Controller/HtmlFormController.php | 52 ++--- .../Core/Controller/HtmlPageController.php | 84 +++------ .../Entity/Enhancer/EntityRouteEnhancer.php | 48 +++-- .../Core/Entity/HtmlEntityFormController.php | 37 ++-- .../EventSubscriber/HtmlViewSubscriber.php | 85 +++++++++ .../Core/EventSubscriber/ViewSubscriber.php | 21 +-- .../Core/Page/DefaultHtmlPageRenderer.php | 101 ++++++++++ core/lib/Drupal/Core/Page/HeadElement.php | 89 +++++++++ core/lib/Drupal/Core/Page/HtmlFragment.php | 126 +++++++++++++ core/lib/Drupal/Core/Page/HtmlPage.php | 160 ++++++++++++++++ .../Core/Page/HtmlPageRendererInterface.php | 49 +++++ .../Core/Routing/Enhancer/AjaxEnhancer.php | 4 +- .../Enhancer/ContentControllerEnhancer.php | 13 +- .../Core/Routing/Enhancer/FormEnhancer.php | 46 +++-- core/lib/Drupal/Core/Routing/RouteBuilder.php | 3 +- .../lib/Drupal/Core/Routing/RouteProvider.php | 2 +- .../lib/Drupal/Core/Routing/RoutingEvents.php | 5 + .../Core/Template/AttributeValueBase.php | 4 +- core/lib/Drupal/Core/Utility/Title.php | 5 + .../modules/aggregator/aggregator.routing.yml | 2 +- core/modules/block/block.routing.yml | 2 +- .../Drupal/comment/CommentFormController.php | 2 +- .../tests/config_test/config_test.routing.yml | 2 +- .../search/Tests/SearchPageOverrideTest.php | 1 + .../Tests/DrupalUnitTestBaseTest.php | 2 +- .../system/Controller/BatchController.php | 78 +++++++- .../system/Tests/System/PageTitleTest.php | 7 - core/modules/system/system.module | 14 +- core/modules/system/system.routing.yml | 11 +- .../ActiveTrailTestSubscriber.php | 58 ++++++ .../router_test/RouteTestSubscriber.php | 2 +- .../router_test.routing.yml | 2 +- .../EventSubscriber/SessionTestSubscriber.php | 2 +- .../session_test/session_test.routing.yml | 18 +- .../PageCacheAcceptHeaderController.php | 3 +- .../test_page_test/test_page_test.routing.yml | 7 - .../modules/theme_test/theme_test.module | 5 +- .../Drupal/user/Controller/UserController.php | 12 +- .../Drupal/user/Tests/Views/BulkFormTest.php | 2 - .../views/EventSubscriber/RouteSubscriber.php | 38 +++- .../Plugin/views/area/HTTPStatusCode.php | 1 + .../views/Plugin/views/display/Page.php | 31 ++- .../views/Routing/ViewPageController.php | 1 + .../views/lib/Drupal/views/ViewExecutable.php | 30 ++- .../EventSubscriber/RouteSubscriberTest.php | 4 +- core/modules/views/views.module | 16 +- core/modules/views/views.services.yml | 1 - core/modules/views_ui/views_ui.routing.yml | 20 +- .../Tests/Core/Common/AttributesTest.php | 2 +- .../Controller/ExceptionControllerTest.php | 27 ++- .../Enhancer/EntityRouteEnhancerTest.php | 33 ++-- core/themes/bartik/bartik.theme | 34 ++-- core/themes/seven/seven.theme | 21 ++- 62 files changed, 1677 insertions(+), 481 deletions(-) create mode 100644 core/lib/Drupal/Core/Controller/FormController.php create mode 100644 core/lib/Drupal/Core/Controller/HtmlControllerBase.php create mode 100644 core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php create mode 100644 core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php create mode 100644 core/lib/Drupal/Core/Page/HeadElement.php create mode 100644 core/lib/Drupal/Core/Page/HtmlFragment.php create mode 100644 core/lib/Drupal/Core/Page/HtmlPage.php create mode 100644 core/lib/Drupal/Core/Page/HtmlPageRendererInterface.php create mode 100644 core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailTestSubscriber.php diff --git a/core/core.services.yml b/core/core.services.yml index 5f24938843..a2d373cffd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -384,16 +384,16 @@ services: class: Drupal\Core\Routing\Enhancer\AjaxEnhancer arguments: ['@content_negotiation'] tags: - - { name: route_enhancer, priority: 20 } - - { name: legacy_route_enhancer, priority: 20 } + - { name: route_enhancer, priority: 15 } + - { name: legacy_route_enhancer, priority: 15 } route_enhancer.entity: class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer - arguments: ['@content_negotiation'] + arguments: ['@controller_resolver', '@entity.manager', '@form_builder'] tags: - - { name: route_enhancer, priority: 15 } + - { name: route_enhancer, priority: 20 } route_enhancer.form: class: Drupal\Core\Routing\Enhancer\FormEnhancer - arguments: ['@content_negotiation'] + arguments: ['@service_container', '@controller_resolver', '@form_builder'] tags: - { name: route_enhancer, priority: 10 } route_special_attributes_subscriber: @@ -402,10 +402,16 @@ services: - { name: event_subscriber } controller.page: 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: class: Drupal\Core\Controller\DialogController - arguments: ['@http_kernel', '@title_resolver'] + arguments: ['@controller_resolver', '@title_resolver'] router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener tags: @@ -418,6 +424,14 @@ services: tags: - { name: event_subscriber } 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: class: Drupal\Core\PrivateKey arguments: ['@state'] @@ -521,7 +535,7 @@ services: arguments: ['@language_manager', '@string_translation'] exception_controller: class: Drupal\Core\Controller\ExceptionController - arguments: ['@content_negotiation'] + arguments: ['@content_negotiation', '@string_translation', '@title_resolver', '@html_page_renderer'] calls: - [setContainer, ['@service_container']] exception_listener: diff --git a/core/includes/batch.inc b/core/includes/batch.inc index 049b3fb8f9..597ab43a74 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -150,30 +150,46 @@ function _batch_progress_page() { $batch['url_options']['query']['op'] = $new_op; $url = url($batch['url'], $batch['url_options']); - $element = array( - // Redirect through a 'Refresh' meta tag if JavaScript is disabled. - '#prefix' => '', - '#tag' => 'meta', - '#attributes' => array( - 'http-equiv' => 'Refresh', - 'content' => '0; URL=' . $url, - ), - ); - drupal_add_html_head($element, 'batch_progress_meta_refresh'); - - // Adds JavaScript code and settings for clients where JavaScript is enabled. - $js_setting = array( - 'batch' => array( - 'errorMessage' => $current_set['error_message'] . '
' . $batch['error_message'], - 'initMessage' => $current_set['init_message'], - 'uri' => $url, + + $build = array( + '#theme' => 'progress_bar', + '#percent' => $percentage, + '#message' => $message, + '#label' => $label, + '#attached' => array( + 'drupal_add_html_head' => array( + array( + array( + // Redirect through a 'Refresh' meta tag if JavaScript is disabled. + '#tag' => 'meta', + '#noscript' => TRUE, + '#attributes' => array( + 'http-equiv' => 'Refresh', + 'content' => '0; URL=' . $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'] . '
' . $batch['error_message'], + 'initMessage' => $current_set['init_message'], + 'uri' => $url, + ), + ), + ), + ), + 'library' => array( + array('system', 'drupal.batch'), + ), ), ); - drupal_add_js($js_setting, 'setting'); - drupal_add_library('system', 'drupal.batch'); - - return theme('progress_bar', array('percent' => $percentage, 'message' => $message, 'label' => $label)); + return drupal_render($build); } /** diff --git a/core/includes/common.inc b/core/includes/common.inc index e70816be28..aff76651bc 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -354,7 +354,7 @@ function _drupal_default_html_head() { /** * Retrieves output to be displayed in the HEAD tag of the HTML page. - */ + */ function drupal_get_html_head() { $elements = drupal_add_html_head(); drupal_alter('html_head', $elements); @@ -3408,7 +3408,12 @@ function drupal_pre_render_html_tag($element) { } $markup .= '\n"; } - $element['#markup'] = $markup; + if (!empty($element['#noscript'])) { + $element['#markup'] = ''; + } + else { + $element['#markup'] = $markup; + } return $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 * A string or array representing the content of a page. The array consists of @@ -3602,10 +3607,13 @@ function drupal_pre_render_dropbutton($element) { * - #show_messages: Suppress drupal_get_message() items. Used by Batch * API (optional). * + * @return array + * The processed render array for the page. + * * @see hook_page_alter() * @see element_info() */ -function drupal_render_page($page) { +function drupal_prepare_page($page) { $main_content_display = &drupal_static('system_main_content_added', FALSE); // Pull out the page title to set it back later. @@ -3642,6 +3650,28 @@ function drupal_render_page($page) { $page['#title'] = $title; } + return $page; +} + +/** + * Renders the page, including all theming. + * + * @param string|array $page + * A string or array representing the content of a page. The array consists of + * the following keys: + * - #type: Value is always 'page'. This pushes the theming through + * the page template (required). + * - #show_messages: Suppress drupal_get_message() items. Used by Batch + * API (optional). + * + * @return string + * Returns the rendered string. + * + * @see hook_page_alter() + * @see element_info() + */ +function drupal_render_page($page) { + $page = drupal_prepare_page($page); return drupal_render($page); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 9545be72ec..3db84a97b1 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2054,15 +2054,22 @@ function _template_preprocess_default_variables() { * @see system_elements() */ function template_preprocess_html(&$variables) { - $language_interface = language(Language::TYPE_INTERFACE); + /** @var $page \Drupal\Core\Page\HtmlPage */ + $page = $variables['page_object']; + + $variables['html_attributes'] = $page->getHtmlAttributes(); + $variables['attributes'] = $page->getBodyAttributes(); + $variables['page'] = $page->getContent(); // Compile a list of classes that are going to be applied to the body element. // This allows advanced theming based on context (home page, node of certain type, etc.). - $variables['attributes']['class'][] = 'html'; + $body_classes = $variables['attributes']['class']; + $body_classes[] = 'html'; // Add a class that tells us whether we're on the front page or not. - $variables['attributes']['class'][] = $variables['is_front'] ? 'front' : 'not-front'; + $body_classes[] = $variables['is_front'] ? 'front' : 'not-front'; // Add a class that tells us whether the page is viewed by an authenticated user or not. - $variables['attributes']['class'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; + $body_classes[] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; + $variables['attributes']['class'] = $body_classes; // Populate the body classes. if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { @@ -2080,6 +2087,7 @@ function template_preprocess_html(&$variables) { $variables['html_attributes'] = new Attribute; // HTML element attributes. + $language_interface = \Drupal::service('language_manager')->getLanguage(); $variables['html_attributes']['lang'] = $language_interface->id; $variables['html_attributes']['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; @@ -2097,9 +2105,9 @@ function template_preprocess_html(&$variables) { $site_config = \Drupal::config('system.site'); // Construct page title. - if (!empty($variables['page']['#title'])) { + if ($page->hasTitle()) { $head_title = array( - 'title' => strip_tags($variables['page']['#title']), + 'title' => strip_tags($page->getTitle()), 'name' => String::checkPlain($site_config->get('name')), ); } @@ -2149,15 +2157,8 @@ function template_preprocess_html(&$variables) { drupal_add_library('system', 'html5shiv', TRUE); - // Render page_top and page_bottom into top level variables. - $variables['page_top'] = array(); - if (isset($variables['page']['page_top'])) { - $variables['page_top'] = drupal_render($variables['page']['page_top']); - } - $variables['page_bottom'] = array(); - if (isset($variables['page']['page_bottom'])) { - $variables['page_bottom'][]['#markup'] = drupal_render($variables['page']['page_bottom']); - } + $variables['page_top'][] = array('#markup' => $page->getBodyTop()); + $variables['page_bottom'][] = array('#markup' => $page->getBodyBottom()); // Add footer scripts as '#markup' so they can be rendered with other // elements in page_bottom. @@ -2557,7 +2558,7 @@ function drupal_common_theme() { return array( // From theme.inc. 'html' => array( - 'render element' => 'page', + 'variables' => array('page_object' => NULL), 'template' => 'html', ), 'page' => array( @@ -2631,7 +2632,7 @@ function drupal_common_theme() { ), // From theme.maintenance.inc. 'maintenance_page' => array( - 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'variables' => array('content' => NULL, 'show_messages' => TRUE, 'page' => array()), 'template' => 'maintenance-page', ), 'install_page' => array( diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php index 7b2cece4fa..2f8498e12f 100644 --- a/core/lib/Drupal/Core/Controller/AjaxController.php +++ b/core/lib/Drupal/Core/Controller/AjaxController.php @@ -10,14 +10,34 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Ajax\PrependCommand; +use Drupal\Core\Page\HtmlFragment; +use Drupal\Core\Page\HtmlPage; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Default controller for ajax requests. */ 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. * @@ -30,50 +50,66 @@ class AjaxController extends ContainerAware { * A response object. */ public function content(Request $request, $_content) { + $content = $this->getContentResult($request, $_content); + // If there is already an AjaxResponse, then return it without + // manipulation. + if ($content instanceof AjaxResponse && $content->isOk()) { + return $content; + } - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - $controller = $_content; + // Allow controllers to return a HtmlFragment or a Response object directly. + if ($content instanceof HtmlFragment) { + $content = $content->getContent(); + } + if ($content instanceof Response) { + $content = $content->getContent(); + } - // We need to clean up the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('_system_path'); - $attributes->remove('_content'); - $attributes->remove('_legacy'); + // Most controllers return a render array, but some return a string. + if (!is_array($content)) { + $content = array( + '#markup' => $content, + ); + } - // Remove the accept header so the subrequest does not end up back in this - // controller. - $request->headers->remove('accept'); - // Remove the header in order to let the subrequest not think that it's an - // ajax request, see \Drupal\Core\ContentNegotiation. - $request->headers->remove('x-requested-with'); + $html = drupal_render($content); - $response = $this->container->get('http_kernel')->forward($controller, $attributes->all(), $request->query->all()); - // For successful (HTTP status 200) responses. - if ($response->isOk()) { - // If there is already an AjaxResponse, then return it without - // manipulation. - if (!($response instanceof AjaxResponse)) { - // Pull the content out of the response. - $content = $response->getContent(); - // A page callback could return a render array or a string. - $html = is_string($content) ? $content : drupal_render($content); - $response = new AjaxResponse(); - // The selector for the insert command is NULL as the new content will - // replace the element making the ajax call. The default 'replaceWith' - // behavior can be changed with #ajax['method']. - $response->addCommand(new InsertCommand(NULL, $html)); - $status_messages = array('#theme' => 'status_messages'); - $output = drupal_render($status_messages); - if (!empty($output)) { - $response->addCommand(new PrependCommand(NULL, $output)); - } - } + $response = new AjaxResponse(); + // The selector for the insert command is NULL as the new content will + // replace the element making the ajax call. The default 'replaceWith' + // behavior can be changed with #ajax['method']. + $response->addCommand(new InsertCommand(NULL, $html)); + $status_messages = array('#theme' => 'status_messages'); + $output = drupal_render($status_messages); + if (!empty($output)) { + $response->addCommand(new PrependCommand(NULL, $output)); } return $response; } + + /** + * Returns the result of invoking the sub-controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * + * @return 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; + } + } diff --git a/core/lib/Drupal/Core/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php index 7bb34ffad3..5fda730ad9 100644 --- a/core/lib/Drupal/Core/Controller/DialogController.php +++ b/core/lib/Drupal/Core/Controller/DialogController.php @@ -9,9 +9,10 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Page\HtmlPage; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpFoundation\Response; /** * Defines a default controller for dialog requests. @@ -19,11 +20,11 @@ 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. @@ -35,112 +36,126 @@ class DialogController { /** * Constructs a new DialogController. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel - * The kernel. + * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver + * The controller resolver service. * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver * The title resolver. */ - public function __construct(HttpKernelInterface $kernel, TitleResolverInterface $title_resolver) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver) { + $this->controllerResolver = $controller_resolver; $this->titleResolver = $title_resolver; } - /** - * Forwards request to a subrequest. - * - * @param \Symfony\Component\HttpFoundation\RequestRequest $request - * The request object. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - protected function forward(Request $request) { - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - // We need to clean up the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('_system_path'); - $attributes->set('dialog', TRUE); - - // Remove the accept header so the subrequest does not end up back in this - // controller. - $request->headers->remove('accept'); - // Remove the X-Requested-With header so the subrequest is not mistaken for - // an ajax request. - $request->headers->remove('x-requested-with'); - - return $this->httpKernel->forward(NULL, $attributes->all(), $request->query->all()); - } - /** * Displays content in a modal dialog. * - * @param \Symfony\Component\HttpFoundation\RequestRequest $request + * @param \Symfony\Component\HttpFoundation\Request $request * The request object. + * @param mixed $_content + * A controller definition string, or a callable object/closure. * * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a modal dialog. */ - public function modal(Request $request) { - return $this->dialog($request, TRUE); + public function modal(Request $request, $_content) { + return $this->dialog($request, $_content, TRUE); } /** * Displays content in a dialog. * - * @param \Symfony\Component\HttpFoundation\RequestRequest $request + * @param \Symfony\Component\HttpFoundation\Request $request * The request object. + * @param mixed $_content + * A controller definition string, or a callable object/closure. * @param bool $modal * (optional) TRUE to render a modal dialog. Defaults to FALSE. * * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a dialog. */ - public function dialog(Request $request, $modal = FALSE) { - $subrequest = $this->forward($request); - if ($subrequest->isOk()) { - $content = $subrequest->getContent(); - if (!$title = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT))) { - // @todo Remove use of drupal_get_title() when - // http://drupal.org/node/1871596 is in. - $title = drupal_get_title(); - } - $response = new AjaxResponse(); - // Fetch any modal options passed in from data-dialog-options. - if (!($options = $request->request->get('dialogOptions'))) { - $options = array(); - } - // Set modal flag and re-use the modal ID. - if ($modal) { - $options['modal'] = TRUE; - $target = '#drupal-modal'; + public function dialog(Request $request, $_content, $modal = FALSE) { + $page_content = $this->getContentResult($request, $_content); + + // Allow controllers to return a HtmlPage or a Response object directly. + if ($page_content instanceof HtmlPage) { + $page_content = $page_content->getContent(); + } + if ($page_content instanceof Response) { + $page_content = $page_content->getContent(); + } + + // Most controllers return a render array, but some return a string. + if (!is_array($page_content)) { + $page_content = array( + '#markup' => $page_content, + ); + } + + $content = drupal_render($page_content); + + // @todo Remove use of drupal_get_title() when + // http://drupal.org/node/1871596 is in. + if (!$title = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT))) { + // @todo Remove use of drupal_get_title() when + // http://drupal.org/node/1871596 is in. + $title = drupal_get_title(); + } + $response = new AjaxResponse(); + // Fetch any modal options passed in from data-dialog-options. + if (!($options = $request->request->get('dialogOptions'))) { + $options = array(); + } + // Set modal flag and re-use the modal ID. + if ($modal) { + $options['modal'] = TRUE; + $target = '#drupal-modal'; + } + else { + // Generate the target wrapper for the dialog. + if (isset($options['target'])) { + // If the target was nominated in the incoming options, use that. + $target = $options['target']; + // Ensure the target includes the #. + if (substr($target, 0, 1) != '#') { + $target = '#' . $target; + } + // This shouldn't be passed on to jQuery.ui.dialog. + unset($options['target']); } else { - // Generate the target wrapper for the dialog. - if (isset($options['target'])) { - // If the target was nominated in the incoming options, use that. - $target = $options['target']; - // Ensure the target includes the #. - if (substr($target, 0, 1) != '#') { - $target = '#' . $target; - } - // This shouldn't be passed on to jQuery.ui.dialog. - unset($options['target']); - } - else { - // Generate a target based on the route id. - $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); - $target = '#' . drupal_html_id("drupal-dialog-$route_name"); - } + // Generate a target based on the route id. + $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); + $target = '#' . drupal_html_id("drupal-dialog-$route_name"); } - $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); - return $response; } - // An error occurred in the subrequest, return that. - return $subrequest; + $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); + return $response; + } + + /** + * Returns the result of invoking the sub-controller. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * + * @return 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; } + } diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index 2ba6f2ca46..af66226f32 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -7,7 +7,10 @@ namespace Drupal\Core\Controller; -use Symfony\Component\DependencyInjection\ContainerAware; +use Drupal\Core\Page\HtmlPageRendererInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; @@ -19,7 +22,7 @@ /** * This controller handles HTTP errors generated by the routing system. */ -class ExceptionController extends ContainerAware { +class ExceptionController extends HtmlControllerBase implements ContainerAwareInterface { /** * The content negotiation library. @@ -28,15 +31,49 @@ class ExceptionController extends ContainerAware { */ protected $negotiation; + /** + * The service container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * The page rendering service. + * + * @var \Drupal\Core\Page\HtmlPageRendererInterface + */ + protected $renderer; + /** * Constructor. * * @param \Drupal\Core\ContentNegotiation $negotiation * The content negotiation library to use to determine the correct response * format. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager + * The translation manager. + * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver + * The title resolver. + * @param \Drupal\Core\Page\HtmlPageRendererInterface $renderer + * The page renderer. */ - public function __construct(ContentNegotiation $negotiation) { + public function __construct(ContentNegotiation $negotiation, TranslationInterface $translation_manager, TitleResolverInterface $title_resolver, HtmlPageRendererInterface $renderer) { + parent::__construct($translation_manager, $title_resolver); $this->negotiation = $negotiation; + $this->renderer = $renderer; + } + + /** + * Sets the Container associated with this Controller. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * A ContainerInterface instance. + * + * @api + */ + public function setContainer(ContainerInterface $container = NULL) { + $this->container = $container; } /** @@ -90,14 +127,15 @@ public function on403Html(FlattenException $exception, Request $request) { $system_path = $request->attributes->get('_system_path'); watchdog('access denied', $system_path, NULL, WATCHDOG_WARNING); - $path = $this->container->get('path.alias_manager')->getSystemPath(\Drupal::config('system.site')->get('page.403')); + $system_config = $this->container->get('config.factory')->get('system.site'); + $path = $this->container->get('path.alias_manager')->getSystemPath($system_config->get('page.403')); if ($path && $path != $system_path) { // Keep old path for reference, and to allow forms to redirect to it. if (!$request->query->has('destination')) { $request->query->set('destination', $system_path); } - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path, '_exception_statuscode' => 403), $request->cookies->all(), array(), $request->server->all()); // The active trail is being statically cached from the parent request to // the subrequest, like any other static. Unfortunately that means the @@ -119,15 +157,15 @@ public function on403Html(FlattenException $exception, Request $request) { $response->setStatusCode(403, 'Access denied'); } else { + $page_content = array( + '#markup' => t('You are not authorized to access this page.'), + '#title' => t('Access denied'), + ); - // @todo Replace this block with something cleaner. - $return = t('You are not authorized to access this page.'); - drupal_set_title(t('Access denied')); - drupal_set_page_content($return); - $page = element_info('page'); - $content = drupal_render_page($page); - - $response = new Response($content, 403); + $fragment = $this->createHtmlFragment($page_content, $request); + $page = $this->renderer->render($fragment, 403); + $response = new Response($this->renderer->renderPage($page), $page->getStatusCode()); + return $response; } return $response; @@ -173,7 +211,8 @@ public function on404Html(FlattenException $exception, Request $request) { // that and sub-call the kernel rather than using meah(). // @todo The create() method expects a slash-prefixed path, but we store a // normal system path in the site_404 variable. - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); + + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path, '_exception_statuscode' => 403), $request->cookies->all(), array(), $request->server->all()); // The active trail is being statically cached from the parent request to // the subrequest, like any other static. Unfortunately that means the @@ -195,14 +234,15 @@ public function on404Html(FlattenException $exception, Request $request) { $response->setStatusCode(404, 'Not Found'); } else { - // @todo Replace this block with something cleaner. - $return = t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())); - drupal_set_title(t('Page not found')); - drupal_set_page_content($return); - $page = element_info('page'); - $content = drupal_render_page($page); - - $response = new Response($content, 404); + $page_content = array( + '#markup' => t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())), + '#title' => t('Page not found'), + ); + + $fragment = $this->createHtmlFragment($page_content, $request); + $page = $this->renderer->render($fragment, 404); + $response = new Response($this->renderer->renderPage($page), $page->getStatusCode()); + return $response; } return $response; @@ -264,16 +304,16 @@ public function on500Html(FlattenException $exception, Request $request) { drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); } - drupal_set_title(t('Error')); - // We fallback to a maintenance page at this point, because the page - // generation itself can generate errors. - $maintenance_page = array( + $page_content = array( '#theme' => 'maintenance_page', '#content' => t('The website has encountered an error. Please try again later.'), + '#page' => array( + '#title' => t('Error'), + ), ); - $output = drupal_render($maintenance_page); - $response = new Response($output, 500); + $output = drupal_render($page_content); + $response = new Response($output); $response->setStatusCode(500, '500 Service unavailable (with message)'); return $response; diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php new file mode 100644 index 0000000000..271de99f39 --- /dev/null +++ b/core/lib/Drupal/Core/Controller/FormController.php @@ -0,0 +1,96 @@ +resolver = $resolver; + $this->formBuilder = $form_builder; + } + + /** + * Invokes the form and returns the result. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return array + * The render array that results from invoking the controller. + */ + public function getContentResult(Request $request) { + $form_object = $this->getFormObject($request, $this->formDefinition); + + // Add the form and form_state to trick the getArguments method of the + // controller resolver. + $form_state = array(); + $request->attributes->set('form', array()); + $request->attributes->set('form_state', $form_state); + $args = $this->resolver->getArguments($request, array($form_object, 'buildForm')); + $request->attributes->remove('form'); + $request->attributes->remove('form_state'); + + // Remove $form and $form_state from the arguments, and re-index them. + unset($args[0], $args[1]); + $form_state['build_info']['args'] = array_values($args); + + $form_id = $this->formBuilder->getFormId($form_object, $form_state); + return $this->formBuilder->buildForm($form_id, $form_state); + } + + /** + * Returns the object used to build the form. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request using this form. + * @param string $form_arg + * Either a class name or a service ID. + * + * @return \Drupal\Core\Form\FormInterface + * The form object to use. + */ + abstract protected function getFormObject(Request $request, $form_arg); + +} diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php new file mode 100644 index 0000000000..bf7be8df3c --- /dev/null +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php @@ -0,0 +1,87 @@ +translationManager = $translation_manager; + $this->titleResolver = $title_resolver; + } + + /** + * Converts a render array into an HtmlFragment object. + * + * @param array|string $page_content + * The page content area to display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Drupal\Core\Page\HtmlPage + * A page object. + */ + protected function createHtmlFragment($page_content, Request $request) { + // Allow controllers to return a HtmlFragment or a Response object directly. + if ($page_content instanceof HtmlFragment || $page_content instanceof Response) { + return $page_content; + } + + if (!is_array($page_content)) { + $page_content = array( + 'main' => array( + '#markup' => $page_content, + ), + ); + } + + $fragment = new HtmlFragment(drupal_render($page_content)); + + // A title defined in the return always wins. + if (isset($page_content['#title'])) { + $fragment->setTitle($page_content['#title'], Title::FILTER_XSS_ADMIN); + } + else if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) { + $fragment->setTitle($this->titleResolver->getTitle($request, $route), PASS_THROUGH); + } + + return $fragment; + } + +} diff --git a/core/lib/Drupal/Core/Controller/HtmlFormController.php b/core/lib/Drupal/Core/Controller/HtmlFormController.php index db783837d0..2e92c80073 100644 --- a/core/lib/Drupal/Core/Controller/HtmlFormController.php +++ b/core/lib/Drupal/Core/Controller/HtmlFormController.php @@ -2,20 +2,19 @@ /** * @file - * Contains \Drupal\Core\Controller\HtmlFormController. + * Contains \Drupal\Core\Controler\HtmlFormController. */ namespace Drupal\Core\Controller; +use Drupal\Core\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Wrapping controller for forms that serve as the main page body. */ -class HtmlFormController implements ContainerAwareInterface { +class HtmlFormController extends FormController { /** * The injection container for this object. @@ -25,48 +24,19 @@ class HtmlFormController implements ContainerAwareInterface { protected $container; /** - * Injects the service container used by this object. + * The name of a class implementing FormInterface that defines a form. * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * The service container this object should use. + * @var string */ - public function setContainer(ContainerInterface $container = NULL) { - $this->container = $container; - } + protected $formClass; /** - * Controller method for generic HTML form pages. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - * @param callable $_form - * The name of the form class for this request. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. */ - public function content(Request $request, $_form) { - $form_object = $this->getFormObject($request, $_form); - - // Using reflection, find all of the parameters needed by the form in the - // request attributes, skipping $form and $form_state. - - // At the form and form_state to trick the getArguments method of the - // controller resolver. - $form_state = array(); - $request->attributes->set('form', array()); - $request->attributes->set('form_state', $form_state); - $args = $this->container->get('controller_resolver')->getArguments($request, array($form_object, 'buildForm')); - $request->attributes->remove('form'); - $request->attributes->remove('form_state'); - - // Remove $form and $form_state from the arguments, and re-index them. - unset($args[0], $args[1]); - $form_state['build_info']['args'] = array_values($args); - - $form_builder = $this->container->get('form_builder'); - $form_id = $form_builder->getFormId($form_object, $form_state); - return $form_builder->buildForm($form_id, $form_state); + public function __construct(ControllerResolverInterface $resolver, ContainerInterface $container, $class, FormBuilderInterface $form_builder) { + parent::__construct($resolver, $form_builder); + $this->container = $container; + $this->formDefinition = $class; } /** diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php index f3358adcd0..167293904c 100644 --- a/core/lib/Drupal/Core/Controller/HtmlPageController.php +++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php @@ -8,22 +8,12 @@ namespace Drupal\Core\Controller; use Drupal\Core\StringTranslation\TranslationInterface; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Default controller for most HTML pages. */ -class HtmlPageController { - - /** - * The HttpKernel object to use for subrequests. - * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface - */ - protected $httpKernel; +class HtmlPageController extends HtmlControllerBase { /** * The controller resolver. @@ -32,36 +22,20 @@ class HtmlPageController { */ protected $controllerResolver; - /** - * The translation manager service. - * - * @var \Drupal\Core\StringTranslation\TranslationInterface - */ - protected $translationManager; - - /** - * The title resolver. - * - * @var \Drupal\Core\Controller\TitleResolver - */ - protected $titleResolver; - /** * Constructs a new HtmlPageController. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver * The controller resolver. * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager * The translation manager. - * @param \Drupal\Core\Controller\TitleResolver $title_resolver + * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver * The title resolver. */ - public function __construct(HttpKernelInterface $kernel, ControllerResolverInterface $controller_resolver, TranslationInterface $translation_manager, TitleResolver $title_resolver) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolverInterface $controller_resolver, TranslationInterface $translation_manager, TitleResolverInterface $title_resolver) { + parent::__construct($translation_manager, $title_resolver); + $this->controllerResolver = $controller_resolver; - $this->translationManager = $translation_manager; - $this->titleResolver = $title_resolver; } /** @@ -76,38 +50,32 @@ public function __construct(HttpKernelInterface $kernel, ControllerResolverInter * A response object. */ public function content(Request $request, $_content) { - $callable = $this->controllerResolver->getControllerFromDefinition($_content); - $arguments = $this->controllerResolver->getArguments($request, $callable); - $page_content = call_user_func_array($callable, $arguments); - if ($page_content instanceof Response) { - return $page_content; - } - if (!is_array($page_content)) { - $page_content = array( - 'main' => array( - '#markup' => $page_content, - ), - ); - } - if (!isset($page_content['#title'])) { - $title = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); - // Ensure that #title will not be set if no title was returned. - if (isset($title)) { - $page_content['#title'] = $title; - } - } - - $response = new Response(drupal_render_page($page_content)); - return $response; + $page_content = $this->getContentResult($request, $_content); + return $this->createHtmlFragment($page_content, $request); } /** - * Translates a string to the current language or to a given language. + * Returns the result of invoking the sub-controller. * - * See the t() documentation for details. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param mixed $controller_definition + * A controller definition string, or a callable object/closure. + * + * @return array + * The render array that results from invoking the controller. */ - protected function t($string, array $args = array(), array $options = array()) { - return $this->translationManager->translate($string, $args, $options); + public function getContentResult(Request $request, $controller_definition) { + if ($controller_definition instanceof \Closure) { + $callable = $controller_definition; + } + else { + $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); + } + $arguments = $this->controllerResolver->getArguments($request, $callable); + $page_content = call_user_func_array($callable, $arguments); + + return $page_content; } } diff --git a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php index 2cf8d47417..0bbbef875a 100644 --- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php +++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php @@ -7,10 +7,13 @@ namespace Drupal\Core\Entity\Enhancer; +use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\HtmlEntityFormController; +use Drupal\Core\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Drupal\Core\ContentNegotiation; /** * Enhances an entity form route with the appropriate controller. @@ -18,38 +21,57 @@ class EntityRouteEnhancer implements RouteEnhancerInterface { /** - * Content negotiation library. + * The controller resolver. * - * @var \Drupal\Core\ContentNegotiation + * @var \Drupal\Core\Controller\ControllerResolverInterface */ - protected $negotiation; + protected $resolver; /** - * Constructs a new \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer. + * The entity manager service. * - * @param \Drupal\Core\ContentNegotiation $negotiation - * The content negotiation library. + * @var \Drupal\Core\Entity\EntityManagerInterface */ - public function __construct(ContentNegotiation $negotiation) { - $this->negotiation = $negotiation; + protected $manager; + + /** + * The form builder. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** + * Constructs a new EntityRouteEnhancer object. + * + * @param \Drupal\Core\Controller\ControllerResolverInterface $resolver + * The controller resolver. + * @param \Drupal\Core\Entity\EntityManagerInterface $manager + * The entity manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + */ + public function __construct(ControllerResolverInterface $resolver, EntityManagerInterface $manager, FormBuilderInterface $form_builder) { + $this->resolver = $resolver; + $this->manager = $manager; + $this->formBuilder = $form_builder; } /** * {@inheritdoc} */ public function enhance(array $defaults, Request $request) { - if (empty($defaults['_controller']) && $this->negotiation->getContentType($request) === 'html') { + if (empty($defaults['_content'])) { if (!empty($defaults['_entity_form'])) { - $defaults['_controller'] = '\Drupal\Core\Entity\HtmlEntityFormController::content'; + $wrapper = new HtmlEntityFormController($this->resolver, $this->manager, $this->formBuilder, $defaults['_entity_form']); + $defaults['_content'] = array($wrapper, 'getContentResult'); } elseif (!empty($defaults['_entity_list'])) { - $defaults['_controller'] = 'controller.page:content'; $defaults['_content'] = '\Drupal\Core\Entity\Controller\EntityListController::listing'; $defaults['entity_type'] = $defaults['_entity_list']; unset($defaults['_entity_list']); } elseif (!empty($defaults['_entity_view'])) { - $defaults['_controller'] = 'controller.page:content'; $defaults['_content'] = '\Drupal\Core\Entity\Controller\EntityViewController::view'; if (strpos($defaults['_entity_view'], '.') !== FALSE) { // The _entity_view entry is of the form entity_type.view_mode. diff --git a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php index 4aa79259c6..44a0bf3578 100644 --- a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php +++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php @@ -7,22 +7,39 @@ namespace Drupal\Core\Entity; -use Drupal\Core\Controller\HtmlFormController; +use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Controller\FormController; +use Drupal\Core\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Request; /** * Wrapping controller for entity forms that serve as the main page body. */ -class HtmlEntityFormController extends HtmlFormController { +class HtmlEntityFormController extends FormController { /** - * {@inheritdoc} + * The entity manager service. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $manager; + + /** + * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object. * - * Due to reflection, the argument must be named $_entity_form. The parent - * method has $request and $_form, but the parameter must match the route. + * @param \Drupal\Core\Controller\ControllerResolverInterface $resolver + * The controller resolver. + * @param \Drupal\Core\Entity\EntityManagerInterface $manager + * The entity manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + * @param string $form_definition + * The definition of this form, usually found in $defaults['_entity_form']. */ - public function content(Request $request, $_entity_form) { - return parent::content($request, $_entity_form); + public function __construct(ControllerResolverInterface $resolver, EntityManagerInterface $manager, FormBuilderInterface $form_builder, $form_definition) { + parent::__construct($resolver, $form_builder); + $this->manager = $manager; + $this->formDefinition = $form_definition; } /** @@ -46,8 +63,6 @@ public function content(Request $request, $_entity_form) { * @endcode */ protected function getFormObject(Request $request, $form_arg) { - $manager = $this->container->get('entity.manager'); - // If no operation is provided, use 'default'. $form_arg .= '.default'; list ($entity_type, $operation) = explode('.', $form_arg); @@ -56,10 +71,10 @@ protected function getFormObject(Request $request, $form_arg) { $entity = $request->attributes->get($entity_type); } else { - $entity = $manager->getStorageController($entity_type)->create(array()); + $entity = $this->manager->getStorageController($entity_type)->create(array()); } - return $manager->getFormController($entity_type, $operation)->setEntity($entity); + return $this->manager->getFormController($entity_type, $operation)->setEntity($entity); } } diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php new file mode 100644 index 0000000000..f66b6040f0 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php @@ -0,0 +1,85 @@ +renderer = $renderer; + } + + /** + * Converts an HtmlFragment into an HtmlPage. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The Event to process. + */ + public function onHtmlFragment(GetResponseForControllerResultEvent $event) { + $fragment = $event->getControllerResult(); + if ($fragment instanceof HtmlFragment && !$fragment instanceof HtmlPage) { + $page = $this->renderer->render($fragment); + $event->setControllerResult($page); + } + } + + /** + * Renders an HtmlPage object to a Response. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event + * The Event to process. + */ + public function onHtmlPage(GetResponseForControllerResultEvent $event) { + $page = $event->getControllerResult(); + if ($page instanceof HtmlPage) { + // In case renderPage() returns NULL due to an error cast it to a string + // so as to not cause issues with Response. This also allows renderPage + // to return an object implementing __toString(), but that is not + // recommended. + $response = new Response((string) $this->renderer->renderPage($page), $page->getStatusCode()); + $event->setResponse($response); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::VIEW][] = array('onHtmlFragment', 100); + $events[KernelEvents::VIEW][] = array('onHtmlPage', 50); + + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php index e98670042d..6fb011d5a3 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php @@ -8,6 +8,7 @@ namespace Drupal\Core\EventSubscriber; 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; @@ -85,28 +86,14 @@ public function onView(GetResponseForControllerResultEvent $event) { $event->setResponse(new Response('Not Acceptable', 406)); } } - elseif ($request->attributes->get('_legacy')) { - // This is an old hook_menu-based subrequest, which means we assume - // the body is supposed to be the complete page. - $page_result = $event->getControllerResult(); - if (!is_array($page_result)) { - $page_result = array( - '#markup' => $page_result, - ); - } - - // If no title was returned fall back to one defined in the route. - if (!isset($page_result['#title'])) { - $page_result['#title'] = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); - } - - $event->setResponse(new Response(drupal_render_page($page_result))); - } else { // This is a new-style Symfony-esque subrequest, which means we assume // the body is not supposed to be a complete page but just a page // fragment. $page_result = $event->getControllerResult(); + if ($page_result instanceof HtmlPage || $page_result instanceof Response) { + return $page_result; + } if (!is_array($page_result)) { $page_result = array( '#markup' => $page_result, diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php new file mode 100644 index 0000000000..6543fbc990 --- /dev/null +++ b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php @@ -0,0 +1,101 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function render(HtmlFragment $fragment, $status_code = 200) { + $page = new HtmlPage('', $fragment->getTitle()); + + $page_content['main'] = array( + '#markup' => $fragment->getContent(), + ); + $page_content['#title'] = $page->getTitle(); + + $page_array = drupal_prepare_page($page_content); + + $page = $this->preparePage($page, $page_array); + + $page->setBodyTop(drupal_render($page_array['page_top'])); + $page->setBodyBottom(drupal_render($page_array['page_bottom'])); + $page->setContent(drupal_render($page_array)); + + $page->setStatusCode($status_code); + + return $page; + } + + /** + * {@inheritdoc} + */ + public function renderPage(HtmlPage $page) { + $render = array( + '#theme' => 'html', + '#page_object' => $page, + ); + return drupal_render($render); + } + + /** + * Enhances a page object based on a render array. + * + * @param \Drupal\Core\Page\HtmlPage $page + * The page object to enhance. + * @param array $page_array + * The page array to extract onto the page object. + * + * @return \Drupal\Core\Page\HtmlPage + * The modified page object. + */ + public function preparePage(HtmlPage $page, &$page_array) { + // @todo Remove this one drupal_get_title() has been eliminated. + if (!$page->hasTitle()) { + $title = drupal_get_title(); + // drupal_set_title() already ensured security, so not letting the + // title pass through would cause double escaping. + $page->setTitle($title, PASS_THROUGH); + } + + $page_array['#page'] = $page; + + // HTML element attributes. + $language_interface = $this->languageManager->getLanguage(Language::TYPE_INTERFACE); + $html_attributes = $page->getHtmlAttributes(); + $html_attributes['lang'] = $language_interface->id; + $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; + + return $page; + } + +} diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php new file mode 100644 index 0000000000..2c929656ed --- /dev/null +++ b/core/lib/Drupal/Core/Page/HeadElement.php @@ -0,0 +1,89 @@ +. + * + * @var bool + */ + protected $noScript = FALSE; + + /** + * Renders this object to an HTML element string. + * + * @return string + */ + public function __toString() { + // Render the attributes via the attribute template class. + // @todo Should HeadElement just extend the Attribute classes? + $attributes = new Attribute($this->attributes); + $rendered = (string) $attributes; + + $string = "<{$this->element}{$rendered} />"; + if ($this->noScript) { + $string = ""; + } + return $string; + } + + /** + * Sets an attribute on this element. + * + * @param mixed $key + * The attribute to set. + * @param mixed $value + * The value to which to set it. + * + * @return self + * The invoked object. + */ + public function setAttribute($key, $value) { + $this->attributes[$key] = $value; + return $this; + } + + /** + * Sets if this element should be wrapped in