diff --git a/core/authorize.php b/core/authorize.php index 35277f6b379822c21e9ddaa767a2b21607c5d7a4..d9f1e56a8d851c33d3fbf5f9c3376345d9f58a71 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -85,8 +85,10 @@ function authorize_access_allowed(Request $request) { $content = []; $show_messages = TRUE; -$response = new Response(); -if (authorize_access_allowed($request)) { +$is_allowed = authorize_access_allowed($request); + +// Build content. +if ($is_allowed) { // Load both the Form API and Batch API. require_once __DIR__ . '/includes/form.inc'; require_once __DIR__ . '/includes/batch.inc'; @@ -152,16 +154,16 @@ function authorize_access_allowed(Request $request) { $show_messages = !(($batch = batch_get()) && isset($batch['running'])); } else { - $response->setStatusCode(403); \Drupal::logger('access denied')->warning('authorize.php'); $page_title = t('Access denied'); $content = ['#markup' => t('You are not allowed to access this page.')]; } -if (!empty($content)) { - $response->headers->set('Content-Type', 'text/html; charset=utf-8'); - $response->setContent(\Drupal::service('bare_html_page_renderer')->renderBarePage($content, $page_title, 'maintenance_page', array( - '#show_messages' => $show_messages, - ))); - $response->send(); +$bare_html_page_renderer = \Drupal::service('bare_html_page_renderer'); +$response = $bare_html_page_renderer->renderBarePage($content, $page_title, 'maintenance_page', array( + '#show_messages' => $show_messages, +)); +if (!$is_allowed) { + $response->setStatusCode(403); } +$response->send(); diff --git a/core/core.services.yml b/core/core.services.yml index 6c2c78ff9e9045653806b55f14d900ab2ce386ce..d8a8d242291df05be9329a41f4df719035d4b644 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -814,8 +814,9 @@ services: tags: - { name: event_subscriber } arguments: ['@resolver_manager.entity'] - ajax_subscriber: - class: Drupal\Core\EventSubscriber\AjaxSubscriber + ajax_response.subscriber: + class: Drupal\Core\EventSubscriber\AjaxResponseSubscriber + arguments: ['@ajax_response.attachments_processor'] tags: - { name: event_subscriber } form_ajax_subscriber: @@ -903,7 +904,7 @@ services: arguments: ['@router', '@router.request_context', NULL, '@request_stack'] bare_html_page_renderer: class: Drupal\Core\Render\BareHtmlPageRenderer - arguments: ['@renderer'] + arguments: ['@renderer', '@html_response.attachments_processor'] lazy: true private_key: class: Drupal\Core\PrivateKey @@ -975,6 +976,19 @@ services: tags: - { name: event_subscriber } arguments: ['@current_user'] + ajax_response.attachments_processor: + class: Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor + tags: + arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler'] + html_response.attachments_processor: + class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor + tags: + arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer'] + html_response.subscriber: + class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber + tags: + - { name: event_subscriber } + arguments: ['@html_response.attachments_processor'] finish_response_subscriber: class: Drupal\Core\EventSubscriber\FinishResponseSubscriber tags: diff --git a/core/includes/batch.inc b/core/includes/batch.inc index 9281837b6fbc77839e89d11bca55ff0df6bda05b..4e474715981ef187a9171cfef285ea0b9709ebbb 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -137,9 +137,14 @@ 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 = \Drupal::service('bare_html_page_renderer')->renderBarePage(['#markup' => $fallback], $current_set['title'], 'maintenance_page', array( + $bare_html_page_renderer = \Drupal::service('bare_html_page_renderer'); + $response = $bare_html_page_renderer->renderBarePage(['#markup' => $fallback], $current_set['title'], 'maintenance_page', array( '#show_messages' => FALSE, )); + + // Just use the content of the response. + $fallback = $response->getContent(); + list($fallback) = explode('', $fallback); print $fallback; diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 6933dc8690c9e2777f7f645d106c3fc5631d5f0e..06753fd0dd650daadb0037b025177b4c1b558c46 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -9,7 +9,6 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Utility\Error; -use Symfony\Component\HttpFoundation\Response; /** * Maps PHP error constants to watchdog severity levels. @@ -241,12 +240,11 @@ function _drupal_log_error($error, $fatal = FALSE) { '#markup' => $message, ); install_display_output($output, $GLOBALS['install_state']); - } - else { - $output = \Drupal::service('bare_html_page_renderer')->renderBarePage(['#markup' => $message], 'Error', 'maintenance_page'); + exit; } - $response = new Response($output, 500); + $bare_html_page_renderer = \Drupal::service('bare_html_page_renderer'); + $response = $bare_html_page_renderer->renderBarePage(['#markup' => $message], 'Error', 'maintenance_page'); $response->setStatusCode(500, '500 Service unavailable (with message)'); // An exception must halt script execution. $response->send(); diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 402d41e9372f4dc9c011577c3ee718d2f82a7973..34bfcb0179fd2c9c5c680c3c588369302406b17a 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -984,7 +984,8 @@ function install_display_output($output, $install_state) { $regions['sidebar_first'] = $task_list; } - $response = new Response(); + $bare_html_page_renderer = \Drupal::service('bare_html_page_renderer'); + $response = $bare_html_page_renderer->renderBarePage($output, $output['#title'], 'install_page', $regions); $default_headers = array( 'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT', 'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME), @@ -992,7 +993,6 @@ function install_display_output($output, $install_state) { 'ETag' => '"' . REQUEST_TIME . '"', ); $response->headers->add($default_headers); - $response->setContent(\Drupal::service('bare_html_page_renderer')->renderBarePage($output, $output['#title'], 'install_page', $regions)); $response->send(); exit; } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 5d645e2350439f21e37ddf3cc798971aa445b9b5..74000172c5d242cde26dd4f220477aeb759edb2b 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -9,21 +9,17 @@ */ use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Unicode; -use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; -use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\Config\Config; use Drupal\Core\Config\StorageException; -use Drupal\Core\Extension\Extension; -use Drupal\Core\Extension\ExtensionNameLengthException; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Render\Element; -use Symfony\Component\HttpFoundation\Request; /** * @defgroup content_flags Content markers @@ -1308,41 +1304,23 @@ function template_preprocess_html(&$variables) { // @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. $variables['head_title_array'] = $head_title; - // 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['page']['#attached']); - if (isset($variables['page_top'])) { - $attached = drupal_merge_attached($attached, $variables['page_top']['#attached']); + // Create placeholder strings for these keys. + // @see \Drupal\Core\Render\HtmlResponseSubscriber + $types = [ + 'styles', + 'scripts', + 'scripts_bottom', + 'head', + ]; + $token = Crypt::randomBytesBase64(55); + foreach ($types as $type) { + $placeholder = SafeMarkup::format('', [ + '@type' => $type, + '@token' => $token, + ]); + $variables[$type]['#markup'] = $placeholder; + $variables[$type]['#attached']['html_response_placeholders'][$type] = $placeholder; } - if (isset($variables['page_bottom'])) { - $attached = drupal_merge_attached($attached, $variables['page_bottom']['#attached']); - } - - // Render the attachments into HTML markup to be used directly in the template - // for #type => html: html.html.twig. - $all_attached = ['#attached' => $attached]; - $assets = AttachedAssets::createFromRenderArray($all_attached); - // Take Ajax page state into account, to allow for something like Turbolinks - // to be implemented without altering core. - // @see https://github.com/rails/turbolinks/ - $ajax_page_state = \Drupal::request()->request->get('ajax_page_state'); - $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); - // Optimize CSS/JS if necessary, but only during normal site operation. - $optimize_css = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess'); - $optimize_js = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess'); - // Render the asset collections. - $asset_resolver = \Drupal::service('asset.resolver'); - $variables['styles'] = \Drupal::service('asset.css.collection_renderer')->render($asset_resolver->getCssAssets($assets, $optimize_css)); - list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, $optimize_js); - $js_collection_renderer = \Drupal::service('asset.js.collection_renderer'); - $variables['scripts'] = $js_collection_renderer->render($js_assets_header); - $variables['scripts_bottom'] = $js_collection_renderer->render($js_assets_footer); - - // Handle all non-asset attachments. - drupal_process_attached($all_attached); - $variables['head'] = drupal_get_html_head(FALSE); } /** diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 1acf03afc2d6c22f60e176dfd3964be8872f3178..a2218e664f0d91f7d35aa859d596370c76077815 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -10,6 +10,8 @@ use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Renderer; +use Drupal\Core\Render\AttachmentsInterface; +use Drupal\Core\Render\AttachmentsTrait; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -20,7 +22,9 @@ * * @ingroup ajax */ -class AjaxResponse extends JsonResponse { +class AjaxResponse extends JsonResponse implements AttachmentsInterface { + + use AttachmentsTrait; /** * The array of ajax commands. @@ -29,32 +33,6 @@ class AjaxResponse extends JsonResponse { */ protected $commands = array(); - /** - * The attachments for this Ajax response. - * - * @var array - */ - protected $attachments = [ - 'library' => [], - 'drupalSettings' => [], - ]; - - /** - * Sets attachments for this Ajax response. - * - * When this Ajax response is rendered, it will take care of generating the - * necessary Ajax commands, if any. - * - * @param array $attachments - * An #attached array. - * - * @return $this - */ - public function setAttachments(array $attachments) { - $this->attachments = $attachments; - return $this; - } - /** * Add an AJAX command to the response. * @@ -80,7 +58,7 @@ public function addCommand(CommandInterface $command, $prepend = FALSE) { 'library' => $assets->getLibraries(), 'drupalSettings' => $assets->getSettings(), ]; - $attachments = BubbleableMetadata::mergeAttachments($this->attachments, $attachments); + $attachments = BubbleableMetadata::mergeAttachments($this->getAttachments(), $attachments); $this->setAttachments($attachments); } @@ -97,147 +75,4 @@ public function &getCommands() { return $this->commands; } - /** - * {@inheritdoc} - * - * Sets the response's data to be the array of AJAX commands. - */ - public function prepare(Request $request) { - $this->prepareResponse($request); - return $this; - } - - /** - * Sets the rendered AJAX right before the response is prepared. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - */ - public function prepareResponse(Request $request) { - if ($this->data == '{}') { - $this->setData($this->ajaxRender($request)); - } - - // IE 9 does not support XHR 2 (http://caniuse.com/#feat=xhr2), so - // for that browser, jquery.form submits requests containing a file upload - // via an IFRAME rather than via XHR. Since the response is being sent to - // an IFRAME, it must be formatted as HTML. Specifically: - // - It must use the text/html content type or else the browser will - // present a download prompt. Note: This applies to both file uploads - // as well as any ajax request in a form with a file upload form. - // - It must place the JSON data into a textarea to prevent browser - // extensions such as Linkification and Skype's Browser Highlighter - // from applying HTML transformations such as URL or phone number to - // link conversions on the data values. - // - // Since this affects the format of the output, it could be argued that - // this should be implemented as a separate Accept MIME type. However, - // that would require separate variants for each type of AJAX request - // (e.g., drupal-ajax, drupal-dialog, drupal-modal), so for expediency, - // this browser workaround is implemented via a GET or POST parameter. - // - // @see http://malsup.com/jquery/form/#file-upload - // @see https://www.drupal.org/node/1009382 - // @see https://www.drupal.org/node/2339491 - // @see Drupal.ajax.prototype.beforeSend() - $accept = $request->headers->get('accept'); - - if (strpos($accept, 'text/html') !== FALSE) { - $this->headers->set('Content-Type', 'text/html; charset=utf-8'); - - // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification - // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into - // links. This corrupts the JSON response. Protect the integrity of the - // JSON data by making it the value of a textarea. - // @see http://malsup.com/jquery/form/#file-upload - // @see https://www.drupal.org/node/1009382 - $this->setContent(''); - } - } - - /** - * Prepares the AJAX commands for sending back to the client. - * - * @param Request $request - * The request object that the AJAX is responding to. - * - * @return array - * An array of commands ready to be returned as JSON. - */ - protected function ajaxRender(Request $request) { - $ajax_page_state = $request->request->get('ajax_page_state'); - - // Aggregate CSS/JS if necessary, but only during normal site operation. - $config = \Drupal::config('system.performance'); - $optimize_css = !defined('MAINTENANCE_MODE') && $config->get('css.preprocess'); - $optimize_js = !defined('MAINTENANCE_MODE') && $config->get('js.preprocess'); - - // Resolve the attached libraries into asset collections. - $assets = new AttachedAssets(); - $assets->setLibraries(isset($this->attachments['library']) ? $this->attachments['library'] : []) - ->setAlreadyLoadedLibraries(isset($ajax_page_state['libraries']) ? explode(',', $ajax_page_state['libraries']) : []) - ->setSettings(isset($this->attachments['drupalSettings']) ? $this->attachments['drupalSettings'] : []); - $asset_resolver = \Drupal::service('asset.resolver'); - $css_assets = $asset_resolver->getCssAssets($assets, $optimize_css); - list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, $optimize_js); - - // Render the HTML to load these files, and add AJAX commands to insert this - // HTML in the page. Settings are handled separately, afterwards. - $settings = []; - if (isset($js_assets_header['drupalSettings'])) { - $settings = $js_assets_header['drupalSettings']['data']; - unset($js_assets_header['drupalSettings']); - } - if (isset($js_assets_footer['drupalSettings'])) { - $settings = $js_assets_footer['drupalSettings']['data']; - unset($js_assets_footer['drupalSettings']); - } - - // Prepend commands to add the assets, preserving their relative order. - $resource_commands = array(); - $renderer = $this->getRenderer(); - if (!empty($css_assets)) { - $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css_assets); - $resource_commands[] = new AddCssCommand($renderer->render($css_render_array)); - } - if (!empty($js_assets_header)) { - $js_header_render_array = \Drupal::service('asset.js.collection_renderer')->render($js_assets_header); - $resource_commands[] = new PrependCommand('head', $renderer->render($js_header_render_array)); - } - if (!empty($js_assets_footer)) { - $js_footer_render_array = \Drupal::service('asset.js.collection_renderer')->render($js_assets_footer); - $resource_commands[] = new AppendCommand('body', $renderer->render($js_footer_render_array)); - } - foreach (array_reverse($resource_commands) as $resource_command) { - $this->addCommand($resource_command, TRUE); - } - - // Prepend a command to merge changes and additions to drupalSettings. - if (!empty($settings)) { - // During Ajax requests basic path-specific settings are excluded from - // new drupalSettings values. The original page where this request comes - // from already has the right values. An Ajax request would update them - // with values for the Ajax request and incorrectly override the page's - // values. - // @see system_js_settings_alter() - unset($settings['path']); - $this->addCommand(new SettingsCommand($settings, TRUE), TRUE); - } - - $commands = $this->commands; - \Drupal::moduleHandler()->alter('ajax_render', $commands); - - return $commands; - } - - /** - * The renderer service. - * - * @return \Drupal\Core\Render\Renderer - * The renderer service. - */ - protected function getRenderer() { - return \Drupal::service('renderer'); - } - } diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..a689b76ef94d799a4438d6c8c34c13e58a673144 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -0,0 +1,199 @@ +assetResolver = $asset_resolver; + $this->config = $config_factory->get('system.performance'); + $this->cssCollectionRenderer = $css_collection_renderer; + $this->jsCollectionRenderer = $js_collection_renderer; + $this->requestStack = $request_stack; + $this->renderer = $renderer; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function processAttachments(AttachmentsInterface $response) { + // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands + if (!$response instanceof AjaxResponse) { + throw new \InvalidArgumentException('\Drupal\Core\Ajax\AjaxResponse instance expected.'); + } + + $request = $this->requestStack->getCurrentRequest(); + + if ($response->getContent() == '{}') { + $response->setData($this->buildAttachmentsCommands($response, $request)); + } + + return $response; + } + + /** + * Prepares the AJAX commands to attach assets. + * + * @param \Drupal\Core\Ajax\AjaxResponse $response + * The AJAX response to update. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object that the AJAX is responding to. + * + * @return array + * An array of commands ready to be returned as JSON. + */ + protected function buildAttachmentsCommands(AjaxResponse $response, Request $request) { + $ajax_page_state = $request->request->get('ajax_page_state'); + + // Aggregate CSS/JS if necessary, but only during normal site operation. + $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess'); + $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess'); + + $attachments = $response->getAttachments(); + + // Resolve the attached libraries into asset collections. + $assets = new AttachedAssets(); + $assets->setLibraries(isset($attachments['library']) ? $attachments['library'] : []) + ->setAlreadyLoadedLibraries(isset($ajax_page_state['libraries']) ? explode(',', $ajax_page_state['libraries']) : []) + ->setSettings(isset($attachments['drupalSettings']) ? $attachments['drupalSettings'] : []); + $css_assets = $this->assetResolver->getCssAssets($assets, $optimize_css); + list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js); + + // Render the HTML to load these files, and add AJAX commands to insert this + // HTML in the page. Settings are handled separately, afterwards. + $settings = []; + if (isset($js_assets_header['drupalSettings'])) { + $settings = $js_assets_header['drupalSettings']['data']; + unset($js_assets_header['drupalSettings']); + } + if (isset($js_assets_footer['drupalSettings'])) { + $settings = $js_assets_footer['drupalSettings']['data']; + unset($js_assets_footer['drupalSettings']); + } + + // Prepend commands to add the assets, preserving their relative order. + $resource_commands = array(); + if ($css_assets) { + $css_render_array = $this->cssCollectionRenderer->render($css_assets); + $resource_commands[] = new AddCssCommand($this->renderer->render($css_render_array)); + } + if ($js_assets_header) { + $js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header); + $resource_commands[] = new PrependCommand('head', $this->renderer->render($js_header_render_array)); + } + if ($js_assets_footer) { + $js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer); + $resource_commands[] = new AppendCommand('body', $this->renderer->render($js_footer_render_array)); + } + foreach (array_reverse($resource_commands) as $resource_command) { + $response->addCommand($resource_command, TRUE); + } + + // Prepend a command to merge changes and additions to drupalSettings. + if (!empty($settings)) { + // During Ajax requests basic path-specific settings are excluded from + // new drupalSettings values. The original page where this request comes + // from already has the right values. An Ajax request would update them + // with values for the Ajax request and incorrectly override the page's + // values. + // @see system_js_settings_alter() + unset($settings['path']); + $response->addCommand(new SettingsCommand($settings, TRUE), TRUE); + } + + $commands = $response->getCommands(); + $this->moduleHandler->alter('ajax_render', $commands); + + return $commands; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..f35b8526a53eee080afb5e21d5aa2dd5305e064e --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php @@ -0,0 +1,117 @@ +ajaxResponseAttachmentsProcessor = $ajax_response_attachments_processor; + } + + /** + * Request parameter to indicate that a request is a Drupal Ajax request. + */ + const AJAX_REQUEST_PARAMETER = '_drupal_ajax'; + + /** + * Sets the AJAX parameter from the current request. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The response event, which contains the current request. + */ + public function onRequest(GetResponseEvent $event) { + // Pass to the Html class that the current request is an Ajax request. + if ($event->getRequest()->request->get(static::AJAX_REQUEST_PARAMETER)) { + Html::setIsAjax(TRUE); + } + } + + /** + * Renders the ajax commands right before preparing the result. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The response event, which contains the possible AjaxResponse object. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + if ($response instanceof AjaxResponse) { + $this->ajaxResponseAttachmentsProcessor->processAttachments($response); + + // IE 9 does not support XHR 2 (http://caniuse.com/#feat=xhr2), so + // for that browser, jquery.form submits requests containing a file upload + // via an IFRAME rather than via XHR. Since the response is being sent to + // an IFRAME, it must be formatted as HTML. Specifically: + // - It must use the text/html content type or else the browser will + // present a download prompt. Note: This applies to both file uploads + // as well as any ajax request in a form with a file upload form. + // - It must place the JSON data into a textarea to prevent browser + // extensions such as Linkification and Skype's Browser Highlighter + // from applying HTML transformations such as URL or phone number to + // link conversions on the data values. + // + // Since this affects the format of the output, it could be argued that + // this should be implemented as a separate Accept MIME type. However, + // that would require separate variants for each type of AJAX request + // (e.g., drupal-ajax, drupal-dialog, drupal-modal), so for expediency, + // this browser workaround is implemented via a GET or POST parameter. + // + // @see http://malsup.com/jquery/form/#file-upload + // @see https://www.drupal.org/node/1009382 + // @see https://www.drupal.org/node/2339491 + // @see Drupal.ajax.prototype.beforeSend() + $accept = $event->getRequest()->headers->get('accept'); + + if (strpos($accept, 'text/html') !== FALSE) { + $response->headers->set('Content-Type', 'text/html; charset=utf-8'); + + // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification + // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. + // into links. This corrupts the JSON response. Protect the integrity of + // the JSON data by making it the value of a textarea. + // @see http://malsup.com/jquery/form/#file-upload + // @see https://www.drupal.org/node/1009382 + $response->setContent(''); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = array('onResponse', -100); + $events[KernelEvents::REQUEST][] = array('onRequest', 50); + + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php deleted file mode 100644 index 8a856590408f69f514120e7363ad40a11f7082ec..0000000000000000000000000000000000000000 --- a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php +++ /dev/null @@ -1,63 +0,0 @@ -getRequest()->request->get(static::AJAX_REQUEST_PARAMETER)) { - Html::setIsAjax(TRUE); - } - } - - /** - * Renders the ajax commands right before preparing the result. - * - * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event - * The response event, which contains the possible AjaxResponse object. - */ - public function onResponse(FilterResponseEvent $event) { - $response = $event->getResponse(); - if ($response instanceof AjaxResponse) { - $response->prepareResponse($event->getRequest()); - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - $events[KernelEvents::RESPONSE][] = array('onResponse', -100); - $events[KernelEvents::REQUEST][] = array('onRequest', 50); - - return $events; - } - -} diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php index 46d6c4605c550bad4a238516429703e66dd55ee6..5f9d4aca8d84a37a35f06b509cbf12d810c47aab 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php @@ -129,8 +129,7 @@ protected function onHtml(GetResponseForExceptionEvent $event) { } $content = $this->t('The website encountered an unexpected error. Please try again later.'); - $output = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Error'), 'maintenance_page'); - $response = new Response($output); + $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Error'), 'maintenance_page'); if ($exception instanceof HttpExceptionInterface) { $response->setStatusCode($exception->getStatusCode()); diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..6fada86d857f7f0c6dc0360e5390a9493c817072 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php @@ -0,0 +1,65 @@ +htmlResponseAttachmentsProcessor = $html_response_attachments_processor; + } + + /** + * Processes attachments for HtmlResponse responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $response = $event->getResponse(); + if (!$response instanceof HtmlResponse) { + return; + } + + $event->setResponse($this->htmlResponseAttachmentsProcessor->processAttachments($response)); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = ['onRespond']; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php index e43d00e57cd7a5f32c18e314521ea268e72537ae..92071da46df6cf5a40601adc373cecf4d6413ecd 100644 --- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php @@ -18,7 +18,6 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -107,8 +106,8 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { $content = Xss::filterAdmin(SafeMarkup::format($this->config->get('system.maintenance')->get('message'), array( '@site' => $this->config->get('system.site')->get('name'), ))); - $output = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Site under maintenance'), 'maintenance_page'); - $response = new Response($output, 503); + $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Site under maintenance'), 'maintenance_page'); + $response->setStatusCode(503); $event->setResponse($response); } else { diff --git a/core/lib/Drupal/Core/Render/AttachmentsInterface.php b/core/lib/Drupal/Core/Render/AttachmentsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..4d9b97c666e476de51049307c15afd3821936fbe --- /dev/null +++ b/core/lib/Drupal/Core/Render/AttachmentsInterface.php @@ -0,0 +1,49 @@ +attachments; + } + + /** + * {@inheritdoc} + */ + public function addAttachments(array $attachments) { + $this->attachments = BubbleableMetadata::mergeAttachments($this->attachments, $attachments); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setAttachments(array $attachments) { + $this->attachments = $attachments; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php index c8012c6c8a9a23fc6d6cf93a324cb7a426d31c54..a7164d9f0b11496eb9251588c96641427c637e6b 100644 --- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php @@ -19,14 +19,24 @@ class BareHtmlPageRenderer implements BareHtmlPageRendererInterface { */ protected $renderer; + /** + * The HTML response attachments processor service. + * + * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface + */ + protected $htmlResponseAttachmentsProcessor; + /** * Constructs a new BareHtmlPageRenderer. * * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer service. + * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor + * The HTML response attachments processor service. */ - public function __construct(RendererInterface $renderer) { + public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $html_response_attachments_processor) { $this->renderer = $renderer; + $this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor; } /** @@ -55,16 +65,17 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr $html['page']['highlighted'] = ['#type' => 'status_messages']; } - // We must first render the contents of the html.html.twig template, see - // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() for more - // information about this; the exact same pattern is used there and - // explained in detail there. - $this->renderer->render($html['page'], TRUE); - // Add the bare minimum of attachments from the system module and the // current maintenance theme. system_page_attachments($html['page']); - return $this->renderer->render($html); + $this->renderer->renderRoot($html); + + $response = new HtmlResponse(); + $response->setContent($html); + // Process attachments, because this does not go via the regular render + // pipeline, but will be sent directly. + $response = $this->htmlResponseAttachmentsProcessor->processAttachments($response); + return $response; } } diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php index 1b4a5f538558ebdb6bce16e9c822cb27b76abaea..9e1f8ee4336e2e38255be08b189dfc2a292a4033 100644 --- a/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php @@ -59,8 +59,8 @@ interface BareHtmlPageRendererInterface { * Additional regions to add to the page. May also be used to pass the * #show_messages property for #type 'page'. * - * @return string - * The rendered HTML page. + * @return \Drupal\Core\Render\HtmlResponse + * The rendered HTML response, ready to be sent. */ public function renderBarePage(array $content, $title, $page_theme_property, array $page_additions = []); diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php index 8e6875f51039bf7c1b5f22b84818c2c01a6d0637..5cb350e965a974d700e0de407e6f78e2780ca5f9 100644 --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -15,14 +15,9 @@ * * @see \Drupal\Core\Render\RendererInterface::render() */ -class BubbleableMetadata extends CacheableMetadata { +class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterface { - /** - * Attached assets. - * - * @var string[][] - */ - protected $attached = []; + use AttachmentsTrait; /** * Merges the values of another bubbleable metadata object with this one. @@ -39,14 +34,14 @@ public function merge(CacheableMetadata $other) { // This is called many times per request, so avoid merging unless absolutely // necessary. if ($other instanceof BubbleableMetadata) { - if (empty($this->attached)) { - $result->attached = $other->attached; + if (empty($this->attachments)) { + $result->attachments = $other->attachments; } - elseif (empty($other->attached)) { - $result->attached = $this->attached; + elseif (empty($other->attachments)) { + $result->attachments = $this->attachments; } else { - $result->attached = static::mergeAttachments($this->attached, $other->attached); + $result->attachments = static::mergeAttachments($this->attachments, $other->attachments); } } @@ -61,7 +56,7 @@ public function merge(CacheableMetadata $other) { */ public function applyTo(array &$build) { parent::applyTo($build); - $build['#attached'] = $this->attached; + $build['#attached'] = $this->attachments; } /** @@ -74,49 +69,10 @@ public function applyTo(array &$build) { */ public static function createFromRenderArray(array $build) { $meta = parent::createFromRenderArray($build); - $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; + $meta->attachments = (isset($build['#attached'])) ? $build['#attached'] : []; return $meta; } - /** - * Gets attachments. - * - * @return array - * The attachments - */ - public function getAttachments() { - return $this->attached; - } - - /** - * Adds attachments. - * - * @param array $attachments - * The attachments to add. - * - * @return $this - */ - public function addAttachments(array $attachments) { - $other = (new BubbleableMetadata())->setAttachments($attachments); - $result = $other->merge($this); - - $this->attached = $result->getAttachments(); - return $this; - } - - /** - * Sets attachments. - * - * @param array $attachments - * The attachments to set. - * - * @return $this - */ - public function setAttachments(array $attachments) { - $this->attached = $attachments; - return $this; - } - /** * Gets assets. * @@ -125,7 +81,7 @@ public function setAttachments(array $attachments) { * @deprecated Use ::getAttachments() instead. To be removed before Drupal 8.0.0. */ public function getAssets() { - return $this->attached; + return $this->attachments; } /** @@ -153,7 +109,7 @@ public function addAssets(array $assets) { * @deprecated Use ::setAttachments() instead. To be removed before Drupal 8.0.0. */ public function setAssets(array $assets) { - $this->attached = $assets; + $this->attachments = $assets; return $this; } diff --git a/core/lib/Drupal/Core/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..7053f83e5293a59b56290d007885763bac8f7483 --- /dev/null +++ b/core/lib/Drupal/Core/Render/HtmlResponse.php @@ -0,0 +1,46 @@ +addCacheableDependency(CacheableMetadata::createFromRenderArray($content)); + $this->setAttachments($content['#attached']); + $content = $content['#markup']; + } + + parent::setContent($content); + } +} diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..50ff584a077a152fe9c90f6ea03cfa9845881daa --- /dev/null +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -0,0 +1,218 @@ +assetResolver = $asset_resolver; + $this->config = $config_factory->get('system.performance'); + $this->cssCollectionRenderer = $css_collection_renderer; + $this->jsCollectionRenderer = $js_collection_renderer; + $this->requestStack = $request_stack; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public function processAttachments(AttachmentsInterface $response) { + // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands + if (!$response instanceof HtmlResponse) { + throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.'); + } + + $attached = $response->getAttachments(); + + // Get the placeholders from attached and then remove them. + $placeholders = $attached['html_response_placeholders']; + unset($attached['html_response_placeholders']); + + $variables = $this->processAssetLibraries($attached, $placeholders); + + // Handle all non-asset attachments. This populates drupal_get_html_head() + // and drupal_get_http_header(). + $all_attached = ['#attached' => $attached]; + drupal_process_attached($all_attached); + + // Get HTML head elements - if present. + if (isset($placeholders['head'])) { + $variables['head'] = drupal_get_html_head(FALSE); + } + + // Now replace the placeholders in the response content with the real data. + $this->renderPlaceholders($response, $placeholders, $variables); + + // Finally set the headers on the response. + $headers = drupal_get_http_header(); + $this->setHeaders($response, $headers); + + return $response; + } + + /** + * Processes asset libraries into render arrays. + * + * @param array $attached + * The attachments to process. + * @param array $placeholders + * The placeholders that exist in the response. + * + * @return array + * An array keyed by asset type, with keys: + * - styles + * - scripts + * - scripts_bottom + */ + protected function processAssetLibraries(array $attached, array $placeholders) { + $all_attached = ['#attached' => $attached]; + $assets = AttachedAssets::createFromRenderArray($all_attached); + + // Take Ajax page state into account, to allow for something like Turbolinks + // to be implemented without altering core. + // @see https://github.com/rails/turbolinks/ + // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request. + $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); + $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); + + $variables = []; + + // Print styles - if present. + if (isset($placeholders['styles'])) { + // Optimize CSS if necessary, but only during normal site operation. + $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess'); + $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css)); + } + + // Print scripts - if any are present. + if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) { + // Optimize JS if necessary, but only during normal site operation. + $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess'); + list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js); + $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header); + $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer); + } + + return $variables; + } + + /** + * Renders variables into HTML markup and replaces placeholders in the + * response content. + * + * @param \Drupal\Core\Render\HtmlResponse $response + * The HTML response to update. + * @param array $placeholders + * An array of placeholders, keyed by type with the placeholders + * present in the content of the response as values. + * @param array $variables + * The variables to render and replace, keyed by type with renderable + * arrays as values. + */ + protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { + $content = $response->getContent(); + foreach ($placeholders as $type => $placeholder) { + if (isset($variables[$type])) { + $content = str_replace($placeholder, $this->renderer->renderPlain($variables[$type]), $content); + } + } + $response->setContent($content); + } + + /** + * Sets headers on a response object. + * + * @param \Drupal\Core\Render\HtmlResponse $response + * The HTML response to update. + * @param array $headers + * The headers to set. + */ + protected function setHeaders(HtmlResponse $response, array $headers) { + foreach ($headers as $name => $value) { + // Drupal treats the HTTP response status code like a header, even though + // it really is not. + if ($name === 'status') { + $response->setStatusCode($value); + } + $response->headers->set($name, $value, FALSE); + } + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index d61bb24d811bc92225fdf2be154e4fee0ba0ef63..cb987f47d0b29c0014935b0ab8632b459b5c69aa 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -8,22 +8,27 @@ namespace Drupal\Core\Render\MainContent; use Drupal\Component\Plugin\PluginManagerInterface; -use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Display\PageVariantInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; use Drupal\Core\Routing\RouteMatchInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; /** * Default main content renderer for HTML requests. + * + * For attachment handling of HTML responses: + * @see template_preprocess_html() + * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface + * @see \Drupal\Core\Render\BareHtmlPageRenderer + * @see \Drupal\Core\Render\HtmlResponse + * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor */ class HtmlRenderer implements MainContentRendererInterface { @@ -119,39 +124,18 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // page.html.twig, hence add them here, just before rendering html.html.twig. $this->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 - // placeholders are replaced (which may attach additional assets). - // html.html.twig must be able to render the final list of attached assets, - // and hence may not replace any placeholders (because they might add yet - // more assets to be attached), and therefore it must be rendered with - // drupal_render(), not drupal_render_root(). - $this->renderer->render($html['page'], TRUE); - if (isset($html['page_top'])) { - $this->renderer->render($html['page_top'], TRUE); - } - if (isset($html['page_bottom'])) { - $this->renderer->render($html['page_bottom'], TRUE); - } - $content = $this->renderer->render($html); - - $response = new CacheableResponse($content, 200,[ - 'Content-Type' => 'text/html; charset=UTF-8', - ]); - - // Bubble the cacheability metadata associated with the rendered render - // arrays to the response. - foreach (['page_top', 'page', 'page_bottom'] as $region) { - if (isset($html[$region])) { - $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($html[$region])); - } - } + // @todo https://www.drupal.org/node/2495001 Make renderRoot return a + // cacheable render array directly. + $this->renderer->renderRoot($html); + $content = $this->renderCache->getCacheableRenderArray($html); // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. - $default = new CacheableMetadata(); - $default->setCacheTags(['rendered']); - $response->addCacheableDependency($default); + $content['#cache']['tags'][] = 'rendered'; + + $response = new HtmlResponse($content, 200, [ + 'Content-Type' => 'text/html; charset=UTF-8', + ]); return $response; } diff --git a/core/modules/editor/src/Tests/QuickEditIntegrationTest.php b/core/modules/editor/src/Tests/QuickEditIntegrationTest.php index ab482374686684edd85b07c74b861b40a28957c1..5a7b9cb82109d5c13b2a220858edbacb560d63d3 100644 --- a/core/modules/editor/src/Tests/QuickEditIntegrationTest.php +++ b/core/modules/editor/src/Tests/QuickEditIntegrationTest.php @@ -8,6 +8,7 @@ namespace Drupal\editor\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\Core\Language\LanguageInterface; use Drupal\quickedit\EditorSelector; use Drupal\quickedit\MetadataGenerator; @@ -16,6 +17,8 @@ use Drupal\quickedit_test\MockEditEntityFieldAccessCheck; use Drupal\editor\EditorController; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Tests Edit module integration (Editor module's inline editing support). @@ -214,7 +217,18 @@ public function testGetUntransformedTextCommand() { 'data' => 'Test', ) ); - $this->assertEqual(Json::encode($expected), $response->prepare($request)->getContent(), 'The GetUntransformedTextCommand AJAX command works correctly.'); + + $ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor'); + $subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor); + $event = new FilterResponseEvent( + \Drupal::service('http_kernel'), + $request, + HttpKernelInterface::MASTER_REQUEST, + $response + ); + $subscriber->onResponse($event); + + $this->assertEqual(Json::encode($expected), $response->getContent(), 'The GetUntransformedTextCommand AJAX command works correctly.'); } } diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 4c2e4a68ae5101b7ee9d762def649e6fc25c37f9..6bd21a5c2376975f380e3a646085b7edd2dac7d1 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -19,7 +19,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\EventSubscriber\AjaxSubscriber; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Extension\MissingDependencyException; use Drupal\Core\Render\Element; @@ -1801,7 +1801,7 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p $extra_post[$key] = $value; } } - $extra_post[AjaxSubscriber::AJAX_REQUEST_PARAMETER] = 1; + $extra_post[AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER] = 1; $extra_post += $this->getAjaxPageStatePostData(); // Now serialize all the $extra_post values, and prepend it with an '&'. $extra_post = '&' . $this->serializePostValues($extra_post); diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 3c10c8a58faa8b80f8cede442c91f89ca282f760..d207a42ab4948c6b09cddd88e775790dd5efda8f 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -198,7 +198,7 @@ public function handle($op, Request $request) { } $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update'); - return new Response($this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions)); + return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions); } /** diff --git a/core/modules/system/src/Tests/Ajax/CommandsTest.php b/core/modules/system/src/Tests/Ajax/CommandsTest.php index 969ec9c97b43fd87a5c6cadb5a81f5e4a7cac0c3..636c769178ae15a655cbdbb2dc155e03c51e10a5 100644 --- a/core/modules/system/src/Tests/Ajax/CommandsTest.php +++ b/core/modules/system/src/Tests/Ajax/CommandsTest.php @@ -23,7 +23,10 @@ use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\Ajax\RestripeCommand; use Drupal\Core\Ajax\SettingsCommand; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Performs tests on AJAX framework commands. @@ -133,7 +136,15 @@ public function testAttachedSettings() { 'drupalSettings' => ['foo' => 'bar'], ]); - $response->prepare(new Request()); + $ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor'); + $subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor); + $event = new FilterResponseEvent( + \Drupal::service('http_kernel'), + new Request(), + HttpKernelInterface::MASTER_REQUEST, + $response + ); + $subscriber->onResponse($event); $expected = [ 'command' => 'settings', ]; diff --git a/core/modules/views/src/Controller/ViewAjaxController.php b/core/modules/views/src/Controller/ViewAjaxController.php index 6a5264cdf4a0c0a4ad67e267ea90e45ecb0e9527..6b7e1d5c32f032098973bc6c04584fc973c3f509 100644 --- a/core/modules/views/src/Controller/ViewAjaxController.php +++ b/core/modules/views/src/Controller/ViewAjaxController.php @@ -12,7 +12,7 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\EventSubscriber\AjaxSubscriber; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\RedirectDestinationInterface; @@ -134,7 +134,7 @@ public function ajaxView(Request $request) { // Remove all of this stuff from the query of the request so it doesn't // end up in pagers and tablesort URLs. - foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxSubscriber::AJAX_REQUEST_PARAMETER) as $key) { + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER) as $key) { $request->query->remove($key); $request->request->remove($key); } diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 1d65c589a80dc27c9c13f1a84d09fb2f4ba1844a..b321940751b449d17ac814bd0cf9765c4cc64bc6 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -301,38 +301,18 @@ function views_theme_suggestions_container_alter(array &$suggestions, array $var } /** - * Implements hook_element_info_alter(). - * - * @see views_page_display_pre_render() - * @see views_preprocess_page() - */ -function views_element_info_alter(&$types) { - $types['page']['#pre_render'][] = 'views_page_display_pre_render'; -} - -/** - * #pre_render callback to set contextual links for views using a Page display. + * Implements MODULE_preprocess_HOOK(). */ -function views_page_display_pre_render(array $element) { +function views_preprocess_html(&$variables) { if (!\Drupal::moduleHandler()->moduleExists('contextual')) { - return $element; + return; } + // If the main content of this page contains a view, attach its contextual // links to the overall page array. This allows them to be rendered directly // next to the page title. if ($render_array = Page::getPageRenderArray()) { - views_add_contextual_links($element, 'page', $render_array['#display_id'], $render_array); - } - return $element; -} - -/** - * Implements MODULE_preprocess_HOOK(). - */ -function views_preprocess_html(&$variables) { - // Early-return to prevent adding unnecessary JavaScript. - if (!\Drupal::moduleHandler()->moduleExists('contextual') || !\Drupal::currentUser()->hasPermission('access contextual links')) { - return; + views_add_contextual_links($variables['page'], 'page', $render_array['#display_id'], $render_array); } // If the page contains a view as its main content, contextual links may have diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index c9050a42b110780a87f0ef8be6870e9f90197de4..23491f22ba166411077a1f09d22e0d7e63e369a3 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -11,7 +11,7 @@ use Drupal\Component\Utility\Timer; use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; -use Drupal\Core\EventSubscriber\AjaxSubscriber; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\views\Views; @@ -578,7 +578,7 @@ public function renderPreview($display_id, $args = array()) { // have some input in the query parameters, so we merge request() and // query() to ensure we get it all. $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all()); - foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { if (isset($exposed_input[$key])) { unset($exposed_input[$key]); } diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php index 19bbe8dd358a87adf75b62cda363e0fd0a32c1c8..acf3290b4a0366458532e130a507c93660dd9eed 100644 --- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php +++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php @@ -8,9 +8,12 @@ namespace Drupal\Tests\Core\Ajax; use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\Core\Render\Element\Ajax; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * @coversDefaultClass \Drupal\Core\Ajax\AjaxResponse @@ -81,7 +84,15 @@ public function testPrepareResponseForIeFormRequestsWithFileUpload() { $response = new AjaxResponse([]); $response->headers->set('Content-Type', 'application/json; charset=utf-8'); - $response->prepare($request); + $ajax_response_attachments_processor = $this->getMock('\Drupal\Core\Render\AttachmentsResponseProcessorInterface'); + $subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor); + $event = new FilterResponseEvent( + $this->getMock('\Symfony\Component\HttpKernel\HttpKernelInterface'), + $request, + HttpKernelInterface::MASTER_REQUEST, + $response + ); + $subscriber->onResponse($event); $this->assertEquals('text/html; charset=utf-8', $response->headers->get('Content-Type')); $this->assertEquals($response->getContent(), ''); } diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index 91c10c836ab094783b348af176b50965674fda10..52b6ef5033b50ebf1845760b1d9d6f6e72443bfe 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -128,7 +128,7 @@ public function providerTestAddAttachments() { return [ [new BubbleableMetadata(), [], new BubbleableMetadata()], [new BubbleableMetadata(), ['library' => ['core/foo']], (new BubbleableMetadata())->setAttachments(['library' => ['core/foo']])], - [(new BubbleableMetadata())->setAttachments(['library' => ['core/foo']]), ['library' => ['core/bar']], (new BubbleableMetadata())->setAttachments(['library' => ['core/bar', 'core/foo']])], + [(new BubbleableMetadata())->setAttachments(['library' => ['core/foo']]), ['library' => ['core/bar']], (new BubbleableMetadata())->setAttachments(['library' => ['core/foo', 'core/bar']])], ]; } diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 9f8bc4a044bf0d1f180de6a580b499b69e5afbce..af5ebe7fdba05297c92cbb8674854c9602d0f00d 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -62,6 +62,16 @@ function twig_render_template($template_file, array $variables) { catch (\Twig_Error_Loader $e) { drupal_set_message($e->getMessage(), 'error'); } + catch (\Twig_Error_Runtime $e) { + // In case there is a previous exception, re-throw the previous exception, + // so that the original exception is shown, rather than + // \Twig_Template::displayWithErrorHandling()'s exception. + $previous_exception = $e->getPrevious(); + if ($previous_exception) { + throw $previous_exception; + } + throw $e; + } if ($twig_service->isDebug()) { $output['debug_prefix'] .= "\n\n"; $output['debug_prefix'] .= "\n";