core/authorize.php | 16 +- core/core.services.yml | 18 +- core/includes/batch.inc | 8 +- core/includes/errors.inc | 9 +- core/includes/install.core.inc | 4 +- core/includes/theme.inc | 51 ++--- core/lib/Drupal/Core/Ajax/AjaxResponse.php | 177 +---------------- .../Core/Ajax/AjaxResponseAttachmentsProcessor.php | 197 +++++++++++++++++++ ...axSubscriber.php => AjaxResponseSubscriber.php} | 62 +++++- .../EventSubscriber/DefaultExceptionSubscriber.php | 3 +- .../EventSubscriber/HtmlResponseSubscriber.php | 65 ++++++ .../EventSubscriber/MaintenanceModeSubscriber.php | 5 +- .../Core/Render/AttachmentsResponseInterface.php | 39 ++++ .../AttachmentsResponseProcessorInterface.php | 30 +++ .../Core/Render/AttachmentsResponseTrait.php | 41 ++++ .../Drupal/Core/Render/BareHtmlPageRenderer.php | 27 ++- .../Core/Render/BareHtmlPageRendererInterface.php | 4 +- core/lib/Drupal/Core/Render/HtmlResponse.php | 46 +++++ .../Render/HtmlResponseAttachmentsProcessor.php | 218 +++++++++++++++++++++ .../Core/Render/MainContent/HtmlRenderer.php | 47 ++--- .../editor/src/Tests/QuickEditIntegrationTest.php | 16 +- .../system/src/Controller/DbUpdateController.php | 2 +- .../modules/system/src/Tests/Ajax/CommandsTest.php | 13 +- core/modules/views/views.module | 30 +-- .../Drupal/Tests/Core/Ajax/AjaxResponseTest.php | 13 +- core/themes/engines/twig/twig.engine | 11 ++ 26 files changed, 853 insertions(+), 299 deletions(-) diff --git a/core/authorize.php b/core/authorize.php index 35277f6..9910362 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,18 @@ 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( + $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 5a9cd6e..43dc9ef 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: + ajax_response.subscriber: class: Drupal\Core\EventSubscriber\AjaxSubscriber + 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..45f0573 100644 --- a/core/includes/batch.inc +++ b/core/includes/batch.inc @@ -19,6 +19,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Batch\Percentage; use Drupal\Core\Form\FormState; +use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -137,9 +138,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 6933dc8..dd4d104 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -8,8 +8,8 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Utility\Error; -use Symfony\Component\HttpFoundation\Response; /** * Maps PHP error constants to watchdog severity levels. @@ -241,12 +241,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..db86eb2 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -9,6 +9,7 @@ */ 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; @@ -1308,41 +1309,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', + ]; + foreach ($types as $type) { + $token = Crypt::randomBytesBase64(55); + $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 a83898d..5f6f0d5 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\AttachmentsResponseInterface; +use Drupal\Core\Render\AttachmentsResponseTrait; 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 AttachmentsResponseInterface { + + use AttachmentsResponseTrait; /** * 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 @@ 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) ? 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..d1e456b --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -0,0 +1,197 @@ +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(AttachmentsResponseInterface $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)); + } + } + + /** + * 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/AjaxSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php similarity index 30% rename from core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php rename to core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php index 8996b5d..045655a 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php @@ -2,22 +2,40 @@ /** * @file - * Contains \Drupal\Core\EventSubscriber\AjaxSubscriber. + * 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; /** - * Subscribes to set AJAX HTML IDs and prepare AJAX responses. + * Response subscriber to handle AJAX responses. */ -class AjaxSubscriber implements EventSubscriberInterface { +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; + } /** * Sets the AJAX HTML IDs from the current request. @@ -38,7 +56,43 @@ public function onRequest(GetResponseEvent $event) { public function onResponse(FilterResponseEvent $event) { $response = $event->getResponse(); if ($response instanceof AjaxResponse) { - $response->prepareResponse($event->getRequest()); + $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(''); + } } } 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 @@ 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 0000000..6fada86 --- /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 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\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/AttachmentsResponseInterface.php b/core/lib/Drupal/Core/Render/AttachmentsResponseInterface.php new file mode 100644 index 0000000..fd501f9 --- /dev/null +++ b/core/lib/Drupal/Core/Render/AttachmentsResponseInterface.php @@ -0,0 +1,39 @@ + [], + 'drupalSettings' => [], + 'html_head' => [], + 'feed' => [], + 'html_head_link' => [], + 'http_header' => [], + ]; + + /** + * {@inheritdoc} + */ + public function setAttachments(array $attachments) { + $this->attachments = $attachments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAttachments() { + return $this->attachments; + } + +} 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 @@ 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 1b4a5f5..9e1f8ee 100644 --- a/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php @@ -59,8 +59,8 @@ * 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/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php new file mode 100644 index 0000000..b8107b0 --- /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 0000000..d143d83 --- /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(AttachmentsResponseInterface $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..8401d21 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -13,6 +13,7 @@ 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; @@ -24,6 +25,13 @@ /** * 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 +127,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 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_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/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 @@ 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 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\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/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/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 @@ 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/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 9f8bc4a..38f3d5f 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -62,6 +62,17 @@ 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, we just display the message and + // show the original Exception so that the original function that fails is + // shown. + $previous_exception = $e->getPrevious(); + if ($previous_exception) { + drupal_set_message($e->getMessage(), 'error'); + throw $previous_exception; + } + throw $e; + } if ($twig_service->isDebug()) { $output['debug_prefix'] .= "\n\n"; $output['debug_prefix'] .= "\n";