Chris@0: name; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function postSave(EntityStorageInterface $storage, $update = TRUE) { Chris@0: parent::postSave($storage, $update); Chris@0: Chris@0: if ($update) { Chris@0: if (!empty($this->original) && $this->id() !== $this->original->id()) { Chris@0: // The old image style name needs flushing after a rename. Chris@0: $this->original->flush(); Chris@0: // Update field settings if necessary. Chris@0: if (!$this->isSyncing()) { Chris@0: static::replaceImageStyle($this); Chris@0: } Chris@0: } Chris@0: else { Chris@0: // Flush image style when updating without changing the name. Chris@0: $this->flush(); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function postDelete(EntityStorageInterface $storage, array $entities) { Chris@0: parent::postDelete($storage, $entities); Chris@0: Chris@0: /** @var \Drupal\image\ImageStyleInterface[] $entities */ Chris@0: foreach ($entities as $style) { Chris@0: // Flush cached media for the deleted style. Chris@0: $style->flush(); Chris@0: // Clear the replacement ID, if one has been previously stored. Chris@0: /** @var \Drupal\image\ImageStyleStorageInterface $storage */ Chris@0: $storage->clearReplacementId($style->id()); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Update field settings if the image style name is changed. Chris@0: * Chris@0: * @param \Drupal\image\ImageStyleInterface $style Chris@0: * The image style. Chris@0: */ Chris@0: protected static function replaceImageStyle(ImageStyleInterface $style) { Chris@0: if ($style->id() != $style->getOriginalId()) { Chris@0: // Loop through all entity displays looking for formatters / widgets using Chris@0: // the image style. Chris@0: foreach (EntityViewDisplay::loadMultiple() as $display) { Chris@0: foreach ($display->getComponents() as $name => $options) { Chris@0: if (isset($options['type']) && $options['type'] == 'image' && $options['settings']['image_style'] == $style->getOriginalId()) { Chris@0: $options['settings']['image_style'] = $style->id(); Chris@0: $display->setComponent($name, $options) Chris@0: ->save(); Chris@0: } Chris@0: } Chris@0: } Chris@17: foreach (EntityFormDisplay::loadMultiple() as $display) { Chris@0: foreach ($display->getComponents() as $name => $options) { Chris@0: if (isset($options['type']) && $options['type'] == 'image_image' && $options['settings']['preview_image_style'] == $style->getOriginalId()) { Chris@0: $options['settings']['preview_image_style'] = $style->id(); Chris@0: $display->setComponent($name, $options) Chris@0: ->save(); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function buildUri($uri) { Chris@0: $source_scheme = $scheme = $this->fileUriScheme($uri); Chris@0: $default_scheme = $this->fileDefaultScheme(); Chris@0: Chris@0: if ($source_scheme) { Chris@0: $path = $this->fileUriTarget($uri); Chris@0: // The scheme of derivative image files only needs to be computed for Chris@0: // source files not stored in the default scheme. Chris@0: if ($source_scheme != $default_scheme) { Chris@0: $class = $this->getStreamWrapperManager()->getClass($source_scheme); Chris@0: $is_writable = $class::getType() & StreamWrapperInterface::WRITE; Chris@0: Chris@0: // Compute the derivative URI scheme. Derivatives created from writable Chris@0: // source stream wrappers will inherit the scheme. Derivatives created Chris@0: // from read-only stream wrappers will fall-back to the default scheme. Chris@0: $scheme = $is_writable ? $source_scheme : $default_scheme; Chris@0: } Chris@0: } Chris@0: else { Chris@0: $path = $uri; Chris@0: $source_scheme = $scheme = $default_scheme; Chris@0: } Chris@0: return "$scheme://styles/{$this->id()}/$source_scheme/{$this->addExtension($path)}"; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function buildUrl($path, $clean_urls = NULL) { Chris@0: $uri = $this->buildUri($path); Chris@0: // The token query is added even if the Chris@0: // 'image.settings:allow_insecure_derivatives' configuration is TRUE, so Chris@0: // that the emitted links remain valid if it is changed back to the default Chris@0: // FALSE. However, sites which need to prevent the token query from being Chris@0: // emitted at all can additionally set the Chris@0: // 'image.settings:suppress_itok_output' configuration to TRUE to achieve Chris@0: // that (if both are set, the security token will neither be emitted in the Chris@0: // image derivative URL nor checked for in Chris@0: // \Drupal\image\ImageStyleInterface::deliver()). Chris@0: $token_query = []; Chris@0: if (!\Drupal::config('image.settings')->get('suppress_itok_output')) { Chris@0: // The passed $path variable can be either a relative path or a full URI. Chris@0: $original_uri = file_uri_scheme($path) ? file_stream_wrapper_uri_normalize($path) : file_build_uri($path); Chris@0: $token_query = [IMAGE_DERIVATIVE_TOKEN => $this->getPathToken($original_uri)]; Chris@0: } Chris@0: Chris@0: if ($clean_urls === NULL) { Chris@0: // Assume clean URLs unless the request tells us otherwise. Chris@0: $clean_urls = TRUE; Chris@0: try { Chris@0: $request = \Drupal::request(); Chris@0: $clean_urls = RequestHelper::isCleanUrl($request); Chris@0: } Chris@0: catch (ServiceNotFoundException $e) { Chris@0: } Chris@0: } Chris@0: Chris@0: // If not using clean URLs, the image derivative callback is only available Chris@0: // with the script path. If the file does not exist, use Url::fromUri() to Chris@0: // ensure that it is included. Once the file exists it's fine to fall back Chris@0: // to the actual file path, this avoids bootstrapping PHP once the files are Chris@0: // built. Chris@0: if ($clean_urls === FALSE && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { Chris@0: $directory_path = $this->getStreamWrapperManager()->getViaUri($uri)->getDirectoryPath(); Chris@0: return Url::fromUri('base:' . $directory_path . '/' . file_uri_target($uri), ['absolute' => TRUE, 'query' => $token_query])->toString(); Chris@0: } Chris@0: Chris@0: $file_url = file_create_url($uri); Chris@0: // Append the query string with the token, if necessary. Chris@0: if ($token_query) { Chris@0: $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query); Chris@0: } Chris@0: Chris@0: return $file_url; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function flush($path = NULL) { Chris@0: // A specific image path has been provided. Flush only that derivative. Chris@18: /** @var \Drupal\Core\File\FileSystemInterface $file_system */ Chris@18: $file_system = \Drupal::service('file_system'); Chris@0: if (isset($path)) { Chris@0: $derivative_uri = $this->buildUri($path); Chris@0: if (file_exists($derivative_uri)) { Chris@18: try { Chris@18: $file_system->delete($derivative_uri); Chris@18: } Chris@18: catch (FileException $e) { Chris@18: // Ignore failed deletes. Chris@18: } Chris@0: } Chris@0: return $this; Chris@0: } Chris@0: Chris@0: // Delete the style directory in each registered wrapper. Chris@0: $wrappers = $this->getStreamWrapperManager()->getWrappers(StreamWrapperInterface::WRITE_VISIBLE); Chris@0: foreach ($wrappers as $wrapper => $wrapper_data) { Chris@0: if (file_exists($directory = $wrapper . '://styles/' . $this->id())) { Chris@18: try { Chris@18: $file_system->deleteRecursive($directory); Chris@18: } Chris@18: catch (FileException $e) { Chris@18: // Ignore failed deletes. Chris@18: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Let other modules update as necessary on flush. Chris@0: $module_handler = \Drupal::moduleHandler(); Chris@0: $module_handler->invokeAll('image_style_flush', [$this]); Chris@0: Chris@0: // Clear caches so that formatters may be added for this style. Chris@0: drupal_theme_rebuild(); Chris@0: Chris@0: Cache::invalidateTags($this->getCacheTagsToInvalidate()); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createDerivative($original_uri, $derivative_uri) { Chris@0: // If the source file doesn't exist, return FALSE without creating folders. Chris@0: $image = $this->getImageFactory()->get($original_uri); Chris@0: if (!$image->isValid()) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // Get the folder for the final location of this style. Chris@18: $directory = \Drupal::service('file_system')->dirname($derivative_uri); Chris@0: Chris@0: // Build the destination folder tree if it doesn't already exist. Chris@18: if (!\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { Chris@0: \Drupal::logger('image')->error('Failed to create style directory: %directory', ['%directory' => $directory]); Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: foreach ($this->getEffects() as $effect) { Chris@0: $effect->applyEffect($image); Chris@0: } Chris@0: Chris@0: if (!$image->save($derivative_uri)) { Chris@0: if (file_exists($derivative_uri)) { Chris@0: \Drupal::logger('image')->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $derivative_uri]); Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function transformDimensions(array &$dimensions, $uri) { Chris@0: foreach ($this->getEffects() as $effect) { Chris@0: $effect->transformDimensions($dimensions, $uri); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getDerivativeExtension($extension) { Chris@0: foreach ($this->getEffects() as $effect) { Chris@0: $extension = $effect->getDerivativeExtension($extension); Chris@0: } Chris@0: return $extension; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getPathToken($uri) { Chris@0: // Return the first 8 characters. Chris@0: return substr(Crypt::hmacBase64($this->id() . ':' . $this->addExtension($uri), $this->getPrivateKey() . $this->getHashSalt()), 0, 8); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function deleteImageEffect(ImageEffectInterface $effect) { Chris@0: $this->getEffects()->removeInstanceId($effect->getUuid()); Chris@0: $this->save(); Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsUri($uri) { Chris@0: // Only support the URI if its extension is supported by the current image Chris@0: // toolkit. Chris@0: return in_array( Chris@17: mb_strtolower(pathinfo($uri, PATHINFO_EXTENSION)), Chris@0: $this->getImageFactory()->getSupportedExtensions() Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getEffect($effect) { Chris@0: return $this->getEffects()->get($effect); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getEffects() { Chris@0: if (!$this->effectsCollection) { Chris@0: $this->effectsCollection = new ImageEffectPluginCollection($this->getImageEffectPluginManager(), $this->effects); Chris@0: $this->effectsCollection->sort(); Chris@0: } Chris@0: return $this->effectsCollection; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getPluginCollections() { Chris@0: return ['effects' => $this->getEffects()]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function addImageEffect(array $configuration) { Chris@0: $configuration['uuid'] = $this->uuidGenerator()->generate(); Chris@0: $this->getEffects()->addInstanceId($configuration['uuid'], $configuration); Chris@0: return $configuration['uuid']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getReplacementID() { Chris@0: /** @var \Drupal\image\ImageStyleStorageInterface $storage */ Chris@0: $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId()); Chris@0: return $storage->getReplacementId($this->id()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getName() { Chris@0: return $this->get('name'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setName($name) { Chris@0: $this->set('name', $name); Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the image effect plugin manager. Chris@0: * Chris@0: * @return \Drupal\Component\Plugin\PluginManagerInterface Chris@0: * The image effect plugin manager. Chris@0: */ Chris@0: protected function getImageEffectPluginManager() { Chris@0: return \Drupal::service('plugin.manager.image.effect'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the image factory. Chris@0: * Chris@0: * @return \Drupal\Core\Image\ImageFactory Chris@0: * The image factory. Chris@0: */ Chris@0: protected function getImageFactory() { Chris@0: return \Drupal::service('image.factory'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the Drupal private key. Chris@0: * Chris@0: * @return string Chris@0: * The Drupal private key. Chris@0: */ Chris@0: protected function getPrivateKey() { Chris@0: return \Drupal::service('private_key')->get(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets a salt useful for hardening against SQL injection. Chris@0: * Chris@0: * @return string Chris@0: * A salt based on information in settings.php, not in the database. Chris@0: * Chris@0: * @throws \RuntimeException Chris@0: */ Chris@0: protected function getHashSalt() { Chris@0: return Settings::getHashSalt(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds an extension to a path. Chris@0: * Chris@0: * If this image style changes the extension of the derivative, this method Chris@0: * adds the new extension to the given path. This way we avoid filename Chris@0: * clashes while still allowing us to find the source image. Chris@0: * Chris@0: * @param string $path Chris@0: * The path to add the extension to. Chris@0: * Chris@0: * @return string Chris@0: * The given path if this image style doesn't change its extension, or the Chris@0: * path with the added extension if it does. Chris@0: */ Chris@0: protected function addExtension($path) { Chris@0: $original_extension = pathinfo($path, PATHINFO_EXTENSION); Chris@0: $extension = $this->getDerivativeExtension($original_extension); Chris@0: if ($original_extension !== $extension) { Chris@0: $path .= '.' . $extension; Chris@0: } Chris@0: return $path; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides a wrapper for file_uri_scheme() to allow unit testing. Chris@0: * Chris@0: * Returns the scheme of a URI (e.g. a stream). Chris@0: * Chris@0: * @param string $uri Chris@0: * A stream, referenced as "scheme://target" or "data:target". Chris@0: * Chris@0: * @see file_uri_target() Chris@0: * Chris@0: * @todo: Remove when https://www.drupal.org/node/2050759 is in. Chris@0: * Chris@0: * @return string Chris@0: * A string containing the name of the scheme, or FALSE if none. For Chris@0: * example, the URI "public://example.txt" would return "public". Chris@0: */ Chris@0: protected function fileUriScheme($uri) { Chris@0: return file_uri_scheme($uri); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides a wrapper for file_uri_target() to allow unit testing. Chris@0: * Chris@0: * Returns the part of a URI after the schema. Chris@0: * Chris@0: * @param string $uri Chris@0: * A stream, referenced as "scheme://target" or "data:target". Chris@0: * Chris@0: * @see file_uri_scheme() Chris@0: * Chris@0: * @todo: Convert file_uri_target() into a proper injectable service. Chris@0: * Chris@0: * @return string|bool Chris@0: * A string containing the target (path), or FALSE if none. Chris@0: * For example, the URI "public://sample/test.txt" would return Chris@0: * "sample/test.txt". Chris@0: */ Chris@0: protected function fileUriTarget($uri) { Chris@0: return file_uri_target($uri); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides a wrapper for file_default_scheme() to allow unit testing. Chris@0: * Chris@0: * Gets the default file stream implementation. Chris@0: * Chris@0: * @todo: Convert file_default_scheme() into a proper injectable service. Chris@0: * Chris@0: * @return string Chris@0: * 'public', 'private' or any other file scheme defined as the default. Chris@0: */ Chris@0: protected function fileDefaultScheme() { Chris@0: return file_default_scheme(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the stream wrapper manager service. Chris@0: * Chris@0: * @return \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface Chris@0: * The stream wrapper manager service Chris@0: * Chris@0: * @todo Properly inject this service in Drupal 9.0.x. Chris@0: */ Chris@0: protected function getStreamWrapperManager() { Chris@0: return \Drupal::service('stream_wrapper_manager'); Chris@0: } Chris@0: Chris@0: }