Verified Commit c794b4c2 authored by Dave Long's avatar Dave Long
Browse files

feat: #1308152 Add stream wrappers to access .json files in extensions

By: Crell
By: brianV
By: chx
By: catch
By: xjm
By: pounard
By: fietserwin
By: jthorson
By: sdboyer
By: pancho
By: ParisLiakos
By: effulgentsia
By: pwolanin
By: jcisio
By: tstoeckler
By: disasm
By: almaudoh
By: arla
By: neclimdul
By: slashrsm
By: garphy
By: jeroent
By: alexpott
By: mgifford
By: mbovan
By: joelpittet
By: benjy
By: deviantintegral
By: juampynr
By: dawehner
By: heddn
By: vijaycs85
By: aspilicious
By: borisson_
By: pingwin4eg
By: hanoii
By: aaronmchale
By: hardik_patel_12
By: adamzimmermann
By: ricovandevin
By: bradjones1
By: berdir
By: mondrake
By: clayfreeman
By: trackleft2
By: rpayanm
By: dave reid
By: lars toomre
By: wim leers
By: claudiu.cristea
By: pdureau
By: nicxvan
By: phenaproxima
By: larowlan
By: godotislate
(cherry picked from commit d9b38e91f7fb47ce5eff08e3de2be5a8e994cacb)
parent fd6dd469
Loading
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -73,6 +73,11 @@ parameters:
  tempstore.expire: 604800
  queue.config:
    suspendMaximumWait: 30.0
  stream_wrapper.allowed_file_extensions:
    module:
      - json
    theme:
      - json
services:
  _defaults:
    autoconfigure: true
@@ -1553,6 +1558,14 @@ services:
    class: Drupal\Core\StreamWrapper\TemporaryStream
    tags:
      - { name: stream_wrapper, scheme: temporary }
  stream_wrapper.module:
    class: Drupal\Core\StreamWrapper\ModuleStream
    tags:
      - { name: stream_wrapper, scheme: module }
  stream_wrapper.theme:
    class: Drupal\Core\StreamWrapper\ThemeStream
    tags:
      - { name: stream_wrapper, scheme: theme }
  image.toolkit.manager:
    class: Drupal\Core\ImageToolkit\ImageToolkitManager
    arguments: ['@config.factory']
+138 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\StreamWrapper;

use Drupal\Core\Extension\Extension;
use Drupal\Core\Routing\RequestContext;

/**
 * Defines a base stream wrapper implementation for extension assets.
 */
abstract class ExtensionStreamBase extends LocalReadOnlyStream {

  /**
   * {@inheritdoc}
   */
  public static function getType(): int {
    return StreamWrapperInterface::LOCAL | StreamWrapperInterface::READ;
  }

  /**
   * {@inheritdoc}
   */
  public function setUri($uri): void {
    if (!str_contains($uri, '://')) {
      throw new \InvalidArgumentException("Malformed extension URI: {$uri}");
    }
    $this->checkFileExtension($uri);
    $this->uri = $uri;
  }

  /**
   * Gets the extension name from the URI.
   *
   * @return string
   *   The extension name.
   */
  protected function getExtensionName(): string {
    $uri_parts = explode('://', $this->uri, 2);
    $extension_name = strtok($uri_parts[1], '/');
    // Any string that evaluates to empty is considered an invalid extension
    // name.
    if (empty($extension_name)) {
      throw new \RuntimeException("Unable to determine the extension name.");
    }
    return $extension_name;
  }

  /**
   * Gets the extension object.
   *
   * @param string $extension_name
   *   The extension name.
   *
   * @return \Drupal\Core\Extension\Extension
   *   The extension object.
   *
   * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
   *   Thrown when the extension is missing.
   */
  abstract protected function getExtension(string $extension_name): Extension;

  /**
   * {@inheritdoc}
   */
  protected function getTarget($uri = NULL): string {
    if ($target = strstr(parent::getTarget($uri), '/')) {
      $this->checkFileExtension($uri ?? $this->uri);
      return trim($target, '/');
    }
    return '';
  }

