function AssetControllerBase::deliver

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

Generates an aggregate, given a filename.

Parameters

\Symfony\Component\HttpFoundation\Request $request: The request object.

string $file_name: The file to deliver.

Return value

\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.

File

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

Class

AssetControllerBase
Defines a controller to serve asset aggregates.

Namespace

Drupal\system\Controller

Code

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;
}

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