You are here

imagecache_external.module in Imagecache External 8

Allows the usage of Image Styles on external images.

File

imagecache_external.module
View source
<?php

/**
 * @file
 * Allows the usage of Image Styles on external images.
 */
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File;

/**
 * Implements hook_theme().
 */
function imagecache_external_theme() {
  return [
    'imagecache_external' => [
      'variables' => [
        'style_name' => NULL,
        'uri' => NULL,
        'alt' => '',
        'title' => NULL,
        'width' => NULL,
        'height' => NULL,
        'attributes' => [],
      ],
    ],
    'imagecache_external_responsive' => [
      'variables' => [
        'responsive_image_style_id' => NULL,
        'uri' => NULL,
        'alt' => '',
        'title' => NULL,
        'width' => NULL,
        'height' => NULL,
        'attributes' => [],
      ],
    ],
  ];
}

/**
 * Returns HTML for an image using a specific image style.
 *
 * @param array $variables
 *   An associative array containing:
 *   - style_name: The name of the style to be used to alter the original image.
 *   - path: The path of the image file relative to the Drupal files directory.
 *     This function does not work with images outside the files directory nor
 *     with remotely hosted images.
 *   - alt: The alternative text for text-based browsers.
 *   - title: The title text is displayed when the image is hovered in some
 *     popular browsers.
 *   - attributes: Associative array of attributes to be placed in the img tag.
 *
 * @ingroup themeable
 * @return bool
 */
function template_preprocess_imagecache_external(&$variables) {
  if ($variables['uri'] = imagecache_external_generate_path($variables['uri'])) {
    template_preprocess_image_style($variables);
  }
  return FALSE;
}

/**
 * Returns HTML for an image using a specific responsive image style.
 *
 * @param array $variables
 *   An associative array containing:
 *   - style_name: The name of the style to be used to alter the original image.
 *   - path: The path of the image file relative to the Drupal files directory.
 *     This function does not work with images outside the files directory nor
 *     with remotely hosted images.
 *   - alt: The alternative text for text-based browsers.
 *   - title: The title text is displayed when the image is hovered in some
 *     popular browsers.
 *   - attributes: Associative array of attributes to be placed in the img tag.
 *
 * @ingroup themeable
 *
 * @return bool
 */
function template_preprocess_imagecache_external_responsive(&$variables) {
  if ($variables['uri'] = imagecache_external_generate_path($variables['uri'])) {
    template_preprocess_responsive_image($variables);
  }
  return FALSE;
}

/**
 * Util to generate a path to an image.
 *
 * @param string $url
 *   The url to the image.
 *
 * @return string
 *   The url to the image.
 */
function imagecache_external_generate_path($url) {

  // Get configuration.
  $config = imagecache_external_config();

  // Create the external images directory and ensure it's writable.
  $hash = md5($url);
  $filename = $hash;

  // Get the FileSystem service.
  $file_system = \Drupal::service('file_system');

  // Check if this is a non-standard file stream and adjust accordingly.
  $scheme = StreamWrapperManager::getScheme($url);
  $default_scheme = \Drupal::config('system.file')
    ->get('default_scheme');
  if ($scheme == $default_scheme) {

    // Don't try fetch already existing files on local file system.
    return $url;
  }
  elseif ($scheme != 'http' && $scheme != 'https') {

    // Obtain the "real" URL to this file.
    $url = $file_system
      ->realpath($url);
  }

  // Parse the url to get the path components.
  $url_parameters = UrlHelper::parse($url);

  // Add the extension for real images.
  if ($extension = strtolower(pathinfo($url_parameters['path'], PATHINFO_EXTENSION))) {
    if (in_array($extension, [
      'jpg',
      'png',
      'gif',
      'jpeg',
    ])) {
      $filename .= '.' . $extension;
    }
  }
  else {

    // Use jpg as default extension.
    $filename .= $config
      ->get('imagecache_default_extension');
  }
  $directory = $default_scheme . '://' . $config
    ->get('imagecache_directory');
  if ($file_system
    ->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
    $needs_refresh = FALSE;
    $filepath = $directory . '/' . $filename;

    // Allow modules to add custom logic to check if it needs to be re-fetched.
    \Drupal::moduleHandler()
      ->alter('imagecache_external_needs_refresh', $needs_refresh, $filepath);
    if ($needs_refresh === FALSE) {
      return $filepath;
    }
    elseif ($filepath = imagecache_external_fetch($url, $directory . '/' . $filename)) {
      return $filepath;
    }
  }

  // We couldn't get the file.
  return FALSE;
}

