core/core.services.yml | 5 + core/lib/Drupal/Core/Ajax/AjaxResponse.php | 145 ---------------- .../Core/Ajax/AjaxResponseAttachmentsProcessor.php | 188 +++++++++++++++++++++ .../Drupal/Core/EventSubscriber/AjaxSubscriber.php | 56 +++++- 4 files changed, 248 insertions(+), 146 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 59c5421..50e921e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -816,6 +816,7 @@ services: arguments: ['@resolver_manager.entity'] ajax_subscriber: class: Drupal\Core\EventSubscriber\AjaxSubscriber + arguments: ['@ajax_response.attachments_processor'] tags: - { name: event_subscriber } form_ajax_subscriber: @@ -975,6 +976,10 @@ 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'] html_response.attachments_processor: class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor tags: diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 814345e..5f6f0d5 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -75,149 +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'); - - $attachments = $this->getAttachments(); - - // Resolve the attached libraries into asset collections. - $assets = new AttachedAssets(); - $assets->setLibraries(isset($attachments['library']) ? $attachments['library'] : []) - ->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []) - ->setSettings(isset($attachments['drupalSettings']) ? $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 index e69de29..79e8495 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -0,0 +1,188 @@ +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) { + 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) ? 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 (!empty($css_assets)) { + $css_render_array = $this->cssCollectionRenderer->render($css_assets); + $resource_commands[] = new AddCssCommand($this->renderer->render($css_render_array)); + } + if (!empty($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 (!empty($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(); + \Drupal::moduleHandler()->alter('ajax_render', $commands); + + return $commands; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php index 8996b5d..a6133d6 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php @@ -9,6 +9,7 @@ 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; @@ -20,6 +21,23 @@ class AjaxSubscriber implements EventSubscriberInterface { /** + * The AJAX response attachments processor service. + * + * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface + */ + protected $ajaxResponseAttachmentsProcessor; + + /** + * Constructs an AjaxSubscriber 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. * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event @@ -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(''); + } } }