Skip to content
Snippets Groups Projects
Commit 12030b35 authored by Geoff Appleby's avatar Geoff Appleby
Browse files

Update CSP Extras to backport AJAX attachments from 10.1

parent c876a2fb
No related branches found
No related tags found
No related merge requests found
......@@ -13,6 +13,6 @@
"require": {
"php": ">=7.2",
"ext-json": "*",
"drupal/core": "^9.3 || ^10"
"drupal/core": "^9.5 || ^10"
}
}
name: Content Security Policy
type: module
description: Provide Content-Security-Policy headers
core_version_requirement: ^9.2 || ^10
core_version_requirement: ^9.5 || ^10
configure: csp.settings
name: Content Security Policy Extras
type: module
description: Optional extra features for Content Security Policy
core_version_requirement: ^9.3 || ^10
core_version_requirement: ^9.5 || ^10
dependencies:
- csp:csp
<?php
/**
* Implements hook_requirements().
*/
function csp_extras_requirements($phase) {
$requirements = [];
if ($phase == 'runtime' && version_compare(\Drupal::VERSION, '10.1', '>=')) {
$requirements['csp_extras'] = [
'title' => t('CSP Extras'),
'value' => t('Module is not required in Drupal ^10.1'),
'severity' => REQUIREMENT_WARNING,
];
}
return $requirements;
}
......@@ -9,12 +9,14 @@
* Implements hook_library_info_alter().
*/
function csp_extras_library_info_alter(&$libraries, $extension) {
// Add module ajax.js to core library.
if ($extension == 'core' && isset($libraries['drupal.ajax'])) {
$path = '/' . \Drupal::service('extension.list.module')->getPath('csp_extras') . '/js/ajax.js';
$libraries['drupal.ajax']['js'][$path] = [
'version' => '1.13',
];
if (version_compare(\Drupal::VERSION, '10.1', '<')) {
// Add module ajax.js to core library.
if ($extension == 'core' && isset($libraries['drupal.ajax'])) {
$path = '/' . \Drupal::service('extension.list.module')
->getPath('csp_extras') . '/js/ajax.js';
$libraries['drupal.ajax']['js'][$path] = [
'version' => '1.18',
];
}
}
}
services:
# Replace the core attachments_processor service.
ajax_response.attachments_processor:
csp_extras.ajax_response.attachments_processor:
class: Drupal\csp_extras\Ajax\AjaxResponseAttachmentsProcessor
decorates: 'ajax_response.attachments_processor'
arguments:
- '@csp_extras.ajax_response.attachments_processor.inner'
- '@asset.resolver'
- '@config.factory'
- '@asset.css.collection_renderer'
- '@asset.js.collection_renderer'
- '@request_stack'
- '@renderer'
- '@module_handler'
- '@datetime.time'
- '@file_url_generator'
......@@ -6,58 +6,55 @@
(function (window, Drupal) {
/**
* Command to add script and style assets.
* Command to add css.
*
* Backported from Drupal 10.1 to handle attributes arrays.
*
* @param {Drupal.Ajax} [ajax]
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
* @param {object} response
* The response from the Ajax request.
* @param {string} response.assets
* An object that contains the script and styles to be added.
* @param {object[]|string} response.data
* An array of styles to be added.
* @param {number} [status]
* The XMLHttpRequest status.
*/
Drupal.AjaxCommands.prototype.add_assets = function (ajax, response, status) {
var assetsLoaded = 0;
function onAssetLoad() {
assetsLoaded += 1;
// When new scripts are loaded, attach newly added behaviors.
if (assetsLoaded >= response.assets.length) {
Drupal.attachBehaviors(document.body, ajax.settings);
}
Drupal.AjaxCommands.prototype.add_css = function (ajax, response, status) {
if (typeof response.data === 'string') {
$('head').prepend(response.data);
return;
}
response.assets.forEach(function (item) {
var elem;
var target = document.body;
if (item.type === "script") {
elem = document.createElement("script");
if (typeof item.attributes.async === "undefined") {
elem.async = false;
}
} else if (item.type === "stylesheet") {
elem = document.createElement("link");
elem.rel = "stylesheet";
target = document.head;
}
Object.keys(item.attributes).forEach(function (key) {
elem[key] = item.attributes[key];
const allUniqueBundleIds = response.data.map(function (style) {
const uniqueBundleId = style.href + ajax.instanceIndex;
loadjs(style.href, uniqueBundleId, {
before(path, styleEl) {
// This allows all attributes to be added, like media.
Object.keys(style).forEach((attributeKey) => {
styleEl.setAttribute(attributeKey, style[attributeKey]);
});
},
});
return uniqueBundleId;
});
// Returns the promise so that the next AJAX command waits on the
// completion of this one to execute, ensuring the CSS is loaded before
// executing.
return new Promise((resolve, reject) => {
loadjs.ready(allUniqueBundleIds, {
success() {
// All CSS files were loaded. Resolve the promise and let the
// remaining commands execute.
resolve();
},
error(depsNotFound) {
const message = Drupal.t(
`The following files could not be loaded: @dependencies`,
{ '@dependencies': depsNotFound.join(', ') },
);
reject(message);
},
});
if (item.type === "script") {
elem.onload = onAssetLoad;
}
else {
// Directly mark this element as loaded. We don't have to wait before
// behaviours can be attached.
onAssetLoad();
}
target.appendChild(elem);
});
};
......
<?php
namespace Drupal\csp_extras\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command for including additional assets.
*
* The 'add_assets' command instructs the client to load additional JS and CSS.
*
* This command is implemented by Drupal.AjaxCommands.prototype.add_assets()
* defined in csp_extras/js/ajax.js.
*
* @ingroup ajax
*/
class AddAssetsCommand implements CommandInterface {
/**
* An array of assets keyed by their type (either 'script' or 'style').
*
* @var array[]
*/
protected $assets;
/**
* Constructs a AddAssetsCommand object.
*
* @param array[] $assets
* An array of asset element definitions.
*/
public function __construct(array $assets) {
$this->assets = array_values($assets);
}
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return [
'command' => 'add_assets',
'assets' => $this->assets,
];
}
}
......@@ -2,16 +2,18 @@
namespace Drupal\csp_extras\Ajax;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AddJsCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\SettingsCommand;
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\File\FileUrlGeneratorInterface;
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;
......@@ -23,6 +25,13 @@ use Symfony\Component\HttpFoundation\RequestStack;
*/
class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
/**
* The decorated Ajax Response Attachments Processor.
*
* @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
*/
protected $decoratedAjaxResponseAttachmentsProcessor;
/**
* The asset resolver service.
*
......@@ -38,75 +47,81 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
protected $config;
/**
* The request stack.
* The CSS asset collection renderer service.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $requestStack;
protected $cssCollectionRenderer;
/**
* The module handler.
* The JS asset collection renderer service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $moduleHandler;
protected $jsCollectionRenderer;
/**
* The time service.
* The request stack.
*
* @var \Drupal\Component\Datetime\TimeInterface
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $time;
protected $requestStack;
/**
* The file URL generator.
* The module handler.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $fileUrlGenerator;
protected $moduleHandler;
/**
* Constructs a AjaxResponseAttachmentsProcessor object.
* Constructs an AjaxResponseAttachmentsProcessor object.
*
* @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $decoratedAjaxResponseAttachmentsProcessor
* The decorated Attachments Response Processor.
* @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.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\File\FileUrlGeneratorInterface|null $file_url_generator
* The file URL generator.
*/
public function __construct(
AttachmentsResponseProcessorInterface $decoratedAjaxResponseAttachmentsProcessor,
AssetResolverInterface $asset_resolver,
ConfigFactoryInterface $config_factory,
AssetCollectionRendererInterface $css_collection_renderer,
AssetCollectionRendererInterface $js_collection_renderer,
RequestStack $request_stack,
ModuleHandlerInterface $module_handler,
TimeInterface $time,
FileUrlGeneratorInterface $file_url_generator = NULL
RendererInterface $renderer,
ModuleHandlerInterface $module_handler
) {
$this->decoratedAjaxResponseAttachmentsProcessor = $decoratedAjaxResponseAttachmentsProcessor;
$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;
$this->time = $time;
if (!$file_url_generator) {
// phpcs:disable
@trigger_error('Calling ' . __CLASS__ . '::__construct() without the $file_url_generator argument is deprecated', E_USER_DEPRECATED);
// phpcs:enable
$file_url_generator = \Drupal::service('file_url_generator');
}
$this->fileUrlGenerator = $file_url_generator;
}
/**
* {@inheritdoc}
*/
public function processAttachments(AttachmentsInterface $response) {
if (version_compare(\Drupal::VERSION, '10.1', '>=')) {
return $this->decoratedAjaxResponseAttachmentsProcessor->processAttachments($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.');
......@@ -171,70 +186,38 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
unset($js_assets_footer['drupalSettings']);
}
// Remove assets that are not available to all browsers.
$css_assets = array_filter(
$css_assets,
[$this, 'filterBrowserAssets']
);
$js_assets = array_filter(
array_merge($js_assets_header, $js_assets_footer),
[$this, 'filterBrowserAssets']
);
if (!empty($css_assets) || !empty($js_assets)) {
$default_query_string = \Drupal::state()->get('system.css_js_query_string') ?: '0';
$css_assets = array_map(
function ($css_asset) use ($default_query_string) {
$asset = [
'type' => 'stylesheet',
'attributes' => [
'media' => $css_asset['media'],
'href' => $this->fileUrlGenerator->generateString($css_asset['data']),
],
];
if (isset($css_asset['attributes'])) {
$asset['attributes'] += $css_asset['attributes'];
}
// Only add the cache-busting query string if this isn't an
// aggregate file.
if ($css_asset['type'] == 'file' && !isset($css_asset['preprocessed'])) {
$query_string_separator = (strpos($css_asset['data'], '?') !== FALSE) ? '&' : '?';
$asset['attributes']['href'] .= $query_string_separator . $default_query_string;
}
return $asset;
},
$css_assets
if (version_compare(\Drupal::VERSION, '10.0.0', '<')) {
// Remove assets that are not available to all browsers.
$css_assets = array_filter(
$css_assets,
[$this, 'filterBrowserAssets']
);
$js_assets = array_map(
function ($js_asset) use ($default_query_string) {
$asset = [
'type' => 'script',
'attributes' => [
'src' => $this->fileUrlGenerator->generateString($js_asset['data']),
],
];
if (isset($js_asset['attributes'])) {
$asset['attributes'] += $js_asset['attributes'];
}
// Only add the cache-busting query string if this isn't an
// aggregate file.
if ($js_asset['type'] == 'file' && !isset($js_asset['preprocessed'])) {
$query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
$query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
$asset['attributes']['src'] .= $query_string_separator . ($js_asset['cache'] ? $query_string : $this->time->getRequestTime());
}
return $asset;
},
$js_assets
$js_assets_header = array_filter(
$js_assets_header,
[$this, 'filterBrowserAssets']
);
$js_assets_footer = array_filter(
$js_assets_footer,
[$this, 'filterBrowserAssets']
);
}
$response->addCommand(new AddAssetsCommand(array_merge($css_assets, $js_assets)), TRUE);
// Prepend commands to add the assets, preserving their relative order.
$resource_commands = [];
if ($css_assets) {
$css_render_array = $this->cssCollectionRenderer->render($css_assets);
$resource_commands[] = new AddCssCommand(array_column($css_render_array, '#attributes'));
}
if ($js_assets_header) {
$js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
$resource_commands[] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head');
}
if ($js_assets_footer) {
$js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
$resource_commands[] = new AddJsCommand(array_column($js_footer_render_array, '#attributes'));
}
foreach (array_reverse($resource_commands) as $resource_command) {
$response->addCommand($resource_command, TRUE);
}
// Prepend a command to merge changes and additions to drupalSettings.
......
......@@ -72,6 +72,8 @@ class CoreCspSubscriber implements EventSubscriberInterface {
if (
in_array('core/drupal.ajax', $libraries)
&&
version_compare(\Drupal::VERSION, '10.1', '<')
&&
// The CSP Extras module alters core to not require 'unsafe-inline'.
!$this->moduleHandler->moduleExists('csp_extras')
) {
......@@ -83,6 +85,7 @@ class CoreCspSubscriber implements EventSubscriberInterface {
$policy->fallbackAwareAppendIfEnabled('script-src', [Csp::POLICY_UNSAFE_INLINE]);
$policy->fallbackAwareAppendIfEnabled('script-src-elem', [Csp::POLICY_UNSAFE_INLINE]);
}
// Drupal 10.1 adds CSS assets in a CSP-compatible way.
$policy->fallbackAwareAppendIfEnabled('style-src-attr', []);
$policy->fallbackAwareAppendIfEnabled('style-src', [Csp::POLICY_UNSAFE_INLINE]);
$policy->fallbackAwareAppendIfEnabled('style-src-elem', [Csp::POLICY_UNSAFE_INLINE]);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment