class AssetControllerBase

Same name and namespace in other branches
  1. 11.x core/modules/system/src/Controller/AssetControllerBase.php \Drupal\system\Controller\AssetControllerBase

Defines a controller to serve asset aggregates.

Hierarchy

Expanded class hierarchy of AssetControllerBase

File

core/modules/system/src/Controller/AssetControllerBase.php, line 27

Namespace

Drupal\system\Controller
View source
abstract class AssetControllerBase extends FileDownloadController {
  use AssetGroupSetHashTrait;
  
  /**
   * The asset type.
   *
   * @var string
   */
  protected string $assetType;
  
  /**
   * The aggregate file extension.
   *
   * @var string
   */
  protected string $fileExtension;
  
  /**
   * The asset aggregate content type to send as Content-Type header.
   *
   * @var string
   */
  protected string $contentType;
  
  /**
   * The cache control header to use.
   *
   * Headers sent from PHP can never perfectly match those sent when the
   * file is served by the filesystem, so ensure this request does not get
   * cached in either the browser or reverse proxies. Subsequent requests
   * for the file will be served from disk and be cached. This is done to
   * avoid situations such as where one CDN endpoint is serving a version
   * cached from PHP, while another is serving a version cached from disk.
   * Should there be any discrepancy in behavior between those files, this
   * can make debugging very difficult.
   */
  protected const CACHE_CONTROL = 'private, no-store';
  
  /**
   * Constructs an object derived from AssetControllerBase.
   *
   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
   *   The stream wrapper manager.
   * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $libraryDependencyResolver
   *   The library dependency resolver.
   * @param \Drupal\Core\Asset\AssetResolverInterface $assetResolver
   *   The asset resolver.
   * @param \Drupal\Core\Theme\ThemeInitializationInterface $themeInitialization
   *   The theme initializer.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
   *   The theme manager.
   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
   *   The asset grouper.
   * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $optimizer
   *   The asset collection optimizer.
   * @param \Drupal\Core\Asset\AssetDumperUriInterface $dumper
   *   The asset dumper.
   */
  public function __construct(StreamWrapperManagerInterface $streamWrapperManager, protected readonly LibraryDependencyResolverInterface $libraryDependencyResolver, protected readonly AssetResolverInterface $assetResolver, protected readonly ThemeInitializationInterface $themeInitialization, protected readonly ThemeManagerInterface $themeManager, protected readonly AssetCollectionGrouperInterface $grouper, protected readonly AssetCollectionOptimizerInterface $optimizer, protected readonly AssetDumperUriInterface $dumper) {
    parent::__construct($streamWrapperManager);
    $this->fileExtension = $this->assetType;
  }
  
  /**
   * Generates an aggregate, given a filename.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param string $file_name
   *   The file to deliver.
   *
   * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
   *   The transferred file as response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the filename is invalid or an invalid query argument is
   *   supplied.
   */
  public function deliver(Request $request, string $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.
    if (file_exists($uri)) {
      return new BinaryFileResponse($uri, 200, [
        'Cache-control' => static::CACHE_CONTROL,
      ]);
    }
    // First validate that the request is valid enough to produce an asset group
    // aggregate. The theme must be passed as a query parameter, since assets
    // always depend on the current theme.
    if (!$request->query
      ->has('theme')) {
      throw new BadRequestHttpException('The theme must be passed as a query argument');
    }
    if (!$request->query
      ->has('delta') || !is_numeric($request->query
      ->get('delta'))) {
      throw new BadRequestHttpException('The numeric delta must be passed as a query argument');
    }
    if (!$request->query
      ->has('language')) {
      throw new BadRequestHttpException('The language must be passed as a query argument');
    }
    if (!$request->query
      ->has('include')) {
      throw new BadRequestHttpException('The libraries to include must be passed as a query argument');
    }
    $file_parts = explode('_', basename($file_name, '.' . $this->fileExtension), 2);
    // Ensure the filename is correctly prefixed.
    if ($file_parts[0] !== $this->fileExtension) {
      throw new BadRequestHttpException('The filename prefix must match the file extension');
    }
    // The hash is the second segment of the filename.
    if (!isset($file_parts[1])) {
      throw new BadRequestHttpException('Invalid filename');
    }
    $received_hash = $file_parts[1];
    // Now build the asset groups based on the libraries.  It requires the full
    // set of asset groups to extract and build the aggregate for the group we
    // want, since libraries may be split across different asset groups.
    $theme = $request->query
      ->get('theme');
    $active_theme = $this->themeInitialization
      ->initTheme($theme);
    $this->themeManager
      ->setActiveTheme($active_theme);
    $attached_assets = new AttachedAssets();
    $include_libraries = explode(',', UrlHelper::uncompressQueryParameter($request->query
      ->get('include')));
    // Check that library names are in the correct format.
    $validate = function ($libraries_to_check) {
      foreach ($libraries_to_check as $library) {
        if (substr_count($library, '/') === 0) {
          throw new BadRequestHttpException(sprintf('The "%s" library name must include at least one slash.', $library));
        }
      }
    };
    $validate($include_libraries);
    $attached_assets->setLibraries($include_libraries);
    if ($request->query
      ->has('exclude')) {
      $exclude_libraries = explode(',', UrlHelper::uncompressQueryParameter($request->query
        ->get('exclude')));
      $validate($exclude_libraries);
      $attached_assets->setAlreadyLoadedLibraries($exclude_libraries);
    }
    $groups = $this->getGroups($attached_assets, $request);
    $group = $this->getGroup($groups, $request->query
      ->get('delta'));
    // Generate a hash based on the asset group, this uses the same method as
    // the collection optimizer does to create the filename, so it should match.
    $generated_hash = $this->generateHash($group);
    $data = $this->optimizer
      ->optimizeGroup($group);
    $response = new Response($data, 200, [
      'Cache-control' => static::CACHE_CONTROL,
      'Content-Type' => $this->contentType,
    ]);
    // However, the hash from the library definitions in code may not match the
    // hash from the URL. This can be for three reasons:
    // 1. Someone has requested an outdated URL, i.e. from a cached page, which
    // matches a different version of the code base.
    // 2. Someone has requested an outdated URL during a deployment. This is
    // the same case as #1 but a much shorter window.
    // 3. Someone is attempting to craft an invalid URL in order to conduct a
    // denial of service attack on the site.
    // Dump the optimized group into an aggregate file, but only if the
    // received hash and generated hash match. This prevents invalid filenames
    // from filling the disk, while still serving aggregates that may be
    // referenced in cached HTML.
    if (hash_equals($generated_hash, $received_hash)) {
      $this->dumper
        ->dumpToUri($data, $this->assetType, $uri);
    }
    else {
      $expected_filename = $this->fileExtension . '_' . $generated_hash . '.' . $this->fileExtension;
      $response = new RedirectResponse(str_replace($file_name, $expected_filename, $request->getRequestUri()), 301, [
        'Cache-Control' => 'public, max-age=3600, must-revalidate',
      ]);
    }
    return $response;
  }
  