/**
 * Implements hook_imagecache_external_needs_refresh_alter().
 */
function imagecache_external_imagecache_external_needs_refresh_alter(&$needs_refresh, $filepath) {
  if (!file_exists($filepath)) {
    $needs_refresh = TRUE;
  }
}

/**
 * Api function to fetch a url.
 *
 * @param string $url
 *   The url to fetch.
 * @param string $cachepath
 *   The directory where to save the images within the files directory.
 *
 * @return bool|string
 *   Either the URI of the file, e.g. public://directory/file.jpg. or FALSE.
 * @throws
 */
function imagecache_external_fetch($url, $cachepath) {

  // Validate the image URL against the whitelist.
  if (imagecache_external_validate_host($url) === FALSE) {
    return FALSE;
  }

  // Drupal config object.
  $config = imagecache_external_config();
  try {

    // Drupal httpClient.
    $http = \Drupal::httpClient();
    $result = $http
      ->request('get', $url);
    $code = floor($result
      ->getStatusCode() / 100) * 100;
    $types = imagecache_external_allowed_mimetypes();

    // If content-type not set, use the default 'application/octet-stream'.
    $response_mimetype = $result
      ->getHeaderLine('content-type') ? strtolower($result
      ->getHeaderLine('content-type')) : 'application/octet-stream';

    // Add extension to the cached file to allow file_entity to use it for
    // mimetype identification.
    $cachepath_ext = pathinfo($cachepath, PATHINFO_EXTENSION);
    $default_extension = $config
      ->get('imagecache_default_extension');
    if (!$cachepath_ext && $default_extension != '') {
      $cachepath .= $default_extension;
    }

    // Explode content-type to handle mimetypes with more than one
    // property (eg. image/jpeg;charset=UTF-8).
    $content_type_array = explode(';', $response_mimetype);
    $content_type_allowed = FALSE;
    foreach ($content_type_array as $content_type) {
      if (in_array(strtolower($content_type), $types)) {
        $content_type_allowed = TRUE;
      }
    }
    if (!empty($result
      ->getBody()) && $code != 400 && $code != 500 && $content_type_allowed) {
      if ($config
        ->get('imagecache_external_management') == 'unmanaged') {
        return \Drupal::service('file_system')
          ->saveData($result
          ->getBody(), $cachepath, FileSystemInterface::EXISTS_REPLACE);
      }
      else {
        $file = file_save_data($result
          ->getBody(), $cachepath, FileSystemInterface::EXISTS_REPLACE);
        return $file
          ->getFileUri();
      }
    }
    else {
      throw new Exception('Image could not be retrieved');
    }
  } catch (Exception $e) {
    $fallback_image_fid = $config
      ->get('imagecache_fallback_image');
    if (!empty($fallback_image_fid) && ($fallback_image = File::load(reset($fallback_image_fid)))) {
      return $fallback_image
        ->getFileUri();
    }
    \Drupal::logger('imagecache_external')
      ->notice(t('The image %url could not be retrieved', [
      '%url' => $url,
    ]));
    return FALSE;
  }
}

/**
 * Helper function to validate the image host against the whitelist.
 *
 * @param string $url
 *   The URL of the image.
 *
 * @return bool
 *   Can the image be fetched or not?
 */
