Commit 27f8cd4c authored by alexpott's avatar alexpott
Browse files

Issue #2027423 by claudiu.cristea, tim.plunkett, andypost: Make image style system flexible.

parent 176fbe7c
......@@ -608,7 +608,7 @@ function theme_image_style_effects($variables) {
*
* @param $variables
* An associative array containing:
* - style: The image style array being previewed.
* - style: \Drupal\image\ImageStyleInterface image style being previewed.
*
* @ingroup themeable
*/
......@@ -634,9 +634,9 @@ function theme_image_style_preview($variables) {
$original_attributes['style'] = 'width: ' . $original_width . 'px; height: ' . $original_height . 'px;';
// Set up preview file information.
$preview_file = image_style_path($style->id(), $original_path);
$preview_file = $style->buildUri($original_path);
if (!file_exists($preview_file)) {
image_style_create_derivative($style, $original_path, $preview_file);
$style->createDerivative($original_path, $preview_file);
}
$preview_image = image_get_info($preview_file);
if ($preview_image['width'] > $preview_image['height']) {
......
......@@ -74,8 +74,8 @@ function hook_image_effect_info_alter(&$effects) {
* be cleared using this hook. This hook is called whenever a style is updated,
* deleted, or any effect associated with the style is update or deleted.
*
* @param Drupal\image\Plugin\Core\Entity\ImageStyle $style
* The image style array that is being flushed.
* @param \Drupal\image\ImageStyleInterface $style
* The image style object that is being flushed.
*/
function hook_image_style_flush($style) {
// Empty cached data that contains information about the style.
......
......@@ -9,13 +9,9 @@
use Drupal\Core\Language\Language;
use Drupal\field\Plugin\Core\Entity\Field;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Uuid\Uuid;
use Drupal\file\Plugin\Core\Entity\File;
use Drupal\image\ImageStyleInterface;
use Drupal\image\Plugin\Core\Entity\ImageStyle;
use Drupal\field\FieldInterface;
use Drupal\field\FieldInstanceInterface;
......@@ -357,10 +353,7 @@ function image_file_predelete(File $file) {
function image_path_flush($path) {
$styles = entity_load_multiple('image_style');
foreach ($styles as $style) {
$image_path = image_style_path($style->id(), $path);
if (file_exists($image_path)) {
file_unmanaged_delete($image_path);
}
$style->flush($path);
}
}
......@@ -403,304 +396,23 @@ function image_style_options($include_empty = TRUE) {
*
* After generating an image, transfer it to the requesting agent.
*
* @param $style
* The image style
*/
function image_style_deliver($style, $scheme) {
$args = func_get_args();
array_shift($args);
array_shift($args);
$target = implode('/', $args);
// 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($style) && file_stream_wrapper_valid_scheme($scheme);
if (!config('image.settings')->get('allow_insecure_derivatives')) {
$image_derivative_token = Drupal::request()->query->get(IMAGE_DERIVATIVE_TOKEN);
$valid = $valid && isset($image_derivative_token) && $image_derivative_token === image_style_path_token($style->name, $scheme . '://' . $target);
}
if (!$valid) {
throw new AccessDeniedHttpException();
}
$image_uri = $scheme . '://' . $target;
$derivative_uri = image_style_path($style->id(), $image_uri);
// 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 = module_invoke_all('file_download', $image_uri);
if (in_array(-1, $headers) || empty($headers)) {
throw new AccessDeniedHttpException();
}
if (count($headers)) {
foreach ($headers as $name => $value) {
drupal_add_http_header($name, $value);
}
}
}
}
// 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(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:' . $style->id() . ':' . Crypt::hashBase64($image_uri);
if (!file_exists($derivative_uri)) {
$lock_acquired = 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) || image_style_create_derivative($style, $image_uri, $derivative_uri);
if (!empty($lock_acquired)) {
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);
}
}
/**
* Creates a new image derivative based on an image style.
*
* Generates an image derivative by creating the destination folder (if it does
* not already exist), applying all image effects defined in $style->effects,
* and saving a cached version of the resulting image.
* @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.).
*
* @param $style
* An image style array.
* @param $source
* Path of the source file.
* @param $destination
* Path or URI of the destination file.
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
* The image to be delivered.
*
* @return
* TRUE if an image derivative was generated, or FALSE if the image derivative
* could not be generated.
*
* @see image_style_load()
* @todo Remove this wrapper in https://drupal.org/node/1987712.
*/
function image_style_create_derivative($style, $source, $destination) {
// Get the folder for the final location of this style.
$directory = drupal_dirname($destination);
// Build the destination folder tree if it doesn't already exist.
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), WATCHDOG_ERROR);
return FALSE;
}
if (!$image = image_load($source)) {
return FALSE;
}
if (!empty($style->effects)) {
foreach ($style->effects as $effect) {
image_effect_apply($image, $effect);
}
}
if (!image_save($image, $destination)) {
if (file_exists($destination)) {
watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), WATCHDOG_ERROR);
}
return FALSE;
}
return TRUE;
}
/**
* Determines the dimensions of the styled image.
*
* Applies all of an image style's effects to $dimensions.
*
* @param $style_name
* The name of the style to be applied.
* @param $dimensions
* Dimensions to be modified - an array with components width and height, in
* pixels.
*/
function image_style_transform_dimensions($style_name, array &$dimensions) {
module_load_include('inc', 'image', 'image.effects');
$style = entity_load('image_style', $style_name);
if (!empty($style->effects)) {
foreach ($style->effects as $effect) {
if (isset($effect['dimensions passthrough'])) {
continue;
}
if (isset($effect['dimensions callback'])) {
$effect['dimensions callback']($dimensions, $effect['data']);
}
else {
$dimensions['width'] = $dimensions['height'] = NULL;
}
}
}
}
/**
* Flushes cached media for a style.
*
* @param $style
* An image style array.
*/
function image_style_flush($style) {
// Delete the style directory in each registered wrapper.
$wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
foreach ($wrappers as $wrapper => $wrapper_data) {
file_unmanaged_delete_recursive($wrapper . '://styles/' . $style->id());
}
// Let other modules update as necessary on flush.
module_invoke_all('image_style_flush', $style);
// Clear field caches so that formatters may be added for this style.
field_info_cache_clear();
drupal_theme_rebuild();
// Clear page caches when flushing.
if (module_exists('block')) {
cache('block')->deleteAll();
}
cache('page')->deleteAll();
}
/**
* Returns the URL for an image derivative given a style and image path.
*
* @param $style_name
* The name of the style to be used with this image.
* @param $path
* The path to the image.
* @param $clean_urls
* (optional) Whether clean URLs are in use.
* @return
* The absolute URL where a style image can be downloaded, suitable for use
* in an <img> tag. Requesting the URL will cause the image to be created.
* @see image_style_deliver()
*/
function image_style_url($style_name, $path, $clean_urls = NULL) {
$uri = image_style_path($style_name, $path);
// The token query is added even if the
// 'image.settings:allow_insecure_derivatives' configuration is TRUE, so that
// the emitted links remain valid if it is changed back to the default FALSE.
// However, sites which need to prevent the token query from being emitted at
// all can additionally set the 'image.settings:suppress_itok_output'
// configuration to TRUE to achieve that (if both are set, the security token
// will neither be emitted in the image derivative URL nor checked for in
// image_style_deliver()).
$token_query = array();
if (!config('image.settings')->get('suppress_itok_output')) {
$token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, file_stream_wrapper_uri_normalize($path)));
}
if ($clean_urls === NULL) {
// Assume clean URLs unless the request tells us otherwise.
$clean_urls = TRUE;
try {
$request = Drupal::request();
$clean_urls = $request->attributes->get('clean_urls');
}
catch (ServiceNotFoundException $e) {
}
}
// If not using clean URLs, the image derivative callback is only available
// with the script path. If the file does not exist, use url() to ensure
// that it is included. Once the file exists it's fine to fall back to the
// actual file path, this avoids bootstrapping PHP once the files are built.
if ($clean_urls === FALSE && file_uri_scheme($uri) == 'public' && !file_exists($uri)) {
$directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath();
return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE, 'query' => $token_query));
}
$file_url = file_create_url($uri);
// Append the query string with the token, if necessary.
if ($token_query) {
$file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query);
}
return $file_url;
}
/**
* Generates a token to protect an image style derivative.
*
* This prevents unauthorized generation of an image style derivative,
* which can be costly both in CPU time and disk space.
*
* @param string $style_name
* The name of the image style.
* @param string $uri
* The URI of the image for this style, for example as returned by
* image_style_path().
*
* @return string
* An eight-character token which can be used to protect image style
* derivatives against denial-of-service attacks.
*/
function image_style_path_token($style_name, $uri) {
// Return the first eight characters.
return substr(Crypt::hmacBase64($style_name . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8);
}
/**
* Returns the URI of an image when using a style.
*
* The path returned by this function may not exist. The default generation
* method only creates images when they are requested by a user's browser.
*
* @param $style_name
* The name of the style to be used with this image.
* @param $uri
* The URI or path to the image.
* @return
* The URI to an image style image.
* @see image_style_url()
*/
function image_style_path($style_name, $uri) {
$scheme = file_uri_scheme($uri);
if ($scheme) {
$path = file_uri_target($uri);
}
else {
$path = $uri;
$scheme = file_default_scheme();
}
return $scheme . '://styles/' . $style_name . '/' . $scheme . '/' . $path;
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);
}
/**
......@@ -835,10 +547,6 @@ function image_effect_save($style, &$effect) {
}
$style->effects[$effect['ieid']] = $effect;
$style->save();
// Flush all derivatives that exist for this style, so they are regenerated
// with the new or updated effect.
image_style_flush($style);
}
/**
......@@ -852,7 +560,6 @@ function image_effect_save($style, &$effect) {
function image_effect_delete($style, $effect) {
unset($style->effects[$effect['ieid']]);
$style->save();
image_style_flush($style);
}
/**
......@@ -892,13 +599,17 @@ function image_effect_apply($image, $effect) {
* @ingroup themeable
*/
function theme_image_style($variables) {
// @todo Image style loading will be moved outside theme in
// https://drupal.org/node/2029649
$style = entity_load('image_style', $variables['style_name']);
// Determine the dimensions of the styled image.
$dimensions = array(
'width' => $variables['width'],
'height' => $variables['height'],
);
image_style_transform_dimensions($variables['style_name'], $dimensions);
$style->transformDimensions($dimensions);
// Add in the image style name as an HTML class.
$variables['attributes']['class'][] = 'image-style-' . drupal_html_class($variables['style_name']);
......@@ -908,7 +619,7 @@ function theme_image_style($variables) {
'#width' => $dimensions['width'],
'#height' => $dimensions['height'],
'#attributes' => $variables['attributes'],
'#uri' => image_style_url($variables['style_name'], $variables['uri']),
'#uri' => $style->buildUrl($variables['uri']),
);
if (isset($variables['alt'])) {
......
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\image\Plugin\Core\Entity\ImageStyleInterface.
* Contains \Drupal\image\ImageStyleInterface.
*/
namespace Drupal\image;
......@@ -14,4 +14,99 @@
*/
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.
*
* The path returned by this function may not exist. The default generation
* method only creates images when they are requested by a user's browser.
* Modules may implement this method to decide where to place derivatives.
*
* @param string $uri
* The URI or path to the original image.
*
* @return string
* The URI to the image derivative for this style.
*/
public function buildUri($uri);
/**
* Returns the URL of this image derivative for an original image path or URI.
*
* @param string $path
* The path or URI to the original image.
* @param mixed $clean_urls
* (optional) Whether clean URLs are in use.
*
* @return string
* The absolute URL where a style image can be downloaded, suitable for use
* in an <img> tag. Requesting the URL will cause the image to be created.
*
* @see \Drupal\image\ImageStyleInterface::deliver()
*/
public function buildUrl($path, $clean_urls = NULL);
/**
* Flushes cached media for this style.
*
* @param string $path
* (optional) The original image path or URI. If it's supplied, only this
* image derivative will be flushed.
*/
public function flush($path = NULL);
/**
* Creates a new image derivative based on this image style.
*
* Generates an image derivative applying all image effects and saving the
* resulting image.
*
* @param string $original_uri
* Original image file URI.
* @param string $derivative_uri
* Derivative image file URI.
*
* @return bool
* TRUE if an image derivative was generated, or FALSE if the image
* derivative could not be generated.
*/
public function createDerivative($original_uri, $derivative_uri);
/**
* Determines the dimensions of this image style.
*
* Stores the dimensions of this image style into $dimensions associative
* array. Implementations have to provide at least values to next keys:
* - width: Integer with the derivative image width.
* - height: Integer with the derivative image height.
*
* @param array $dimensions
* Associative array passed by reference. Implementations have to store the
* resulting width and height, in pixels.
*/
public function transformDimensions(array &$dimensions);
}
......@@ -12,6 +12,13 @@
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\image\ImageStyleInterface;
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.
......@@ -68,7 +75,7 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface {
/**
* The array of image effects for this image style.
*
* @var string
* @var array
*/
public $effects;
......@@ -83,11 +90,17 @@ public function id() {
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if ($update && !empty($this->original) && $this->id() !== $this->original->id()) {
// The old image style name needs flushing after a rename.
image_style_flush($this->original);
// Update field instance settings if necessary.
static::replaceImageStyle($this);
if ($update) {
if (!empty($this->original) && $this->id() !== $this->original->id()) {
// The old image style name needs flushing after a rename.
$this->original->flush();
// Update field instance settings if necessary.
static::replaceImageStyle($this);
}
else {
// Flush image style when updating without changing the name.
$this->flush();
}
}
}
......@@ -97,7 +110,7 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $style) {
// Flush cached media for the deleted style.
image_style_flush($style);
$style->flush();
// Check whether field instance settings need to be updated.
// In case no replacement style was specified, all image fields that are
// using the deleted style are left in a broken state.
......@@ -112,10 +125,10 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
/**
* Update field instance settings if the image style name is changed.
*
* @param \Drupal\image\Plugin\Core\Entity\ImageStyle $style
* @param \Drupal\image\ImageStyleInterface $style
* The image style.
*/
protected static function replaceImageStyle(ImageStyle $style) {
protected static function replaceImageStyle(ImageStyleInterface $style) {
if ($style->id() != $style->getOriginalID()) {
$instances = field_read_instances();
// Loop through all fields searching for image fields.
......@@ -148,4 +161,252 @@ protected static function replaceImageStyle(ImageStyle $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);