class AssetControllerBase
Same name in other branches
- 10 core/modules/system/src/Controller/AssetControllerBase.php \Drupal\system\Controller\AssetControllerBase
Defines a controller to serve asset aggregates.
Hierarchy
- class \Drupal\Core\Controller\ControllerBase implements \Drupal\Core\DependencyInjection\ContainerInjectionInterface uses \Drupal\Core\DependencyInjection\AutowireTrait, \Drupal\Core\Logger\LoggerChannelTrait, \Drupal\Core\Messenger\MessengerTrait, \Drupal\Core\Routing\RedirectDestinationTrait, \Drupal\Core\StringTranslation\StringTranslationTrait
- class \Drupal\system\FileDownloadController extends \Drupal\Core\Controller\ControllerBase
- class \Drupal\system\Controller\AssetControllerBase extends \Drupal\system\FileDownloadController uses \Drupal\Core\Asset\AssetGroupSetHashTrait
- class \Drupal\system\FileDownloadController extends \Drupal\Core\Controller\ControllerBase
Expanded class hierarchy of AssetControllerBase
File
-
core/
modules/ system/ src/ Controller/ AssetControllerBase.php, line 27
Namespace
Drupal\system\ControllerView 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.