function imagecache_external_validate_host($url) {
  $config = imagecache_external_config();

  // Extract the hostname from the url.
  if (!($host = parse_url($url, PHP_URL_HOST))) {
    return FALSE;
  }

  // Check if a whitelist is used and if the host is in the list.
  if ($config
    ->get('imagecache_external_use_whitelist')) {
    $list = preg_split('/\\s+/', $config
      ->get('imagecache_external_hosts'));
    $validhost = FALSE;
    foreach ($list as $ext_host) {
      if (preg_match('/\\.?' . $ext_host . '/', $host) == 1) {
        $validhost = TRUE;
        break;
      }
    }
    if (!$validhost) {

      // If we are unsuccessful then log a message in watchdog.
      \Drupal::logger('imagecache_external')
        ->notice(t('The image %url could not be retrieved, it did not meet the whitelist requirements.', [
        '%url' => $url,
      ]));
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Implements hook_module_implements_alter().
 *
 * Because the Image module already checks for the image style paths,
 * and returns and access_denied() for Imagecache External images,
 * we need to override this function and do the check ourselves.
 */
function imagecache_external_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'file_download') {
    unset($implementations['image']);
  }
}

/**
 * Implements hook_file_download().
 *
 * When using the private file system, we have to let Drupal know it's OK to
 * download images from our Imagecache External directory.
 */
function imagecache_external_file_download($uri) {

  // Check if the path contains 'imagecache/external'.
  // If not, we fallback to the Image module.
  if (strpos($uri, '/' . imagecache_external_config()
    ->get('imagecache_directory') . '/') > 0) {

    /** @var \Drupal\Core\Image\Image $image */
    $image = Drupal::service('image.factory')
      ->get($uri);

    // For safety, we only allow our own mimetypes.
    if (in_array($image
      ->getMimeType(), imagecache_external_allowed_mimetypes())) {
      return [
        'Content-Type' => $image
          ->getMimeType(),
        'Content-Length' => $image
          ->getFileSize(),
      ];
    }
  }
  else {

    // Do a fallback to the Image module.
    return image_file_download($uri);
  }
}

/**
 * Helper function to get externals directory.
 *
 * @return string
 *   The path to the external images directory.
 */
function imagecache_external_get_directory_path() {
  $config = imagecache_external_config();
  $scheme = \Drupal::config('system.file')
    ->get('default_scheme');
  $wrapper = \Drupal::service('stream_wrapper_manager')
    ->getViaScheme($scheme);
  $path = realpath($wrapper
    ->getDirectoryPath() . '/' . $config
    ->get('imagecache_directory'));
  return $path;
}

/**
 * Implements hook_cron().
 *
 * Periodically flush caches at configured frequency.
 */
function imagecache_external_cron() {
  $last_cron_flush = \Drupal::state()
    ->get('imagecache_external.last_cron_flush', 0);
  $frequency = \Drupal::config('imagecache_external.settings')
    ->get('imagecache_external_cron_flush_frequency', 0);
  if ($frequency !== 0 && (\Drupal::time()
    ->getRequestTime() - $last_cron_flush) / (3600 * 24) >= $frequency) {
    if (imagecache_external_flush_cache()) {
      \Drupal::state()
        ->set('imagecache_external.last_cron_flush', \Drupal::time()
        ->getRequestTime());
    }
  }
}

/**
 * Helper function to flush caches.
 *
 * @return bool
 *   A Boolean value to indicate that the operation succeeded.
 */
function imagecache_external_flush_cache() {
  $path = imagecache_external_get_directory_path();
  if (is_dir($path)) {
    if (imagecache_external_batch_flush($path)) {
      \Drupal::logger('imagecache_external')
        ->notice('Imagecache caches have been flushed');
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
}

/**
 * The batch callback.
 */
function imagecache_external_batch_flush($path) {
  $files = scandir(imagecache_external_get_directory_path());
  $files = array_diff($files, [
    '..',
    '.',
  ]);
  if (!empty($files)) {
    foreach ($files as &$value) {
      $value = $path . '/' . $value;
    }
    unset($value);
    $config = imagecache_external_config();
    $chunks = array_chunk($files, $config
      ->get('imagecache_external_batch_flush_limit'), TRUE);
    $count_chunks = count($chunks);
    $i = '';
    foreach ($chunks as $chunk) {
      $i++;
      $operations[] = [
        'imagecache_external_batch_flush_process',
        [
          $chunk,
          'details' => t('(Deleting file batch @chunk of @count)', [
            '@chunk ' => $i,
            '@count' => $count_chunks,
          ]),
        ],
      ];
    }
    $batch = [
      'operations' => $operations,
      'finished' => 'imagecache_external_batch_flush_finished',
      'title' => t('Deleting Imagecache External files...'),
      'init_message' => t('Bulk delete is starting...'),
      'error_message' => t('Imagecache external batch flush has encountered an error.'),
    ];
    batch_set($batch);
    return TRUE;
  }
  elseif (empty($files)) {
    $directory = imagecache_external_get_directory_path();
    \Drupal::service('file_system')
      ->deleteRecursive($directory);
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * The batch processor.
 */
function imagecache_external_batch_flush_process($chunk, $operation_details, &$context) {
  foreach ($chunk as $file) {
    \Drupal::service('file_system')
      ->deleteRecursive($file);
  }

  // Show what chunk is currently being processed.
  $context['message'] = $operation_details;
}

/**
 * The batch finish handler.
 */
function imagecache_external_batch_flush_finished($success, $files, $operations) {
  if ($success) {
    $directory = imagecache_external_get_directory_path();
    \Drupal::service('file_system')
      ->deleteRecursive($directory);
  }
  else {
    $error_operation = reset($operations);
    $message = t('An error occurred while processing %error_operation with arguments: @arguments', [
      '%error_operation' => $error_operation[0],
      '@arguments' => print_r($error_operation[1], TRUE),
    ]);
    \Drupal::messenger()
      ->addMessage($message, MessengerInterface::TYPE_ERROR);
  }
}

/**
 * Helper function that returns allowed mimetypes for external caching.
 *
 * @return array
 *   The allowed mimetypes.
 */
function imagecache_external_allowed_mimetypes() {
  return imagecache_external_config()
    ->get('imagecache_external_allowed_mimetypes');
}

/**
 * Helper function that returns a config object for imagecache_external.
 *
 * @return \Drupal\Core\Config\ImmutableConfig
 *   An immutable configuration object.
 */
function imagecache_external_config() {
  $config = \Drupal::config('imagecache_external.settings');
  return $config;
}

Functions

Namesort descending Description
imagecache_external_allowed_mimetypes Helper function that returns allowed mimetypes for external caching.
imagecache_external_batch_flush The batch callback.
imagecache_external_batch_flush_finished The batch finish handler.
imagecache_external_batch_flush_process The batch processor.
imagecache_external_config Helper function that returns a config object for imagecache_external.
imagecache_external_cron Implements hook_cron().
imagecache_external_fetch Api function to fetch a url.
imagecache_external_file_download Implements hook_file_download().
imagecache_external_flush_cache Helper function to flush caches.
imagecache_external_generate_path Util to generate a path to an image.
imagecache_external_get_directory_path Helper function to get externals directory.
imagecache_external_imagecache_external_needs_refresh_alter Implements hook_imagecache_external_needs_refresh_alter().
imagecache_external_module_implements_alter Implements hook_module_implements_alter().
imagecache_external_theme Implements hook_theme().
imagecache_external_validate_host Helper function to validate the image host against the whitelist.
template_preprocess_imagecache_external Returns HTML for an image using a specific image style.
template_preprocess_imagecache_external_responsive Returns HTML for an image using a specific responsive image style.