Issue #1987712 by dawehner, claudiu.cristea, damiankloip, andypost: Convert file_download() to a new style controller.
parent
ad137531c9
commit
36d647e61b
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
use Drupal\Core\StreamWrapper\LocalStream;
|
use Drupal\Core\StreamWrapper\LocalStream;
|
||||||
use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
|
use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream wrapper bit flags that are the basis for composite types.
|
* Stream wrapper bit flags that are the basis for composite types.
|
||||||
|
@ -1331,42 +1328,6 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
|
||||||
return file_unmanaged_move($temp_name, $destination, $replace);
|
return file_unmanaged_move($temp_name, $destination, $replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Page callback: Handles private file transfers.
|
|
||||||
*
|
|
||||||
* Call modules that implement hook_file_download() to find out if a file is
|
|
||||||
* accessible and what headers it should be transferred with. If one or more
|
|
||||||
* modules returned headers the download will start with the returned headers.
|
|
||||||
* If a module returns -1 an AccessDeniedHttpException will be thrown.
|
|
||||||
* If the file exists but no modules responded an AccessDeniedHttpException will
|
|
||||||
* be thrown.If the file does not exist a NotFoundHttpException will be thrown.
|
|
||||||
*
|
|
||||||
* @see hook_file_download()
|
|
||||||
* @see system_menu()
|
|
||||||
*/
|
|
||||||
function file_download() {
|
|
||||||
// Merge remaining path arguments into relative file path.
|
|
||||||
$args = func_get_args();
|
|
||||||
$scheme = array_shift($args);
|
|
||||||
$target = implode('/', $args);
|
|
||||||
$uri = $scheme . '://' . $target;
|
|
||||||
if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
|
|
||||||
// Let other modules provide headers and controls access to the file.
|
|
||||||
$headers = module_invoke_all('file_download', $uri);
|
|
||||||
foreach ($headers as $result) {
|
|
||||||
if ($result == -1) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (count($headers)) {
|
|
||||||
return new BinaryFileResponse($uri, 200, $headers);
|
|
||||||
}
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
throw new NotFoundHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all files that match a given mask in a given directory.
|
* Finds all files that match a given mask in a given directory.
|
||||||
*
|
*
|
||||||
|
|
|
@ -10,9 +10,10 @@ namespace Drupal\config\Controller;
|
||||||
use Drupal\Core\Controller\ControllerInterface;
|
use Drupal\Core\Controller\ControllerInterface;
|
||||||
use Drupal\Core\Config\StorageInterface;
|
use Drupal\Core\Config\StorageInterface;
|
||||||
use Drupal\Component\Archiver\ArchiveTar;
|
use Drupal\Component\Archiver\ArchiveTar;
|
||||||
use Drupal\Core\Ajax\AjaxResponse;
|
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||||
use Drupal\Core\Ajax\OpenModalDialogCommand;
|
use Drupal\system\FileDownloadController;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns responses for config module routes.
|
* Returns responses for config module routes.
|
||||||
|
@ -22,22 +23,33 @@ class ConfigController implements ControllerInterface {
|
||||||
/**
|
/**
|
||||||
* The target storage.
|
* The target storage.
|
||||||
*
|
*
|
||||||
* @var \Drupal\Core\Config\StorageInterface;
|
* @var \Drupal\Core\Config\StorageInterface
|
||||||
*/
|
*/
|
||||||
protected $targetStorage;
|
protected $targetStorage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The source storage.
|
* The source storage.
|
||||||
*
|
*
|
||||||
* @var \Drupal\Core\Config\StorageInterface;
|
* @var \Drupal\Core\Config\StorageInterface
|
||||||
*/
|
*/
|
||||||
protected $sourceStorage;
|
protected $sourceStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file download controller.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Controller\ControllerInterface
|
||||||
|
*/
|
||||||
|
protected $fileDownloadController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public static function create(ContainerInterface $container) {
|
public static function create(ContainerInterface $container) {
|
||||||
return new static($container->get('config.storage'), $container->get('config.storage.staging'));
|
return new static(
|
||||||
|
$container->get('config.storage'),
|
||||||
|
$container->get('config.storage.staging'),
|
||||||
|
FileDownloadController::create($container)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,10 +59,13 @@ class ConfigController implements ControllerInterface {
|
||||||
* The target storage.
|
* The target storage.
|
||||||
* @param \Drupal\Core\Config\StorageInterface $source_storage
|
* @param \Drupal\Core\Config\StorageInterface $source_storage
|
||||||
* The source storage
|
* The source storage
|
||||||
|
* @param \Drupal\Core\Controller\ControllerInterface $file_download_controller
|
||||||
|
* The file download controller.
|
||||||
*/
|
*/
|
||||||
public function __construct(StorageInterface $target_storage, StorageInterface $source_storage) {
|
public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ControllerInterface $file_download_controller) {
|
||||||
$this->targetStorage = $target_storage;
|
$this->targetStorage = $target_storage;
|
||||||
$this->sourceStorage = $source_storage;
|
$this->sourceStorage = $source_storage;
|
||||||
|
$this->fileDownloadController = $file_download_controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,7 +79,9 @@ class ConfigController implements ControllerInterface {
|
||||||
$config_files[] = $config_dir . '/' . $config_name . '.yml';
|
$config_files[] = $config_dir . '/' . $config_name . '.yml';
|
||||||
}
|
}
|
||||||
$archiver->createModify($config_files, '', config_get_config_directory());
|
$archiver->createModify($config_files, '', config_get_config_directory());
|
||||||
return file_download('temporary', 'config.tar.gz');
|
|
||||||
|
$request = new Request(array('file' => 'config.tar.gz'));
|
||||||
|
return $this->fileDownloadController->download($request, 'temporary');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1578,7 +1578,7 @@ function file_get_file_references(File $file, $field = NULL, $age = FIELD_LOAD_R
|
||||||
// for every revision or the entity does not support revisions then
|
// for every revision or the entity does not support revisions then
|
||||||
// every usage is already a match.
|
// every usage is already a match.
|
||||||
$match_entity_type = $age == FIELD_LOAD_REVISION || !isset($entity_info['entity_keys']['revision']);
|
$match_entity_type = $age == FIELD_LOAD_REVISION || !isset($entity_info['entity_keys']['revision']);
|
||||||
$entities = entity_load_multiple($entity_type, $entity_ids);
|
$entities = entity_load_multiple($entity_type, array_keys($entity_ids));
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
$bundle = $entity->bundle();
|
$bundle = $entity->bundle();
|
||||||
// We need to find file fields for this entity type and bundle.
|
// We need to find file fields for this entity type and bundle.
|
||||||
|
|
|
@ -95,28 +95,6 @@ function image_style_entity_uri(ImageStyle $style) {
|
||||||
function image_menu() {
|
function image_menu() {
|
||||||
$items = array();
|
$items = array();
|
||||||
|
|
||||||
// Generate image derivatives of publicly available files.
|
|
||||||
// If clean URLs are disabled, image derivatives will always be served
|
|
||||||
// through the menu system.
|
|
||||||
// If clean URLs are enabled and the image derivative already exists,
|
|
||||||
// PHP will be bypassed.
|
|
||||||
$directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath();
|
|
||||||
$items[$directory_path . '/styles/%image_style'] = array(
|
|
||||||
'title' => 'Generate image style',
|
|
||||||
'page callback' => 'image_style_deliver',
|
|
||||||
'page arguments' => array(count(explode('/', $directory_path)) + 1),
|
|
||||||
'access callback' => TRUE,
|
|
||||||
'type' => MENU_CALLBACK,
|
|
||||||
);
|
|
||||||
// Generate and deliver image derivatives of private files.
|
|
||||||
// These image derivatives are always delivered through the menu system.
|
|
||||||
$items['system/files/styles/%image_style'] = array(
|
|
||||||
'title' => 'Generate image style',
|
|
||||||
'page callback' => 'image_style_deliver',
|
|
||||||
'page arguments' => array(3),
|
|
||||||
'access callback' => TRUE,
|
|
||||||
'type' => MENU_CALLBACK,
|
|
||||||
);
|
|
||||||
$items['admin/config/media/image-styles'] = array(
|
$items['admin/config/media/image-styles'] = array(
|
||||||
'title' => 'Image styles',
|
'title' => 'Image styles',
|
||||||
'description' => 'Configure styles that can be used for resizing or adjusting images on display.',
|
'description' => 'Configure styles that can be used for resizing or adjusting images on display.',
|
||||||
|
@ -391,30 +369,6 @@ function image_style_options($include_empty = TRUE) {
|
||||||
return $options;
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Page callback: Generates a derivative, given a style and image path.
|
|
||||||
*
|
|
||||||
* After generating an image, transfer it to the requesting agent.
|
|
||||||
*
|
|
||||||
* @param \Drupal\image\ImageStyleInterface $style
|
|
||||||
* The image style.
|
|
||||||
* @param string $scheme
|
|
||||||
* The scheme name of the original image file stream wrapper ('public',
|
|
||||||
* 'private', 'temporary', etc.).
|
|
||||||
*
|
|
||||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
|
|
||||||
* The image to be delivered.
|
|
||||||
*
|
|
||||||
* @todo Remove this wrapper in https://drupal.org/node/1987712.
|
|
||||||
*/
|
|
||||||
function image_style_deliver(ImageStyleInterface $style, $scheme) {
|
|
||||||
$args = func_get_args();
|
|
||||||
// Remove $style and $scheme from the arguments.
|
|
||||||
unset($args[0], $args[1]);
|
|
||||||
$target = implode('/', $args);
|
|
||||||
return $style->deliver($scheme, $target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a set of image effects.
|
* Returns a set of image effects.
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,3 +11,11 @@ image_effect_delete:
|
||||||
_form: '\Drupal\image\Form\ImageEffectDeleteForm'
|
_form: '\Drupal\image\Form\ImageEffectDeleteForm'
|
||||||
requirements:
|
requirements:
|
||||||
_permission: 'administer image styles'
|
_permission: 'administer image styles'
|
||||||
|
|
||||||
|
image_style_private:
|
||||||
|
pattern: '/system/files/styles/{image_style}/{scheme}'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver'
|
||||||
|
requirements:
|
||||||
|
_access: 'TRUE'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
image.route_subscriber:
|
||||||
|
class: Drupal\image\EventSubscriber\RouteSubscriber
|
||||||
|
tags:
|
||||||
|
- { name: 'event_subscriber' }
|
||||||
|
path_processor.image_styles:
|
||||||
|
class: Drupal\image\PathProcessor\PathProcessorImageStyles
|
||||||
|
tags:
|
||||||
|
- { name: path_processor_inbound, priority: 300 }
|
|
@ -0,0 +1,176 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\image\Controller\ImageStyleDownloadController.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\image\Controller;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Crypt;
|
||||||
|
use Drupal\Core\Config\ConfigFactory;
|
||||||
|
use Drupal\Core\Controller\ControllerInterface;
|
||||||
|
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||||
|
use Drupal\Core\Lock\LockBackendInterface;
|
||||||
|
use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
|
||||||
|
use Drupal\image\ImageStyleInterface;
|
||||||
|
use Drupal\system\FileDownloadController;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a controller to serve image styles.
|
||||||
|
*/
|
||||||
|
class ImageStyleDownloadController extends FileDownloadController implements ControllerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config factory.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\config\ConfigFactory
|
||||||
|
*/
|
||||||
|
protected $configFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock backend.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Lock\LockBackendInterface
|
||||||
|
*/
|
||||||
|
protected $lock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translator service.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\StringTranslation\Translator\TranslatorInterface
|
||||||
|
*/
|
||||||
|
protected $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a ImageStyleDownloadController object.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||||
|
* The module handler.
|
||||||
|
* @param \Drupal\Core\Config\ConfigFactory $config_factory
|
||||||
|
* The config factory.
|
||||||
|
* @param \Drupal\Core\Lock\LockBackendInterface $lock
|
||||||
|
* The lock backend.
|
||||||
|
* @param \Drupal\Core\StringTranslation\Translator\TranslatorInterface $translator
|
||||||
|
* The translator service.
|
||||||
|
*/
|
||||||
|
public function __construct(ModuleHandlerInterface $module_handler, ConfigFactory $config_factory, LockBackendInterface $lock, TranslatorInterface $translator) {
|
||||||
|
parent::__construct($module_handler);
|
||||||
|
$this->configFactory = $config_factory;
|
||||||
|
$this->lock = $lock;
|
||||||
|
$this->translator = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function create(ContainerInterface $container) {
|
||||||
|
return new static(
|
||||||
|
$container->get('module_handler'),
|
||||||
|
$container->get('config.factory'),
|
||||||
|
$container->get('lock'),
|
||||||
|
$container->get('string_translation')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a derivative, given a style and image path.
|
||||||
|
*
|
||||||
|
* After generating an image, transfer it to the requesting agent.
|
||||||
|
*
|
||||||
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||||
|
* The request object.
|
||||||
|
* @param string $scheme
|
||||||
|
* The file scheme, defaults to 'private'.
|
||||||
|
* @param \Drupal\image\ImageStyleInterface $image_style
|
||||||
|
* The image style to deliver.
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||||
|
* Thrown when the user does not have access to the file.
|
||||||
|
*
|
||||||
|
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
|
||||||
|
* The transferred file as response or some error response.
|
||||||
|
*/
|
||||||
|
public function deliver(Request $request, $scheme, ImageStyleInterface $image_style) {
|
||||||
|
$target = $request->query->get('file');
|
||||||
|
$image_uri = $scheme . '://' . $target;
|
||||||
|
|
||||||
|
// Check that the style is defined, the scheme is valid, and the image
|
||||||
|
// derivative token is valid. Sites which require image derivatives to be
|
||||||
|
// generated without a token can set the
|
||||||
|
// 'image.settings:allow_insecure_derivatives' configuration to TRUE to
|
||||||
|
// bypass the latter check, but this will increase the site's vulnerability
|
||||||
|
// to denial-of-service attacks.
|
||||||
|
$valid = !empty($image_style) && file_stream_wrapper_valid_scheme($scheme);
|
||||||
|
if (!$this->configFactory->get('image.settings')->get('allow_insecure_derivatives')) {
|
||||||
|
$valid &= $request->query->get(IMAGE_DERIVATIVE_TOKEN) === $image_style->getPathToken($image_uri);
|
||||||
|
}
|
||||||
|
if (!$valid) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$derivative_uri = $image_style->buildUri($image_uri);
|
||||||
|
$headers = array();
|
||||||
|
|
||||||
|
// If using the private scheme, let other modules provide headers and
|
||||||
|
// control access to the file.
|
||||||
|
if ($scheme == 'private') {
|
||||||
|
if (file_exists($derivative_uri)) {
|
||||||
|
return parent::download($request, $scheme);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$headers = $this->moduleHandler->invokeAll('file_download', array($image_uri));
|
||||||
|
if (in_array(-1, $headers) || empty($headers)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't try to generate file if source is missing.
|
||||||
|
if (!file_exists($image_uri)) {
|
||||||
|
watchdog('image', 'Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri));
|
||||||
|
return new Response($this->translator->translate('Error generating image, missing source file.'), 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't start generating the image if the derivative already exists or if
|
||||||
|
// generation is in progress in another thread.
|
||||||
|
$lock_name = 'image_style_deliver:' . $image_style->id() . ':' . Crypt::hashBase64($image_uri);
|
||||||
|
if (!file_exists($derivative_uri)) {
|
||||||
|
$lock_acquired = $this->lock->acquire($lock_name);
|
||||||
|
if (!$lock_acquired) {
|
||||||
|
// Tell client to retry again in 3 seconds. Currently no browsers are
|
||||||
|
// known to support Retry-After.
|
||||||
|
throw new ServiceUnavailableHttpException(3, $this->translator->translate('Image generation in progress. Try again shortly.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate the image, unless another thread just did it while we
|
||||||
|
// were acquiring the lock.
|
||||||
|
$success = file_exists($derivative_uri) || $image_style->createDerivative($image_uri, $derivative_uri);
|
||||||
|
|
||||||
|
if (!empty($lock_acquired)) {
|
||||||
|
$this->lock->release($lock_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$image = image_load($derivative_uri);
|
||||||
|
$uri = $image->source;
|
||||||
|
$headers += array(
|
||||||
|
'Content-Type' => $image->info['mime_type'],
|
||||||
|
'Content-Length' => $image->info['file_size'],
|
||||||
|
);
|
||||||
|
return new BinaryFileResponse($uri, 200, $headers);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri));
|
||||||
|
return new Response($this->translator->translate('Error generating image.'), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\image\EventSubscriber\RouteSubscriber.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\image\EventSubscriber;
|
||||||
|
|
||||||
|
use Drupal\Core\Routing\RouteBuildEvent;
|
||||||
|
use Drupal\Core\Routing\RoutingEvents;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Routing\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a route subscriber to register a url for serving image styles.
|
||||||
|
*/
|
||||||
|
class RouteSubscriber implements EventSubscriberInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function getSubscribedEvents() {
|
||||||
|
$events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers dynamic routes for image styles.
|
||||||
|
*
|
||||||
|
* Generate image derivatives of publicly available files. If clean URLs are
|
||||||
|
* disabled, image derivatives will always be served through the menu system.
|
||||||
|
* If clean URLs are enabled and the image derivative already exists, PHP will
|
||||||
|
* be bypassed.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Routing\RouteBuildEvent $event
|
||||||
|
* The route building event.
|
||||||
|
*/
|
||||||
|
public function dynamicRoutes(RouteBuildEvent $event) {
|
||||||
|
$collection = $event->getRouteCollection();
|
||||||
|
|
||||||
|
$directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath();
|
||||||
|
|
||||||
|
$route = new Route('/' . $directory_path . '/styles/{image_style}/{scheme}',
|
||||||
|
array(
|
||||||
|
'_controller' => 'Drupal\image\Controller\ImageStyleDownloadController::deliver',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'_access' => 'TRUE',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$collection->add('image_style_public', $route);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,30 +14,6 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||||
*/
|
*/
|
||||||
interface ImageStyleInterface extends ConfigEntityInterface {
|
interface ImageStyleInterface extends ConfigEntityInterface {
|
||||||
|
|
||||||
/**
|
|
||||||
* Delivers an image derivative.
|
|
||||||
*
|
|
||||||
* Transfers a generated image derivative to the requesting agent. Modules may
|
|
||||||
* implement this method to set different serve different image derivatives
|
|
||||||
* from different stream wrappers or to customize different permissions on
|
|
||||||
* each image style.
|
|
||||||
*
|
|
||||||
* @param string $scheme
|
|
||||||
* The scheme name of the original image file stream wrapper ('public',
|
|
||||||
* 'private', 'temporary', etc.).
|
|
||||||
* @param string $target
|
|
||||||
* The target part of the uri.
|
|
||||||
*
|
|
||||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
|
|
||||||
* The image to be delivered.
|
|
||||||
*
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
|
||||||
* \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException
|
|
||||||
*
|
|
||||||
* @todo Move to controller after https://drupal.org/node/1987712.
|
|
||||||
*/
|
|
||||||
public function deliver($scheme, $target);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URI of this image when using this style.
|
* Returns the URI of this image when using this style.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\image\PathProcessor\PathProcessorImageStyles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\image\PathProcessor;
|
||||||
|
|
||||||
|
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a path processor to rewrite image styles URLs.
|
||||||
|
*
|
||||||
|
* As the route system does not allow arbitrary amount of parameters convert
|
||||||
|
* the file path to a query parameter on the request.
|
||||||
|
*
|
||||||
|
* This processor handles two different cases:
|
||||||
|
* - public image styles: In order to allow the webserver to serve these files
|
||||||
|
* directly, the route is registered under the same path as the image style so
|
||||||
|
* it took over the first generation. Therefore the path processor converts
|
||||||
|
* the file path to a query parameter.
|
||||||
|
* - private image styles: In contrast to public image styles, private
|
||||||
|
* derivatives are already using system/files/styles. Similar to public image
|
||||||
|
* styles, it also converts the file path to a query parameter.
|
||||||
|
*/
|
||||||
|
class PathProcessorImageStyles implements InboundPathProcessorInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function processInbound($path, Request $request) {
|
||||||
|
$directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath();
|
||||||
|
if (strpos($path, $directory_path . '/styles/') === 0) {
|
||||||
|
$path_prefix = $directory_path . '/styles/';
|
||||||
|
}
|
||||||
|
elseif (strpos($path, 'system/files/styles/') === 0) {
|
||||||
|
$path_prefix = 'system/files/styles/';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip out path prefix.
|
||||||
|
$rest = preg_replace('|^' . $path_prefix . '|', '', $path);
|
||||||
|
|
||||||
|
// Get the image style, scheme and path.
|
||||||
|
list($image_style, $scheme, $file) = explode('/', $rest, 3);
|
||||||
|
|
||||||
|
// Set the file as query parameter.
|
||||||
|
$request->query->set('file', $file);
|
||||||
|
|
||||||
|
return $path_prefix . $image_style . '/' . $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,10 +15,6 @@ use Drupal\image\ImageStyleInterface;
|
||||||
use Drupal\Component\Utility\Crypt;
|
use Drupal\Component\Utility\Crypt;
|
||||||
use Drupal\Component\Utility\Url;
|
use Drupal\Component\Utility\Url;
|
||||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines an image style configuration entity.
|
* Defines an image style configuration entity.
|
||||||
|
@ -161,84 +157,6 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function deliver($scheme, $target) {
|
|
||||||
$original_uri = $scheme . '://' . $target;
|
|
||||||
|
|
||||||
// Check that the scheme is valid, and the image derivative token is valid.
|
|
||||||
// (Sites which require image derivatives to be generated without a token
|
|
||||||
// can set the 'image.settings:allow_insecure_derivatives' configuration to
|
|
||||||
// TRUE to bypass the latter check, but this will increase the site's
|
|
||||||
// vulnerability to denial-of-service attacks.)
|
|
||||||
$valid = file_stream_wrapper_valid_scheme($scheme);
|
|
||||||
if (!\Drupal::config('image.settings')->get('allow_insecure_derivatives')) {
|
|
||||||
$image_derivative_token = \Drupal::request()->query->get(IMAGE_DERIVATIVE_TOKEN);
|
|
||||||
$valid &= isset($image_derivative_token) && $image_derivative_token === $this->getPathToken($original_uri);
|
|
||||||
}
|
|
||||||
if (!$valid) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$derivative_uri = $this->buildUri($original_uri);
|
|
||||||
$headers = array();
|
|
||||||
|
|
||||||
// If using the private scheme, let other modules provide headers and
|
|
||||||
// control access to the file.
|
|
||||||
if ($scheme == 'private') {
|
|
||||||
if (file_exists($derivative_uri)) {
|
|
||||||
file_download($scheme, file_uri_target($derivative_uri));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$headers = \Drupal::moduleHandler()->invokeAll('file_download', array($original_uri));
|
|
||||||
if (in_array(-1, $headers) || empty($headers)) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't try to generate file if source is missing.
|
|
||||||
if (!file_exists($original_uri)) {
|
|
||||||
watchdog('image', 'Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $original_uri, '%derivative_path' => $derivative_uri));
|
|
||||||
return new Response(t('Error generating image, missing source file.'), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't start generating the image if the derivative already exists or if
|
|
||||||
// generation is in progress in another thread.
|
|
||||||
$lock_name = 'image_style_deliver:' . $this->id() . ':' . Crypt::hashBase64($original_uri);
|
|
||||||
if (!file_exists($derivative_uri)) {
|
|
||||||
$lock_acquired = \Drupal::lock()->acquire($lock_name);
|
|
||||||
if (!$lock_acquired) {
|
|
||||||
// Tell client to retry again in 3 seconds. Currently no browsers are
|
|
||||||
// known to support Retry-After.
|
|
||||||
throw new ServiceUnavailableHttpException(3, t('Image generation in progress. Try again shortly.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate the image, unless another thread just did it while we
|
|
||||||
// were acquiring the lock.
|
|
||||||
$success = file_exists($derivative_uri) || $this->createDerivative($original_uri, $derivative_uri);
|
|
||||||
|
|
||||||
if (!empty($lock_acquired)) {
|
|
||||||
\Drupal::lock()->release($lock_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($success) {
|
|
||||||
$image = image_load($derivative_uri);
|
|
||||||
$uri = $image->source;
|
|
||||||
$headers += array(
|
|
||||||
'Content-Type' => $image->info['mime_type'],
|
|
||||||
'Content-Length' => $image->info['file_size'],
|
|
||||||
);
|
|
||||||
return new BinaryFileResponse($uri, 200, $headers);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri));
|
|
||||||
return new Response(t('Error generating image.'), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -397,14 +315,13 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface {
|
||||||
* which can be costly both in CPU time and disk space.
|
* which can be costly both in CPU time and disk space.
|
||||||
*
|
*
|
||||||
* @param string $uri
|
* @param string $uri
|
||||||
* The URI of the image for this style, for example as returned by
|
* The URI of the original image of this style.
|
||||||
* \Drupal\image\ImageStyleInterface::buildUri().
|
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
* An eight-character token which can be used to protect image style
|
* An eight-character token which can be used to protect image style
|
||||||
* derivatives against denial-of-service attacks.
|
* derivatives against denial-of-service attacks.
|
||||||
*/
|
*/
|
||||||
protected function getPathToken($uri) {
|
public function getPathToken($uri) {
|
||||||
// Return the first eight characters.
|
// Return the first eight characters.
|
||||||
return substr(Crypt::hmacBase64($this->id() . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8);
|
return substr(Crypt::hmacBase64($this->id() . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\system\FileDownloadController.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\system;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerInterface;
|
||||||
|
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System file controller.
|
||||||
|
*/
|
||||||
|
class FileDownloadController implements ControllerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module handler.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $moduleHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a FileDownloadController object.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||||
|
* The module handler.
|
||||||
|
*/
|
||||||
|
public function __construct(ModuleHandlerInterface $module_handler) {
|
||||||
|
$this->moduleHandler = $module_handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function create(ContainerInterface $container) {
|
||||||
|
return new static(
|
||||||
|
$container->get('module_handler')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles private file transfers.
|
||||||
|
*
|
||||||
|
* Call modules that implement hook_file_download() to find out if a file is
|
||||||
|
* accessible and what headers it should be transferred with. If one or more
|
||||||
|
* modules returned headers the download will start with the returned headers.
|
||||||
|
* If a module returns -1 an AccessDeniedHttpException will be thrown. If the
|
||||||
|
* file exists but no modules responded an AccessDeniedHttpException will be
|
||||||
|
* thrown. If the file does not exist a NotFoundHttpException will be thrown.
|
||||||
|
*
|
||||||
|
* @see hook_file_download()
|
||||||
|
*
|
||||||
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||||
|
* The request object.
|
||||||
|
* @param string $scheme
|
||||||
|
* The file scheme, defaults to 'private'.
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||||
|
* Thrown when the requested file does not exist.
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||||
|
* Thrown when the user does not have access to the file.
|
||||||
|
*
|
||||||
|
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||||
|
* The transferred file as response.
|
||||||
|
*/
|
||||||
|
public function download(Request $request, $scheme = 'private') {
|
||||||
|
$target = $request->query->get('file');
|
||||||
|
// Merge remaining path arguments into relative file path.
|
||||||
|
$uri = $scheme . '://' . $target;
|
||||||
|
|
||||||
|
if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
|
||||||
|
// Let other modules provide headers and controls access to the file.
|
||||||
|
$headers = $this->moduleHandler->invokeAll('file_download', array($uri));
|
||||||
|
|
||||||
|
foreach ($headers as $result) {
|
||||||
|
if ($result == -1) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($headers)) {
|
||||||
|
return new BinaryFileResponse($uri, 200, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\system\PathProcessor\PathProcessorPrivateFiles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\system\PathProcessor;
|
||||||
|
|
||||||
|
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a path processor to rewrite file URLs.
|
||||||
|
*
|
||||||
|
* As the route system does not allow arbitrary amount of parameters convert
|
||||||
|
* the file path to a query parameter on the request.
|
||||||
|
*/
|
||||||
|
class PathProcessorFiles implements InboundPathProcessorInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function processInbound($path, Request $request) {
|
||||||
|
if (strpos($path, 'system/files/') === 0 && !$request->query->has('file')) {
|
||||||
|
$file_path = preg_replace('|^system\/files\/|', '', $path);
|
||||||
|
$request->query->set('file', $file_path);
|
||||||
|
return 'system/files';
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -609,13 +609,6 @@ function system_element_info() {
|
||||||
* Implements hook_menu().
|
* Implements hook_menu().
|
||||||
*/
|
*/
|
||||||
function system_menu() {
|
function system_menu() {
|
||||||
$items['system/files'] = array(
|
|
||||||
'title' => 'File download',
|
|
||||||
'page callback' => 'file_download',
|
|
||||||
'page arguments' => array('private'),
|
|
||||||
'access callback' => TRUE,
|
|
||||||
'type' => MENU_CALLBACK,
|
|
||||||
);
|
|
||||||
$items['system/temporary'] = array(
|
$items['system/temporary'] = array(
|
||||||
'title' => 'Temporary files',
|
'title' => 'Temporary files',
|
||||||
'page callback' => 'file_download',
|
'page callback' => 'file_download',
|
||||||
|
|
|
@ -165,6 +165,14 @@ system_admin_index:
|
||||||
requirements:
|
requirements:
|
||||||
_permission: 'access administration pages'
|
_permission: 'access administration pages'
|
||||||
|
|
||||||
|
system_files:
|
||||||
|
pattern: '/system/files/{scheme}'
|
||||||
|
defaults:
|
||||||
|
_controller: 'Drupal\system\FileDownloadController::download'
|
||||||
|
scheme: private
|
||||||
|
requirements:
|
||||||
|
_access: 'TRUE'
|
||||||
|
|
||||||
system_theme_settings:
|
system_theme_settings:
|
||||||
pattern: '/admin/appearance/settings'
|
pattern: '/admin/appearance/settings'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
|
@ -18,6 +18,10 @@ services:
|
||||||
class: Drupal\system\LegacyBreadcrumbBuilder
|
class: Drupal\system\LegacyBreadcrumbBuilder
|
||||||
tags:
|
tags:
|
||||||
- {name: breadcrumb_builder, priority: 500}
|
- {name: breadcrumb_builder, priority: 500}
|
||||||
|
path_processor.files:
|
||||||
|
class: Drupal\system\PathProcessor\PathProcessorFiles
|
||||||
|
tags:
|
||||||
|
- { name: path_processor_inbound, priority: 200 }
|
||||||
system.route_subscriber:
|
system.route_subscriber:
|
||||||
class: Drupal\system\Routing\RouteSubscriber
|
class: Drupal\system\Routing\RouteSubscriber
|
||||||
tags:
|
tags:
|
||||||
|
|
Loading…
Reference in New Issue