diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php index c1db106c606e5aef46aa2c4212826d987dea2e56..5f88a44c164ee5513ff0fea4363a944af82129f2 100644 --- a/core/assets/scaffold/files/default.settings.php +++ b/core/assets/scaffold/files/default.settings.php @@ -487,6 +487,15 @@ # $settings['file_chmod_directory'] = 0775; # $settings['file_chmod_file'] = 0664; +/** + * Optimized assets path: + * + * A local file system path where optimized assets will be stored. This directory + * must exist and be writable by Drupal. This directory must be relative to + * the Drupal installation directory and be accessible over the web. + */ +# $settings['file_assets_path'] = 'sites/default/files'; + /** * Public file base URL: * diff --git a/core/core.services.yml b/core/core.services.yml index f91e5976f3503a07d4d9aa864ab956b5aeac653f..18b22728037866fc7f91838f2a53e0e416fe0cf5 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1496,6 +1496,11 @@ services: calls: - [setContainer, ['@service_container']] Drupal\Core\StreamWrapper\StreamWrapperManagerInterface: '@stream_wrapper_manager' + stream_wrapper.assets: + class: Drupal\Core\StreamWrapper\AssetsStream + tags: + - { name: stream_wrapper, scheme: assets } + Drupal\Core\StreamWrapper\AssetsStream: '@stream_wrapper.assets' stream_wrapper.public: class: Drupal\Core\StreamWrapper\PublicStream tags: diff --git a/core/lib/Drupal/Core/Asset/AssetDumper.php b/core/lib/Drupal/Core/Asset/AssetDumper.php index 91360e5bc4642a132ab14afbcafda0aac1484507..278aeadef6349d1b067fdb347bb5afc5a68f2002 100644 --- a/core/lib/Drupal/Core/Asset/AssetDumper.php +++ b/core/lib/Drupal/Core/Asset/AssetDumper.php @@ -36,7 +36,7 @@ public function __construct(FileSystemInterface $file_system) { * browsers to download new CSS when the CSS changes. */ public function dump($data, $file_extension) { - $path = 'public://' . $file_extension; + $path = 'assets://' . $file_extension; // Prefix filename to prevent blocking by firewalls which reject files // starting with "ad*". $filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension; @@ -48,7 +48,7 @@ public function dump($data, $file_extension) { * {@inheritdoc} */ public function dumpToUri(string $data, string $file_extension, string $uri): string { - $path = 'public://' . $file_extension; + $path = 'assets://' . $file_extension; // Create the CSS or JS file. $this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY); try { diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php index ef5afa8eded007ba386dcba119eff201716b8a55..ec3b805c971dfada69fd904b3f22ee8b9d6e00d8 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -210,8 +210,8 @@ public function deleteAll() { $this->fileSystem->delete($uri); } }; - if (is_dir('public://css')) { - $this->fileSystem->scanDirectory('public://css', '/.*/', ['callback' => $delete_stale]); + if (is_dir('assets://css')) { + $this->fileSystem->scanDirectory('assets://css', '/.*/', ['callback' => $delete_stale]); } } diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php index 34bfaad71c7f049f7649a2c26271833d6c6c2bde..e11d66293b7d50061984f4b03f797dbc1b04e13b 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php @@ -122,7 +122,7 @@ public function optimize(array $css_assets, array $libraries) { if (!empty($css_asset['preprocessed'])) { $query = ['delta' => "$order"] + $query_args; $filename = 'css_' . $this->generateHash($css_asset) . '.css'; - $uri = 'public://css/' . $filename; + $uri = 'assets://css/' . $filename; $css_assets[$order]['data'] = $this->fileUrlGenerator->generateAbsoluteString($uri) . '?' . UrlHelper::buildQuery($query); } unset($css_assets[$order]['items']); @@ -153,8 +153,8 @@ public function deleteAll() { $this->fileSystem->delete($uri); } }; - if (is_dir('public://css')) { - $this->fileSystem->scanDirectory('public://css', '/.*/', ['callback' => $delete_stale]); + if (is_dir('assets://css')) { + $this->fileSystem->scanDirectory('assets://css', '/.*/', ['callback' => $delete_stale]); } } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index d208bee078a85bc1ec2db33c3407e61d0cd200f3..2d3597002cf8aae3096ba0705968d74e4adbb4e8 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -208,8 +208,8 @@ public function deleteAll() { $this->fileSystem->delete($uri); } }; - if (is_dir('public://js')) { - $this->fileSystem->scanDirectory('public://js', '/.*/', ['callback' => $delete_stale]); + if (is_dir('assets://js')) { + $this->fileSystem->scanDirectory('assets://js', '/.*/', ['callback' => $delete_stale]); } } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php index e2b31520356357d31bb15f17993f738260201d3e..b14fe621dad4a8daf536e309430bae7a680beaf4 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -134,7 +134,7 @@ public function optimize(array $js_assets, array $libraries) { 'delta' => "$order", ] + $query_args; $filename = 'js_' . $this->generateHash($js_asset) . '.js'; - $uri = 'public://js/' . $filename; + $uri = 'assets://js/' . $filename; $js_assets[$order]['data'] = $this->fileUrlGenerator->generateAbsoluteString($uri) . '?' . UrlHelper::buildQuery($query); } unset($js_assets[$order]['items']); @@ -165,8 +165,8 @@ public function deleteAll() { $this->fileSystem->delete($uri); } }; - if (is_dir('public://js')) { - $this->fileSystem->scanDirectory('public://js', '/.*/', ['callback' => $delete_stale]); + if (is_dir('assets://js')) { + $this->fileSystem->scanDirectory('assets://js', '/.*/', ['callback' => $delete_stale]); } } diff --git a/core/lib/Drupal/Core/File/HtaccessWriter.php b/core/lib/Drupal/Core/File/HtaccessWriter.php index af971e63629cf8c1503794111a4c300d319a2d43..bca152c45a33d830899146715a4a01d0f044fe5a 100644 --- a/core/lib/Drupal/Core/File/HtaccessWriter.php +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php @@ -107,6 +107,14 @@ public function defaultProtectedDirs() { $protected_dirs[] = new ProtectedDirectory('Private files directory', 'private://', TRUE); } $protected_dirs[] = new ProtectedDirectory('Temporary files directory', 'temporary://'); + + // The assets path may be the same as the public file path, if so don't try + // to write the same .htaccess twice. + $public_path = Settings::get('file_public_path', 'sites/default/files'); + $assets_path = Settings::get('file_assets_path', $public_path); + if ($assets_path !== $public_path) { + $protected_dirs[] = new ProtectedDirectory('Optimized assets directory', $assets_path); + } return $protected_dirs; } diff --git a/core/lib/Drupal/Core/StreamWrapper/AssetsStream.php b/core/lib/Drupal/Core/StreamWrapper/AssetsStream.php new file mode 100644 index 0000000000000000000000000000000000000000..9e29c0b8a934f53e14c32fc485f760dcfd1e63dd --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/AssetsStream.php @@ -0,0 +1,64 @@ +<?php + +namespace Drupal\Core\StreamWrapper; + +use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Defines a Drupal stream wrapper class for optimized assets (assets://). + * + * Provides support for storing publicly accessible optimized assets files + * with the Drupal file interface. + */ +class AssetsStream extends PublicStream { + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + public static function getType(): int { + return StreamWrapperInterface::LOCAL_HIDDEN; + } + + /** + * {@inheritdoc} + */ + public function getName(): string { + return $this->t('Optimized assets files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription(): string { + return $this->t('Public local optimized assets files served by the webserver.'); + } + + /** + * {@inheritdoc} + */ + public static function basePath($site_path = NULL): string { + return Settings::get( + 'file_assets_path', + Settings::get('file_public_path', 'sites/default/files') + ); + } + + /** + * {@inheritdoc} + */ + public static function baseUrl(): string { + $public_path = Settings::get('file_public_path', 'sites/default/files'); + $path = Settings::get('file_assets_path', $public_path); + if ($path === $public_path) { + $base_url = PublicStream::baseUrl(); + } + else { + $base_url = $GLOBALS['base_url'] . '/' . $path; + } + + return $base_url; + } + +} diff --git a/core/modules/system/src/Controller/AssetControllerBase.php b/core/modules/system/src/Controller/AssetControllerBase.php index 023bc031f74a86d29b7230e9ab4bf5e0e98ede41..e9a91c07ea60ceae431d1f9c7de9df91c3bdeb03 100644 --- a/core/modules/system/src/Controller/AssetControllerBase.php +++ b/core/modules/system/src/Controller/AssetControllerBase.php @@ -112,7 +112,7 @@ public function __construct( * supplied. */ public function deliver(Request $request, string $file_name) { - $uri = 'public://' . $this->assetType . '/' . $file_name; + $uri = 'assets://' . $this->assetType . '/' . $file_name; // Check to see whether a file matching the $uri already exists, this can // happen if it was created while this request was in progress. diff --git a/core/modules/system/src/Form/FileSystemForm.php b/core/modules/system/src/Form/FileSystemForm.php index 95006064419f3360acfdeaa248899fc58f93262f..18c1cb8ce160b0ddcbfb20c792adee6f2a0f1e80 100644 --- a/core/modules/system/src/Form/FileSystemForm.php +++ b/core/modules/system/src/Form/FileSystemForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StreamWrapper\AssetsStream; use Drupal\Core\StreamWrapper\PrivateStream; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Form\ConfigFormBase; @@ -105,6 +106,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => $this->t('The base URL that will be used for public file URLs. This can be changed in settings.php'), ]; + $form['file_assets_path'] = [ + '#type' => 'item', + '#title' => $this->t('Optimized assets file system path'), + '#markup' => AssetsStream::basePath(), + '#description' => $this->t('A local file system path where optimized assets files will be stored. This directory must exist and be writable by Drupal. This directory must be relative to the Drupal installation directory and be accessible over the web. This must be changed in settings.php'), + ]; + $form['file_private_path'] = [ '#type' => 'item', '#title' => $this->t('Private file system path'), diff --git a/core/modules/system/src/Form/PerformanceForm.php b/core/modules/system/src/Form/PerformanceForm.php index 9d28c4b8291060555a8c198995f261610c42a05c..c76aaf40181763d02d4c2b2029f7d3448ad883c1 100644 --- a/core/modules/system/src/Form/PerformanceForm.php +++ b/core/modules/system/src/Form/PerformanceForm.php @@ -138,12 +138,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#access' => !$this->moduleHandler->moduleExists('page_cache'), ]; - $directory = 'public://'; + $directory = 'assets://'; $is_writable = is_dir($directory) && is_writable($directory); $disabled = !$is_writable; $disabled_message = ''; if (!$is_writable) { - $disabled_message = ' ' . $this->t('<strong class="error">Set up the <a href=":file-system">public files directory</a> to make these optimizations available.</strong>', [':file-system' => Url::fromRoute('system.file_system_settings')->toString()]); + $disabled_message = ' ' . $this->t('<strong class="error">Set up the <a href=":file-system">optimized assets file system path</a> to make these optimizations available.</strong>', [':file-system' => Url::fromRoute('system.file_system_settings')->toString()]); } $form['bandwidth_optimization'] = [ diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php index b569824ec532b801c1891445c97e6e9282978eb5..8b3d31aa73f131d7148b0d2768466d699ade5b25 100644 --- a/core/modules/system/src/Routing/AssetRoutes.php +++ b/core/modules/system/src/Routing/AssetRoutes.php @@ -42,7 +42,7 @@ public function routes(): array { // Generate assets. If clean URLs are disabled image derivatives will always // be served through the routing system. If clean URLs are enabled and the // image derivative already exists, PHP will be bypassed. - $directory_path = $this->streamWrapperManager->getViaScheme('public')->getDirectoryPath(); + $directory_path = $this->streamWrapperManager->getViaScheme('assets')->getDirectoryPath(); $routes['system.css_asset'] = new Route( '/' . $directory_path . '/css/{file_name}', diff --git a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php index c59daabe74ad37a25fba31487da106685fd76d5b..48337977968d49604541e03667ebccef85fe84e4 100644 --- a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php +++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php @@ -19,6 +19,11 @@ class AssetOptimizationTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * The file assets path settings value. + */ + protected $fileAssetsPath; + /** * {@inheritdoc} */ @@ -28,6 +33,13 @@ class AssetOptimizationTest extends BrowserTestBase { * Tests that asset aggregates are rendered and created on disk. */ public function testAssetAggregation(): void { + $this->fileAssetsPath = $this->publicFilesDirectory . '/test-assets'; + $settings['settings']['file_assets_path'] = (object) [ + 'value' => $this->fileAssetsPath, + 'required' => TRUE, + ]; + $this->writeSettings($settings); + $this->rebuildAll(); $this->config('system.performance')->set('css', [ 'preprocess' => TRUE, 'gzip' => TRUE, @@ -88,6 +100,7 @@ public function testAssetAggregation(): void { */ protected function assertAggregate(string $url, bool $from_php = TRUE): void { $url = $this->getAbsoluteUrl($url); + $this->assertStringContainsString($this->fileAssetsPath, $url); $session = $this->getSession(); $session->visit($url); $this->assertSession()->statusCodeEquals(200); diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index c1db106c606e5aef46aa2c4212826d987dea2e56..5f88a44c164ee5513ff0fea4363a944af82129f2 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -487,6 +487,15 @@ # $settings['file_chmod_directory'] = 0775; # $settings['file_chmod_file'] = 0664; +/** + * Optimized assets path: + * + * A local file system path where optimized assets will be stored. This directory + * must exist and be writable by Drupal. This directory must be relative to + * the Drupal installation directory and be accessible over the web. + */ +# $settings['file_assets_path'] = 'sites/default/files'; + /** * Public file base URL: *