summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2015-06-23 16:24:29 (GMT)
committerAlex Pott2015-06-23 16:24:29 (GMT)
commit42c693a966eb26401211ca9c6525af9715adafa7 (patch)
treef5b2748e2b74ed2649a24a573f3d6baa55a903ea
parentc65b82f4f842ba495219625a8929ae492ead6fe6 (diff)
Issue #2407195 by Wim Leers, Fabianx, joelpittet, lauriii, Crell: Move attachment processing to services and per-type response subclasses
-rw-r--r--core/authorize.php20
-rw-r--r--core/core.services.yml20
-rw-r--r--core/includes/batch.inc7
-rw-r--r--core/includes/errors.inc8
-rw-r--r--core/includes/install.core.inc4
-rw-r--r--core/includes/theme.inc56
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxResponse.php177
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php199
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php117
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php63
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php3
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php65
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php5
-rw-r--r--core/lib/Drupal/Core/Render/AttachmentsInterface.php49
-rw-r--r--core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php30
-rw-r--r--core/lib/Drupal/Core/Render/AttachmentsTrait.php42
-rw-r--r--core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php27
-rw-r--r--core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php4
-rw-r--r--core/lib/Drupal/Core/Render/BubbleableMetadata.php66
-rw-r--r--core/lib/Drupal/Core/Render/HtmlResponse.php46
-rw-r--r--core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php218
-rw-r--r--core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php50
-rw-r--r--core/modules/editor/src/Tests/QuickEditIntegrationTest.php16
-rw-r--r--core/modules/simpletest/src/WebTestBase.php4
-rw-r--r--core/modules/system/src/Controller/DbUpdateController.php2
-rw-r--r--core/modules/system/src/Tests/Ajax/CommandsTest.php13
-rw-r--r--core/modules/views/src/Controller/ViewAjaxController.php4
-rw-r--r--core/modules/views/views.module30
-rw-r--r--core/modules/views_ui/src/ViewUI.php4
-rw-r--r--core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php13
-rw-r--r--core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php2
-rw-r--r--core/themes/engines/twig/twig.engine10
32 files changed, 942 insertions, 432 deletions
diff --git a/core/authorize.php b/core/authorize.php
index 35277f6..d9f1e56 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -85,8 +85,10 @@ drupal_maintenance_theme();
$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 @@ if (authorize_access_allowed($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 6c2c78f..d8a8d24 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 9281837..4e47471 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 </html> 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('<!--partial-->', $fallback);
print $fallback;
diff --git a/core/includes/errors.inc b/core/includes/errors.inc
index 6933dc8..06753fd 100644
--- a/core/includes/errors.inc
+++ b/core/includes/errors.inc
@@ -9,7 +9,6 @@ use Drupal\Component\Utility\SafeMarkup;
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 402d41e..34bfcb0 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 5d645e2..7400017 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('<drupal-html-response-attachment-placeholder type="@type" token="@token"></drupal-html-response-attachment-placeholder>', [
+ '@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 1acf03a..a2218e6 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -10,6 +10,8 @@ namespace Drupal\Core\Ajax;
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 @@ use Symfony\Component\HttpFoundation\Response;
*
* @ingroup ajax
*/
-class AjaxResponse extends JsonResponse {
+class AjaxResponse extends JsonResponse implements AttachmentsInterface {
+
+ use AttachmentsTrait;
/**
* The array of ajax commands.
@@ -30,32 +34,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.
*
* @param \Drupal\Core\Ajax\CommandInterface $command
@@ -80,7 +58,7 @@ class AjaxResponse extends JsonResponse {
'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 @@ class AjaxResponse extends JsonResponse {
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('<textarea>' . $this->data . '</textarea>');
- }
- }
-
- /**
- * 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 0000000..a689b76
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor.
+ */
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\AttachmentsInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Processes attachments of AJAX responses.
+ *
+ * @see \Drupal\Core\Ajax\AjaxResponse
+ * @see \Drupal\Core\Render\MainContent\AjaxRenderer
+ */
+class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
+
+ /**
+ * The asset resolver service.
+ *
+ * @var \Drupal\Core\Asset\AssetResolverInterface
+ */
+ protected $assetResolver;
+
+ /**
+ * A config object for the system performance configuration.
+ *
+ * @var \Drupal\Core\Config\Config
+ */
+ protected $config;
+
+ /**
+ * The CSS asset collection renderer service.
+ *
+ * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
+ */
+ protected $cssCollectionRenderer;
+
+ /**
+ * The JS asset collection renderer service.
+ *
+ * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
+ */
+ protected $jsCollectionRenderer;
+
+ /**
+ * The request stack.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * The renderer.
+ *
+ * @var \Drupal\Core\Render\RendererInterface
+ */
+ protected $renderer;
+
+ /**
+ * The module handler.
+ *
+ * @var \Drupal\Core\Extension\ModuleHandlerInterface
+ */
+ protected $moduleHandler;
+
+ /**
+ * Constructs a AjaxResponseAttachmentsProcessor object.
+ *
+ * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
+ * An asset resolver.
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * A config factory for retrieving required config objects.
+ * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
+ * The CSS asset collection renderer.
+ * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
+ * The JS asset collection renderer.
+ * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+ * The request stack.
+ * @param \Drupal\Core\Render\RendererInterface $renderer
+ * The renderer.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler.
+ */
+ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+ $this->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 0000000..f35b852
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\AjaxResponseSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Response subscriber to handle AJAX responses.
+ */
+class AjaxResponseSubscriber implements EventSubscriberInterface {
+
+ /**
+ * The AJAX response attachments processor service.
+ *
+ * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+ */
+ protected $ajaxResponseAttachmentsProcessor;
+
+ /**
+ * Constructs an AjaxResponseSubscriber object.
+ *
+ * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $ajax_response_attachments_processor
+ * The AJAX response attachments processor service.
+ */
+ public function __construct(AttachmentsResponseProcessorInterface $ajax_response_attachments_processor) {
+ $this->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('<textarea>' . $response->getContent() . '</textarea>');
+ }
+ }
+ }
+
+ /**
+ * {@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 8a85659..0000000
--- a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\EventSubscriber\AjaxSubscriber.
- */
-
-namespace Drupal\Core\EventSubscriber;
-
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Ajax\AjaxResponse;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
-use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\KernelEvents;
-
-/**
- * Subscribes to set AJAX HTML IDs and prepare AJAX responses.
- */
-class AjaxSubscriber implements EventSubscriberInterface {
-
- /**
- * 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) {
- $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 46d6c46..5f9d4ac 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
@@ -129,8 +129,7 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
}
$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 0000000..6fada86
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\HtmlResponseSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Response subscriber to handle HTML responses.
+ */
+class HtmlResponseSubscriber implements EventSubscriberInterface {
+
+ /**
+ * The HTML response attachments processor service.
+ *
+ * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+ */
+ protected $htmlResponseAttachmentsProcessor;
+
+ /**
+ * Constructs a HtmlResponseSubscriber object.
+ *
+ * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor
+ * The HTML response attachments processor service.
+ */
+ public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor) {
+ $this->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 e43d00e..92071da 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
@@ -18,7 +18,6 @@ use Drupal\Core\Site\MaintenanceModeInterface;
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 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
$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 0000000..4d9b97c
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/AttachmentsInterface.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\AttachmentsInterface.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Defines an interface for responses that can expose #attached metadata.
+ *
+ * @todo If in Drupal 9, we remove attachments other than assets (libraries +
+ * drupalSettings), then we can look into unifying this with
+ * \Drupal\Core\Asset\AttachedAssetsInterface.
+ *
+ * @see \Drupal\Core\Render\AttachmentsTrait
+ */
+interface AttachmentsInterface {
+
+ /**
+ * Gets attachments.
+ *
+ * @return array
+ * The attachments.
+ */
+ public function getAttachments();
+
+ /**
+ * Adds attachments.
+ *
+ * @param array $attachments
+ * The attachments to add.
+ *
+ * @return $this
+ */
+ public function addAttachments(array $attachments);
+
+ /**
+ * Sets attachments.
+ *
+ * @param array $attachments
+ * The attachments to set.
+ *
+ * @return $this
+ */
+ public function setAttachments(array $attachments);
+
+}
diff --git a/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
new file mode 100644
index 0000000..8257763
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Render\AttachmentsResponseProcessorInterface.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Defines an interface for processing attachments of responses that have them.
+ *
+ * @see \Drupal\Core\Render\HtmlResponse
+ * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
+ */
+interface AttachmentsResponseProcessorInterface {
+
+ /**
+ * Processes the attachments of a response that has attachments.
+ *
+ * @param \Drupal\Core\Render\AttachmentsInterface $response
+ * The response to process the attachments for.
+ *
+ * @return \Drupal\Core\Render\AttachmentsInterface
+ * The processed response.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function processAttachments(AttachmentsInterface $response);
+
+}
diff --git a/core/lib/Drupal/Core/Render/AttachmentsTrait.php b/core/lib/Drupal/Core/Render/AttachmentsTrait.php
new file mode 100644
index 0000000..9616e70
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/AttachmentsTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Core\Render;
+
+/**
+ * Provides an implementation of AttachmentsInterface.
+ *
+ * @see \Drupal\Core\Render\AttachmentsInterface
+ */
+trait AttachmentsTrait {
+
+ /**
+ * The attachments for this response.
+ *
+ * @var array
+ */
+ protected $attachments = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttachments() {
+ return $this->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 c8012c6..a7164d9 100644
--- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -20,13 +20,23 @@ 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 @@ class BareHtmlPageRenderer implements BareHtmlPageRendererInterface {
$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 1b4a5f5..9e1f8ee 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 8e6875f..5cb350e 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -15,14 +15,9 @@ use Drupal\Core\Cache\CacheableMetadata;
*
* @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 @@ class BubbleableMetadata extends CacheableMetadata {
// 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 @@ class BubbleableMetadata extends CacheableMetadata {
*/
public function applyTo(array &$build) {
parent::applyTo($build);
- $build['#attached'] = $this->attached;
+ $build['#attached'] = $this->attachments;
}
/**
@@ -74,50 +69,11 @@ class BubbleableMetadata extends CacheableMetadata {
*/
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.
*
* @return array
@@ -125,7 +81,7 @@ class BubbleableMetadata extends CacheableMetadata {
* @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 @@ class BubbleableMetadata extends CacheableMetadata {
* @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 0000000..7053f83
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/HtmlResponse.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\HtmlResponse.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A response that contains and can expose cacheability metadata and attachments.
+ *
+ * Supports Drupal's caching concepts: cache tags for invalidation and cache
+ * contexts for variations.
+ *
+ * Supports Drupal's idea of #attached metadata: libraries, settings, http_header and html_head.
+ *
+ * @see \Drupal\Core\Cache\CacheableResponse
+ * @see \Drupal\Core\Render\AttachmentsInterface
+ * @see \Drupal\Core\Render\AttachmentsTrait
+ */
+class HtmlResponse extends Response implements CacheableResponseInterface, AttachmentsInterface {
+
+ use CacheableResponseTrait;
+ use AttachmentsTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContent($content) {
+ // A render array can automatically be converted to a string and set the
+ // necessary metadata.
+ if (is_array($content) && (isset($content['#markup']))) {
+ $this->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 0000000..50ff584
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Processes attachments 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\MainContent\HtmlRenderer
+ */
+class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
+
+ /**
+ * The asset resolver service.
+ *
+ * @var \Drupal\Core\Asset\AssetResolverInterface
+ */
+ protected $assetResolver;
+
+ /**
+ * A config object for the system performance configuration.
+ *
+ * @var \Drupal\Core\Config\Config
+ */
+ protected $config;
+
+ /**
+ * The CSS asset collection renderer service.
+ *
+ * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
+ */
+ protected $cssCollectionRenderer;
+
+ /**
+ * The JS asset collection renderer service.
+ *
+ * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
+ */
+ protected $jsCollectionRenderer;
+
+ /**
+ * The request stack.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * The renderer.
+ *
+ * @var \Drupal\Core\Render\RendererInterface
+ */
+ protected $renderer;
+
+ /**
+ * Constructs a HtmlResponseAttachmentsProcessor object.
+ *
+ * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
+ * An asset resolver.
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * A config factory for retrieving required config objects.
+ * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
+ * The CSS asset collection renderer.
+ * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
+ * The JS asset collection renderer.
+ * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+ * The request stack.
+ * @param \Drupal\Core\Render\RendererInterface $renderer
+ * The renderer.
+ */
+ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer) {
+ $this->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 d61bb24..cb987f4 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 @@ class HtmlRenderer implements MainContentRendererInterface {
// 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 ab48237..5a7b9cb 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\Tests\QuickEditTestBase;
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 @@ class QuickEditIntegrationTest extends QuickEditTestBase {
'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 4c2e4a6..6bd21a5 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -19,7 +19,7 @@ use Drupal\Component\Utility\SafeMarkup;
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 @@ abstract class WebTestBase extends TestBase {
$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 3c10c8a..d207a42 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -198,7 +198,7 @@ class DbUpdateController extends ControllerBase {
}
$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 969ec9c..636c769 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\PrependCommand;
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 @@ class CommandsTest extends AjaxTestBase {
'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 6a5264c..6b7e1d5 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\AjaxResponse;
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 @@ class ViewAjaxController implements ContainerInjectionInterface {
// 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 1d65c58..b321940 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 c9050a4..23491f2 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\Html;
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 @@ class ViewUI implements ViewEntityInterface {
// 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 19bbe8d..acf3290 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 @@ class AjaxResponseTest extends UnitTestCase {
$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(), '<textarea>[]</textarea>');
}
diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
index 91c10c8..52b6ef5 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -128,7 +128,7 @@ class BubbleableMetadataTest extends UnitTestCase {
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 9f8bc4a..af5ebe7 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<!-- THEME DEBUG -->";
$output['debug_prefix'] .= "\n<!-- THEME HOOK: '" . SafeMarkup::checkPlain($variables['theme_hook_original']) . "' -->";