From d45cf927fced6b3171af2de240b448179536c6b4 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Tue, 11 Apr 2023 14:10:23 +0100
Subject: [PATCH] Issue #3027639 by catch, jonhattan, ankithashetty, Lal_,
 voleger, beunerd, cmlara, yogeshmpawar, Jose Reyero, drfuzetto, MiguelArber,
 smustgrave, longwave, Berdir, leandro713, Ambient.Impact, olli, amitaibu:
 Make css/js optimized assets path configurable

---
 .../scaffold/files/default.settings.php       |  9 +++
 core/core.services.yml                        |  5 ++
 core/lib/Drupal/Core/Asset/AssetDumper.php    |  4 +-
 .../Core/Asset/CssCollectionOptimizer.php     |  4 +-
 .../Core/Asset/CssCollectionOptimizerLazy.php |  6 +-
 .../Core/Asset/JsCollectionOptimizer.php      |  4 +-
 .../Core/Asset/JsCollectionOptimizerLazy.php  |  6 +-
 core/lib/Drupal/Core/File/HtaccessWriter.php  |  8 +++
 .../Core/StreamWrapper/AssetsStream.php       | 64 +++++++++++++++++++
 .../src/Controller/AssetControllerBase.php    |  2 +-
 .../system/src/Form/FileSystemForm.php        |  8 +++
 .../system/src/Form/PerformanceForm.php       |  4 +-
 .../system/src/Routing/AssetRoutes.php        |  2 +-
 .../Asset/AssetOptimizationTest.php           | 13 ++++
 sites/default/default.settings.php            |  9 +++
 15 files changed, 132 insertions(+), 16 deletions(-)
 create mode 100644 core/lib/Drupal/Core/StreamWrapper/AssetsStream.php

diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php
index c1db106c606e..5f88a44c164e 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 f91e5976f350..18b227280378 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 91360e5bc464..278aeadef634 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 ef5afa8eded0..ec3b805c971d 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 34bfaad71c7f..e11d66293b7d 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 d208bee078a8..2d3597002cf8 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 e2b315203563..b14fe621dad4 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 af971e63629c..bca152c45a33 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 000000000000..9e29c0b8a934
--- /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 023bc031f74a..e9a91c07ea60 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 95006064419f..18c1cb8ce160 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 9d28c4b82910..c76aaf401817 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 b569824ec532..8b3d31aa73f1 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 c59daabe74ad..48337977968d 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 c1db106c606e..5f88a44c164e 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:
  *
-- 
GitLab