cdn.routing.yml | 3 +- cdn_ui/js/summaries.js | 10 ---- cdn_ui/src/Form/CdnSettingsForm.php | 52 +---------------- config/install/cdn.settings.yml | 1 - config/schema/cdn.schema.yml | 5 -- src/CdnFarfutureController.php | 25 ++------ src/CdnSettings.php | 53 ----------------- src/File/FileUrlGenerator.php | 77 ++++++++++++++++--------- src/PathProcessor/CdnFarfuturePathProcessor.php | 6 +- 9 files changed, 59 insertions(+), 173 deletions(-) diff --git a/cdn.routing.yml b/cdn.routing.yml index a1c77ff..7dec5ad 100644 --- a/cdn.routing.yml +++ b/cdn.routing.yml @@ -1,8 +1,7 @@ cdn.farfuture.download: - path: '/cdn/farfuture/{security_token}/{mtime}/{scheme}' + path: '/cdn/farfuture/{security_token}/{mtime}' defaults: _controller: '\Drupal\cdn\CdnFarfutureController::download' requirements: _access: 'TRUE' mtime: \d+ - scheme: .+ diff --git a/cdn_ui/js/summaries.js b/cdn_ui/js/summaries.js index 89e0d58..64696be 100644 --- a/cdn_ui/js/summaries.js +++ b/cdn_ui/js/summaries.js @@ -15,16 +15,6 @@ return document.querySelector('input[name="status"]').checked ? Drupal.t('Enabled') : Drupal.t('Disabled'); }); - $('[data-drupal-selector="edit-wrappers"]').drupalSetSummary(function () { - var additional = $('[data-drupal-selector="edit-wrappers-additional-wrappers"] input:checked'); - var wrappers = []; - additional.each(function(index) { - wrappers.push(this.getAttribute('value')); - }); - var text = wrappers.join(', '); - return text.length ? text : Drupal.t('No additional wrappers'); - }); - $('[data-drupal-selector="edit-mapping"]').drupalSetSummary(function () { if (document.querySelector('select[name="mapping[type]"]').value === 'simple') { var domain = document.querySelector('input[name="mapping[simple][domain]"]').value; diff --git a/cdn_ui/src/Form/CdnSettingsForm.php b/cdn_ui/src/Form/CdnSettingsForm.php index 47b6209..bd975a7 100644 --- a/cdn_ui/src/Form/CdnSettingsForm.php +++ b/cdn_ui/src/Form/CdnSettingsForm.php @@ -3,12 +3,8 @@ namespace Drupal\cdn_ui\Form; use Drupal\cdn\CdnSettings; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\StreamWrapper\StreamWrapperInterface; -use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Configure CDN settings for this site. @@ -16,31 +12,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class CdnSettingsForm extends ConfigFormBase { /** - * The stream wrapper manager. - * - * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface - */ - protected $streamWrapperManager; - - /** - * @inheritDoc - */ - public function __construct(ConfigFactoryInterface $config_factory, StreamWrapperManagerInterface $streamWrapperManager) { - parent::__construct($config_factory); - $this->streamWrapperManager = $streamWrapperManager; - } - - /** - * @inheritDoc - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory'), - $container->get('stream_wrapper_manager') - ); - } - - /** * {@inheritdoc} */ public function getFormId() { @@ -103,6 +74,7 @@ class CdnSettingsForm extends ConfigFormBase { '#wrapper_attributes' => ['class' => ['container-inline']], '#attributes' => ['class' => ['container-inline']], '#default_value' => $config->get('mapping.type') === 'simple' ?: 'advanced', + '#attributes' => ['class' => ['container-inline']], ]; $form['mapping']['simple'] = [ '#type' => 'container', @@ -171,25 +143,6 @@ class CdnSettingsForm extends ConfigFormBase { '#default_value' => $config->get('farfuture.status'), ]; - $wrappers = array_keys($this->streamWrapperManager - ->getWrappers(StreamWrapperInterface::WRITE_VISIBLE)); - $localWrappers = array_keys($this->streamWrapperManager - ->getWrappers(StreamWrapperInterface::LOCAL)); - $wrappers = array_diff($wrappers, $localWrappers); - $existingWrappers = $config->get('additional_wrappers'); - $form['wrappers'] = [ - '#type' => 'details', - '#title' => $this->t('Additional stream wrappers'), - '#group' => 'cdn_settings', - '#access' => $wrappers || $existingWrappers, - '#tree' => TRUE, - ]; - $form['wrappers']['additional_wrappers'] = [ - '#type' => 'checkboxes', - '#options' => array_combine($wrappers, $wrappers), - '#default_value' => !is_null($existingWrappers) ? $existingWrappers : [], - '#description' => $this->t('Additional stream wrappers to rewrite for CDN.') - ]; return parent::buildForm($form, $form_state); } @@ -216,9 +169,6 @@ class CdnSettingsForm extends ConfigFormBase { // Vertical tab: 'Status'. $config->set('status', (bool) $form_state->getValue('status')); - // Vertical tab: 'Additional stream wrappers' - $config->set('additional_wrappers', array_values(array_filter($form_state->getValue(['wrappers', 'additional_wrappers'])))); - // Vertical tab: 'Mapping'. if ($form_state->getValue(['mapping', 'type']) === 'simple') { $simple_mapping = $form_state->getValue(['mapping', 'simple']); diff --git a/config/install/cdn.settings.yml b/config/install/cdn.settings.yml index 5934069..0ed3fc6 100644 --- a/config/install/cdn.settings.yml +++ b/config/install/cdn.settings.yml @@ -69,4 +69,3 @@ mapping: farfuture: status: true -additional_wrappers: [] diff --git a/config/schema/cdn.schema.yml b/config/schema/cdn.schema.yml index 8a419ed..b0d9272 100644 --- a/config/schema/cdn.schema.yml +++ b/config/schema/cdn.schema.yml @@ -20,8 +20,3 @@ cdn.settings: status: label: 'Forever cacheable files — status' type: boolean - additional_wrappers: - label: 'Additional stream wrappers for CDN' - type: sequence - sequence: - type: string diff --git a/src/CdnFarfutureController.php b/src/CdnFarfutureController.php index ec11763..34bcb35 100644 --- a/src/CdnFarfutureController.php +++ b/src/CdnFarfutureController.php @@ -15,13 +15,6 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class CdnFarfutureController implements ContainerInjectionInterface { /** - * The app root. - * - * @var string - */ - protected $root; - - /** * The private key service. * * @var \Drupal\Core\PrivateKey @@ -32,8 +25,7 @@ class CdnFarfutureController implements ContainerInjectionInterface { * @param \Drupal\Core\PrivateKey $private_key * The private key service. */ - public function __construct($root, PrivateKey $private_key) { - $this->root = $root; + public function __construct(PrivateKey $private_key) { $this->privateKey = $private_key; } @@ -42,7 +34,6 @@ class CdnFarfutureController implements ContainerInjectionInterface { */ public static function create(ContainerInterface $container) { return new static( - $container->get('app.root'), $container->get('private_key') ); } @@ -59,8 +50,6 @@ class CdnFarfutureController implements ContainerInjectionInterface { * example). See https://www.drupal.org/node/1441502. * @param int $mtime * The file's mtime. - * @param string $scheme - * The file's scheme. * * @returns \Symfony\Component\HttpFoundation\BinaryFileResponse * The response that will efficiently send the requested file. @@ -72,7 +61,7 @@ class CdnFarfutureController implements ContainerInjectionInterface { * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException * Thrown when an invalid security token is provided. */ - public function download(Request $request, $security_token, $mtime, $scheme) { + public function download(Request $request, $security_token, $mtime) { // Ensure \Drupal\cdn\PathProcessor\CdnFarfuturePathProcessor did its job. if (!$request->query->has('root_relative_file_url')) { throw new BadRequestHttpException(); @@ -80,10 +69,7 @@ class CdnFarfutureController implements ContainerInjectionInterface { // Validate security token. $root_relative_file_url = $request->query->get('root_relative_file_url'); - $uri = $scheme == '_shipped' - ? '/' . $root_relative_file_url - : $scheme . '://' . $root_relative_file_url; - $calculated_token = Crypt::hmacBase64($mtime . $uri, $this->privateKey->get() . Settings::getHashSalt()); + $calculated_token = Crypt::hmacBase64($mtime . $root_relative_file_url, $this->privateKey->get() . Settings::getHashSalt()); if ($security_token !== $calculated_token) { throw new AccessDeniedHttpException('Invalid security token.'); } @@ -119,10 +105,7 @@ class CdnFarfutureController implements ContainerInjectionInterface { 'Last-Modified' => 'Wed, 20 Jan 1988 04:20:42 GMT', ]; - if ($scheme == '_shipped') { - $uri = $this->root . $uri; - } - $response = new BinaryFileResponse($uri, 200, $farfuture_headers, TRUE, NULL, FALSE, FALSE); + $response = new BinaryFileResponse(substr($root_relative_file_url, 1), 200, $farfuture_headers, TRUE, NULL, FALSE, FALSE); $response->isNotModified($request); return $response; } diff --git a/src/CdnSettings.php b/src/CdnSettings.php index 8fbfd18..f232c10 100644 --- a/src/CdnSettings.php +++ b/src/CdnSettings.php @@ -2,7 +2,6 @@ namespace Drupal\cdn; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigValueException; @@ -79,16 +78,6 @@ class CdnSettings { } /** - * Returns all additional stream wrappers to apply CDN to. - * - * @return array - */ - public function additionalWrappers() { - $additional = $this->rawSettings->get('additional_wrappers'); - return $additional ? $additional : []; - } - - /** * Builds a lookup table: file extension to CDN domain(s). * * @param array $mapping @@ -186,46 +175,4 @@ class CdnSettings { return $components === FALSE ? FALSE : empty(array_intersect($forbidden_components, array_keys($components))); } - /** - * Maps a URI to a CDN domain. - * - * @param string $uri - * The URI to map. - * - * @return string|bool - * The mapped domain, or FALSE if it could not be matched. - */ - public function getCdnDomain($uri) { - // Extension-specific mapping. - $file_extension = Unicode::strtolower(pathinfo($uri, PATHINFO_EXTENSION)); - $lookup_table = $this->getLookupTable(); - if (isset($lookup_table[$file_extension])) { - $key = $file_extension; - } - // Generic or fallback mapping. - elseif (isset($lookup_table['*'])) { - $key = '*'; - } - // No mapping. - else { - return FALSE; - } - - $result = $lookup_table[$key]; - - if ($result === FALSE) { - return FALSE; - } - // If there are multiple results, pick one using consistent hashing: ensure - // the same file is always served from the same CDN domain. - elseif (is_array($result)) { - $filename = basename($uri); - $hash = hexdec(substr(md5($filename), 0, 5)); - $cdn_domain = $result[$hash % count($result)]; - } - else { - $cdn_domain = $result; - } - return $cdn_domain; - } } diff --git a/src/File/FileUrlGenerator.php b/src/File/FileUrlGenerator.php index ed114db..88dc1e6 100644 --- a/src/File/FileUrlGenerator.php +++ b/src/File/FileUrlGenerator.php @@ -111,62 +111,78 @@ class FileUrlGenerator { return FALSE; } - if (!$this->canServe($uri)) { + $relative_url = $this->getRelativeUrl($uri); + if ($relative_url === FALSE) { return FALSE; } - if (!$cdn_domain = $this->settings->getCdnDomain($uri)) { + // Extension-specific mapping. + $file_extension = Unicode::strtolower(pathinfo($uri, PATHINFO_EXTENSION)); + $lookup_table = $this->settings->getLookupTable(); + if (isset($lookup_table[$file_extension])) { + $key = $file_extension; + } + // Generic or fallback mapping. + elseif (isset($lookup_table['*'])) { + $key = '*'; + } + // No mapping. + else { return FALSE; } - // When farfuture is enabled, rewrite the file URL to let Drupal serve the - // file with optimal headers. Only possible if the file exists. - if (!$scheme = $this->fileSystem->uriScheme($uri)) { - $scheme = '_shipped'; - $fileUri = $filePath = '/' . $uri; - $realFile = $this->root . $fileUri; + $result = $lookup_table[$key]; + + if ($result === FALSE) { + return FALSE; + } + // If there are multiple results, pick one using consistent hashing: ensure + // the same file is always served from the same CDN domain. + elseif (is_array($result)) { + $filename = basename($uri); + $hash = hexdec(substr(md5($filename), 0, 5)); + $cdn_domain = $result[$hash % count($result)]; } else { - $fileUri = $realFile = $uri; - $filePath = substr($fileUri, strlen($scheme . ':/')); // Leading slash. + $cdn_domain = $result; } - if ($this->settings->farfutureIsEnabled() && file_exists($realFile)) { + + // When farfuture is enabled, rewrite the file URL to let Drupal serve the + // file with optimal headers. Only possible if the file exists. + $absolute_file_path = $this->root . $relative_url; + if ($this->settings->farfutureIsEnabled() && file_exists($absolute_file_path)) { // We do the filemtime() call separately, because a failed filemtime() // will cause a PHP warning to be written to the log, which would remove // any performance gain achieved by removing the file_exists() call. - $mtime = filemtime($realFile); + $mtime = filemtime($absolute_file_path); // Generate a security token. Ensures that users can not request any file // they want by manipulating the URL (they could otherwise request // settings.php for example). See https://www.drupal.org/node/1441502. - $calculated_token = Crypt::hmacBase64($mtime . $fileUri, $this->privateKey->get() . Settings::getHashSalt()); - return '//' . $cdn_domain . $this->getBasePath() . '/cdn/farfuture/' . $calculated_token . '/' . $mtime . '/' . $scheme . $filePath; + $calculated_token = Crypt::hmacBase64($mtime . $relative_url, $this->privateKey->get() . Settings::getHashSalt()); + return '//' . $cdn_domain . $this->getBasePath() . '/cdn/farfuture/' . $calculated_token . '/' . $mtime . $relative_url; } - return '//' . $cdn_domain . $this->getBasePath() . '/' . $uri; + return '//' . $cdn_domain . $this->getBasePath() . $relative_url; } /** - * Determines if a URI can/should be served by CDN. + * Gets the relative URL for files that are shipped or in a local stream. * * @param string $uri * The URI to a file for which we need a CDN URL, or the path to a shipped * file. * - * @return bool - * Returns FALSE if the URI is not for a shipped file or in an eligible stream. - * TRUE otherwise. + * @return bool|string + * Returns FALSE if the URI is not for a shipped file or in a local stream. + * Otherwise, returns the relative URL. */ - protected function canServe($uri) { + protected function getRelativeUrl($uri) { $scheme = $this->fileSystem->uriScheme($uri); - // Allow additional stream wrappers to be served via CDN. - $streamWrapperTypes = array_merge(array_keys($this->streamWrapperManager - ->getWrappers(StreamWrapperInterface::LOCAL)), - $this->settings->additionalWrappers()); // If the URI is absolute — HTTP(S) or otherwise — return early, except if - // it's an absolute URI using an approved stream wrapper type. - if ($scheme && !in_array($scheme, $streamWrapperTypes)) { + // it's an absolute URI using a local stream wrapper scheme. + if ($scheme && !isset($this->streamWrapperManager->getWrappers(StreamWrapperInterface::LOCAL)[$scheme])) { return FALSE; } // If the URI is protocol-relative, return early. @@ -177,7 +193,14 @@ class FileUrlGenerator { elseif ($scheme === 'private') { return FALSE; } - return TRUE; + + $request = $this->requestStack->getCurrentRequest(); + + return $scheme + // Local stream wrapper. + ? str_replace($request->getSchemeAndHttpHost() . $this->getBasePath(), '', $this->streamWrapperManager->getViaUri($uri)->getExternalUrl()) + // Shipped file. + : '/' . $uri; } /** diff --git a/src/PathProcessor/CdnFarfuturePathProcessor.php b/src/PathProcessor/CdnFarfuturePathProcessor.php index ec5209e..4704d00 100644 --- a/src/PathProcessor/CdnFarfuturePathProcessor.php +++ b/src/PathProcessor/CdnFarfuturePathProcessor.php @@ -25,13 +25,13 @@ class CdnFarfuturePathProcessor implements InboundPathProcessorInterface { // Parse the security token, mtime and root-relative file URL. $tail = substr($path, strlen('/cdn/farfuture/')); - list($security_token, $mtime, $scheme, $root_relative_file_url) = explode('/', $tail, 4); + list($security_token, $mtime, $root_relative_file_url) = explode('/', $tail, 3); // Set the root-relative file URL as query parameter. - $request->query->set('root_relative_file_url', $root_relative_file_url); + $request->query->set('root_relative_file_url', '/' . $root_relative_file_url); // Return the same path, but without the trailing file. - return "/cdn/farfuture/$security_token/$mtime/$scheme"; + return "/cdn/farfuture/$security_token/$mtime"; } }