class AssetControllerBase

Same name 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, LibraryDependencyResolverInterface $libraryDependencyResolver, AssetResolverInterface $assetResolver, ThemeInitializationInterface $themeInitialization, ThemeManagerInterface $themeManager, AssetCollectionGrouperInterface $grouper, AssetCollectionOptimizerInterface $optimizer, 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.