function AssetControllerBase::deliver

Same name in other branches
  1. 10 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.