  /**
   * Gets a group.
   *
   * @param array $groups
   *   An array of asset groups.
   * @param int $group_delta
   *   The group delta.
   *
   * @return array
   *   The correct asset group matching $group_delta.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the filename is invalid.
   */
  protected function getGroup(array $groups, int $group_delta) : array {
    if (isset($groups[$group_delta])) {
      return $groups[$group_delta];
    }
    throw new BadRequestHttpException('Invalid filename.');
  }
  
  /**
   * Get grouped assets.
   *
   * @param \Drupal\Core\Asset\AttachedAssetsInterface $attached_assets
   *   The attached assets.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return array
   *   The grouped assets.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the query argument is omitted.
   */
  protected abstract function getGroups(AttachedAssetsInterface $attached_assets, Request $request) : array;

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
AssetControllerBase::$assetType protected property The asset type. 2
AssetControllerBase::$contentType protected property The asset aggregate content type to send as Content-Type header. 2
AssetControllerBase::$fileExtension protected property The aggregate file extension.
AssetControllerBase::CACHE_CONTROL protected constant The cache control header to use.
AssetControllerBase::deliver public function Generates an aggregate, given a filename.
AssetControllerBase::getGroup protected function Gets a group.
AssetControllerBase::getGroups abstract protected function Get grouped assets. 2
AssetControllerBase::__construct public function Constructs an object derived from AssetControllerBase. Overrides FileDownloadController::__construct
AssetGroupSetHashTrait::generateHash protected function Generates a hash for an array of asset groups.
AutowireTrait::create public static function Instantiates a new instance of the implementing class using autowiring. 33
ControllerBase::$configFactory protected property The configuration factory.
ControllerBase::$currentUser protected property The current user service. 2
ControllerBase::$entityFormBuilder protected property The entity form builder.
ControllerBase::$entityTypeManager protected property The entity type manager.
ControllerBase::$formBuilder protected property The form builder. 1
ControllerBase::$keyValue protected property The key-value storage. 1
ControllerBase::$languageManager protected property The language manager. 1
ControllerBase::$moduleHandler protected property The module handler. 1
ControllerBase::$stateService protected property The state service.
ControllerBase::cache protected function Returns the requested cache bin.
ControllerBase::config protected function Retrieves a configuration object.
ControllerBase::container private function Returns the service container.
ControllerBase::currentUser protected function Returns the current user. 2
ControllerBase::entityFormBuilder protected function Retrieves the entity form builder.
ControllerBase::entityTypeManager protected function Retrieves the entity type manager.
ControllerBase::formBuilder protected function Returns the form builder service. 1
ControllerBase::keyValue protected function Returns a key/value storage collection. 1
ControllerBase::languageManager protected function Returns the language manager service. 1
ControllerBase::moduleHandler protected function Returns the module handler. 1
ControllerBase::redirect protected function Returns a redirect response object for the specified route.
ControllerBase::state protected function Returns the state storage service.
FileDownloadController::$streamWrapperManager protected property The stream wrapper manager.
FileDownloadController::download public function Handles private file transfers.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 16
MessengerTrait::messenger public function Gets the messenger. 16
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 2
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.