  /**
   * {@inheritdoc}
   */
  public function getExternalUrl(): string {
    $dir = $this->getDirectoryPath();
    return \Drupal::service(RequestContext::class)->getCompleteBaseUrl() . rtrim("/$dir/" . $this->getTarget(), '/');
  }

  /**
   * {@inheritdoc}
   */
  public function dirname($uri = NULL): string {
    if (isset($uri)) {
      $this->setUri($uri);
    }
    else {
      $uri = $this->uri;
    }
    [$scheme] = explode('://', $uri, 2);
    $dirname = dirname($this->getTarget($uri));
    $dirname = $dirname !== '.' ? rtrim("/$dirname", '/') : '';

    // Call the getExtension() method to ensure the extension exists.
    $extension = $this->getExtension($this->getExtensionName());
    return "$scheme://{$extension->getName()}{$dirname}";
  }

  /**
   * {@inheritdoc}
   */
  public function getDirectoryPath() {
    $extension_name = $this->getExtensionName();
    return $this->getExtension($extension_name)->getPath();
  }

  /**
   * Checks that the given URI has an allowed file extension.
   *
   * This checks the `stream_wrapper.allowed_file_extensions` container
   * parameter, which lists all file extensions allowed for different URI
   * schemes. If there is no list for the given scheme, then the file is assumed
   * to be disallowed.
   *
   * @param string $uri
   *   A URI to check.
   *
   * @throws \InvalidArgumentException
   *   Thrown if the given URI has a file extension that is not allowed by the
   *   container parameter.
   */
  protected function checkFileExtension(string $uri): void {
    [$scheme] = explode('://', $uri, 2);

    $allowed = \Drupal::getContainer()
      ->getParameter('stream_wrapper.allowed_file_extensions');

    $extension = pathinfo($uri, PATHINFO_EXTENSION);
    if (isset($allowed[$scheme]) && in_array(strtolower($extension), $allowed[$scheme], TRUE)) {
      return;
    }
    throw new \InvalidArgumentException("The $scheme stream wrapper does not support the '$extension' file type.");
  }

}
+44 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\StreamWrapper;

use Drupal\Core\Extension\Extension;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Defines the read-only module:// stream wrapper for module files.
 *
 * Only enabled modules are supported.
 *
 * Example usage:
 * @code
 * module://my_module/css/component.css
 * @endcode
 * Points to the component.css file in the module my_module's css directory.
 */
final class ModuleStream extends ExtensionStreamBase {

  /**
   * {@inheritdoc}
   */
  public function getName(): TranslatableMarkup {
    return new TranslatableMarkup('Module files');
  }

  /**
   * {@inheritdoc}
   */
  public function getDescription(): TranslatableMarkup {
    return new TranslatableMarkup("Local files stored under a module's directory.");
  }

  /**
   * {@inheritdoc}
   */
  protected function getExtension(string $extension_name): Extension {
    return \Drupal::moduleHandler()->getModule($extension_name);
  }

}
+44 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\StreamWrapper;

use Drupal\Core\Extension\Extension;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Defines the read-only theme:// stream wrapper for theme files.
 *
 * Only enabled themes are supported.
 *
 * Example Usage:
 * @code
 * theme://my_theme/css/component.css
 * @endcode
 * Points to the component.css file in the theme my_theme's css directory.
 */
final class ThemeStream extends ExtensionStreamBase {

  /**
   * {@inheritdoc}
   */
  public function getName(): TranslatableMarkup {
    return new TranslatableMarkup('Theme files');
  }

  /**
   * {@inheritdoc}
   */
  public function getDescription(): TranslatableMarkup {
    return new TranslatableMarkup("Local files stored under a theme's directory.");
  }

  /**
   * {@inheritdoc}
   */
  protected function getExtension(string $extension_name): Extension {
    return \Drupal::service('theme_handler')->getTheme($extension_name);
  }

}
+1 −0
Original line number Diff line number Diff line
@@ -332,6 +332,7 @@ system.files:
    scheme: private
  requirements:
    _access: 'TRUE'
    scheme: '^(?!(module|theme)).*'

system.private_file_download:
  path: '/system/files/{filepath}'
Loading