Commit 36d647e6 authored by alexpott's avatar alexpott

Issue #1987712 by dawehner, claudiu.cristea, damiankloip, andypost: Convert...

Issue #1987712 by dawehner, claudiu.cristea, damiankloip, andypost: Convert file_download() to a new style controller.
parent ad137531
......@@ -7,9 +7,6 @@
use Drupal\Core\StreamWrapper\LocalStream;
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.
......@@ -1331,42 +1328,6 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
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.
*
......
......@@ -10,9 +10,10 @@
use Drupal\Core\Controller\ControllerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Component\Archiver\ArchiveTar;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns responses for config module routes.
......@@ -22,22 +23,33 @@ class ConfigController implements ControllerInterface {
/**
* The target storage.
*
* @var \Drupal\Core\Config\StorageInterface;
* @var \Drupal\Core\Config\StorageInterface
*/
protected $targetStorage;
/**
* The source storage.
*
* @var \Drupal\Core\Config\StorageInterface;
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sourceStorage;
/**
* The file download controller.
*
* @var \Drupal\Core\Controller\ControllerInterface
*/
protected $fileDownloadController;
/**
* {@inheritdoc}
*/
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 @@ public static function create(ContainerInterface $container) {
* The target storage.
* @param \Drupal\Core\Config\StorageInterface $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->sourceStorage = $source_storage;
$this->fileDownloadController = $file_download_controller;
}
/**
......@@ -64,7 +79,9 @@ public function downloadExport() {
$config_files[] = $config_dir . '/' . $config_name . '.yml';
}
$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
// every usage is already a match.
$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) {
$bundle = $entity->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() {
$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(
'title' => 'Image styles',
'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;
}
/**
* 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.
*
......
......@@ -11,3 +11,11 @@ image_effect_delete:
_form: '\Drupal\image\Form\ImageEffectDeleteForm'
requirements:
_permission: 'administer image styles'
image_style_private:
pattern: '/system/files/styles/{image_style}/{scheme}'
defaults:
_controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver'
requirements:
_access: 'TRUE'
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 }
<?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);
}
}
}
<?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 @@
*/
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.
*
......
<?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\Component\Utility\Crypt;
use Drupal\Component\Utility\Url;
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.
......@@ -161,84 +157,6 @@ protected static function replaceImageStyle(ImageStyleInterface $style) {
}
}
/**
* {@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();
}
}
}