You are here

youtube.module in YouTube Field 8

Same filename and directory in other branches
  1. 7 youtube.module

Youtube field module adds a field for YouTube videos.

File

youtube.module
View source
<?php

/**
 * @file
 * Youtube field module adds a field for YouTube videos.
 */
use GuzzleHttp\Exception\RequestException;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\field\Entity\FieldConfig;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;

/**
 * Implements hook_menu_link_defaults().
 */
function youtube_menu_link_defaults() {
  $links = [];
  $links['youtube.settings'] = [
    'link_title' => 'YouTube Field settings',
    'description' => 'Configure global settings for YouTube fields.',
    'route_name' => 'youtube.settings',
    'parent' => 'system.admin.config.media',
  ];
  return $links;
}

/**
 * Extracts the video_id from the submitted field value.
 *
 * @param string $input
 *   The input submitted to the field.
 *
 * @return string|bool
 *   Returns the video_id if available, or FALSE if not.
 */
function youtube_get_video_id($input) {

  // See README.txt for accepted URL formats.
  preg_match("/^(?:http(?:s)?:\\/\\/)?(?:www\\.)?(?:youtu\\.be\\/|youtube\\.com\\/(?:(?:watch)?\\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\\/))([^\\?&\"'<> ]+)/", $input, $matches);
  if (!empty($matches[1])) {
    $video_id = $matches[1];
    return $video_id;
  }
  return FALSE;
}

/**
 * Returns a list of standard YouTube video sizes.
 */
function youtube_size_options() {
  return [
    '450x315' => '450px by 315px',
    '480x360' => '480px by 360px',
    '640x480' => '640px by 480px',
    '960x720' => '960px by 720px',
    'responsive' => 'responsive (full-width of container)',
    'custom' => 'custom',
  ];
}

/**
 * Implements hook_theme().
 */
function youtube_theme($existing, $type, $theme, $path) {
  return [
    'youtube_thumbnail' => [
      'variables' => [
        'video_id' => NULL,
        'entity_title' => NULL,
        'image_style' => NULL,
        'image_link' => NULL,
        'image' => NULL,
      ],
    ],
    'youtube_video' => [
      'variables' => [
        'input' => NULL,
        'video_id' => NULL,
        'entity_title' => NULL,
        'settings' => [],
        'alternative_content' => NULL,
      ],
    ],
  ];
}

/**
 * Prepares variables for the YouTube Video template.
 *
 * Default template: youtube-video.html.twig.
 */
function template_preprocess_youtube_video(&$variables) {
  $config = \Drupal::config('youtube.settings');

  // Add global YouTube module configuration to the settings array.
  $variables['settings'] += [
    'youtube_suggest' => $config
      ->get('youtube_suggest'),
    'youtube_privacy' => $config
      ->get('youtube_privacy'),
    'youtube_player_class' => $config
      ->get('youtube_player_class'),
    'youtube_modestbranding' => $config
      ->get('youtube_modestbranding'),
    'youtube_theme' => $config
      ->get('youtube_theme'),
    'youtube_color' => $config
      ->get('youtube_color'),
    'youtube_enablejsapi' => $config
      ->get('youtube_enablejsapi'),
    'youtube_wmode' => $config
      ->get('youtube_wmode'),
  ];

  // Build the query for the embedded video using module and field settings.
  $query = [];
  if (!$variables['settings']['youtube_suggest']) {
    $query['rel'] = '0';
  }
  if ($variables['settings']['youtube_modestbranding']) {
    $query['modestbranding'] = '1';
  }
  if ($variables['settings']['youtube_theme']) {
    $query['theme'] = 'light';
  }
  if ($variables['settings']['youtube_color']) {
    $query['color'] = 'white';
  }
  if ($variables['settings']['youtube_enablejsapi']) {
    global $base_root;
    $query['enablejsapi'] = '1';
    $query['origin'] = $base_root;
  }
  if ($variables['settings']['youtube_wmode']) {
    $query['wmode'] = 'opaque';
  }
  if ($variables['settings']['youtube_autoplay']) {
    $query['autoplay'] = '1';
  }
  if ($variables['settings']['youtube_mute']) {
    $query['mute'] = '1';
  }
  if ($variables['settings']['youtube_loop']) {
    $query['loop'] = '1';
    $query['playlist'] = $variables['video_id'];
  }
  if ($variables['settings']['youtube_controls']) {
    $query['controls'] = '0';
  }
  if ($variables['settings']['youtube_autohide']) {
    $query['autohide'] = '1';
  }
  if ($variables['settings']['youtube_iv_load_policy']) {
    $query['iv_load_policy'] = '3';
  }

  // If the override setting is enabled, add any additional parameters provided
  // in the initial field value to the query of the embedded video.
  if ($config
    ->get('youtube_override')) {
    if ($url_parts = UrlHelper::parse($variables['input'])) {
      foreach ($url_parts['query'] as $key => $value) {
        if ($key == 'v') {
          continue;
        }
        $query[$key] = $value;
      }
    }
  }

  // Use the module's privacy configuration to determine the domain.
  $domain = !$variables['settings']['youtube_privacy'] ? 'youtube.com' : 'youtube-nocookie.com';
  $path = 'https://www.' . $domain . '/embed/' . $variables['video_id'];

  // Build the src attribute with the path and query array constructed above.
  $url = Url::fromUri($path, [
    'query' => $query,
  ]);
  $variables['content_attributes']['src'] = $url
    ->toString();

  // Use the field's display settings to retrieve the video's dimensions.
  $size = $variables['settings']['youtube_size'];
  $width = $variables['settings']['youtube_width'];
  $height = $variables['settings']['youtube_height'];
  if ($size != 'responsive') {
    $dimensions = youtube_get_dimensions($size, $width, $height);

    // Assign the retrieved dimensions as attributes on the iframe element.
    $variables['content_attributes']['width'] = $dimensions['width'];
    $variables['content_attributes']['height'] = $dimensions['height'];
  }

  // Build the iframe element's id and class attribute values.
  $class = $variables['settings']['youtube_player_class'];
  $id = Html::getUniqueId($class);
  $variables['content_attributes']['id'] = $id;
  $variables['content_attributes']['class'][] = Html::getClass($class);

  // Build the iframe element's title attribute value (for accessibility).
  $variables['content_attributes']['title'] = t('Embedded video');
  if (!empty($variables['entity_title'])) {
    $variables['content_attributes']['title'] = t('Embedded video for @entity_title', [
      '@entity_title' => $variables['entity_title'],
    ]);
  }

  // Alternative content for browsers that don't understand iframes (WCAG).
  $variables['content_attributes']['aria-label'] = $variables['content_attributes']['title'] . ': ' . $url
    ->toString();

  // Remove iframe border.
  $variables['content_attributes']['frameborder'] = 0;

  // Add classes to the wrapping element.
  $variables['attributes']['class'][] = 'youtube-container';
  if ($size == 'responsive') {

    // When the "responsive" size is chosen in the field's display settings,
    // this class is used by the module's CSS to make the player responsive.
    $variables['attributes']['class'][] = 'youtube-container--responsive';
  }
}

/**
 * Prepares variables for the YouTube Thumbnail template.
 *
 * Default template: youtube-thumbnail.html.twig.
 */
function template_preprocess_youtube_thumbnail(&$variables) {
  $video_id = $variables['video_id'];
  $image_style = $variables['image_style'];

  // Build the image element's alt attribute value (for accessibility).
  $alt = t('Embedded thumbnail');
  if (!empty($variables['entity_title'])) {
    $alt .= ' ' . t('for @entity_title', [
      '@entity_title' => $variables['entity_title'],
    ]);
  }

  // Check to see if a thumbnail exists locally.
  $uri = youtube_build_thumbnail_uri($video_id);
  if (!file_exists($uri)) {

    // Retrieve the image from YouTube.
    if (!youtube_get_remote_image($video_id)) {

      // Use the remote source if local copy fails.
      $uri = youtube_build_remote_image_path($video_id);
    }
  }

  // Build the initial image render array.
  $variables['image'] = [
    '#theme' => 'image',
    '#uri' => $uri,
    '#alt' => $alt,
  ];

  // If an image style has been chosen in the field's display settings, alter
  // the render array to use that image style. Remote images cannot be rendered
  // through an image style.
  if ($image_style && empty($remote_uri)) {
    $variables['image']['#theme'] = 'image_style';
    $variables['image']['#style_name'] = $image_style;
  }

  // If a URL path is provided, create a link with the image and that path.
  if ($variables['image_link'] != NULL) {
    $variables['image'] = Link::fromTextAndUrl($variables['image'], $variables['image_link']);
  }
}

/**
 * Splits height and width when given size, as from youtube_size_options.
 *
 * @param string $size
 *   Image size.
 * @param string $width
 *   Image width.
 * @param string $height
 *   Image height.
 *
 * @return array
 *   An array containing the dimensions.
 */
function youtube_get_dimensions($size = NULL, $width = NULL, $height = NULL) {
  $dimensions = [];
  if ($size == 'custom') {
    $dimensions['width'] = (int) $width;
    $dimensions['height'] = (int) $height;
  }
  else {

    // Locate the 'x'.
    $strpos = strpos($size, 'x');

    // Width is the first dimension.
    $dimensions['width'] = substr($size, 0, $strpos);

    // Height is the second dimension.
    $dimensions['height'] = substr($size, $strpos + 1, strlen($size));
  }
  return $dimensions;
}

/**
 * Retrieves the thumbnail image for a given video from YouTube.
 *
 * @param int $video_id
 *   The video ID of the particular YouTube video.
 * @param bool $force_small
 *   (optional) When TRUE, this function should return the standard size image
 *   regardless of what the youtube_thumb_hires variable is set to. This is used
 *   should the high resolution image be found to not exist for a particular
 *   video.
 *
 * @return bool|object
 *   Either the Drupal $file object of saved image, or FALSE if the save failed.
 */
function youtube_get_remote_image($video_id = NULL, $force_small = FALSE) {

  // This value is TRUE when higher resolution thumbnails should be saved.
  // This resolution is not guaranteed to exist and if it doesn't, the smaller
  // resolution image will be saved in its place.
  $youtube_thumb_hires = \Drupal::config('youtube.settings')
    ->get('youtube_thumb_hires');

  // This boolean is TRUE if we're obtaining a hi-res thumbnail.
  $get_hires = $youtube_thumb_hires && !$force_small;

  // Build the image url.
  if ($get_hires) {
    $src = youtube_build_remote_image_path($video_id, 'maxresdefault');
  }
  else {
    $src = youtube_build_remote_image_path($video_id);
  }

  // Make the request for the file.
  try {
    $image_request = Drupal::httpClient()
      ->get($src);
  } catch (RequestException $e) {

    // The high resolution image didn't exist and the request was a 404. Force
    // it to try again, but look for the smaller resolution image.
    if ($get_hires) {
      return youtube_get_remote_image($video_id, TRUE);
    }
    watchdog_exception('youtube', $e);
    return FALSE;
  }
  $youtube_thumb_uri = youtube_build_thumbnail_uri();
  if (!\Drupal::service('file_system')
    ->prepareDirectory($youtube_thumb_uri, FileSystemInterface::CREATE_DIRECTORY) && !mkdir($youtube_thumb_uri, 0775, TRUE)) {
    \Drupal::service('logger.factory')
      ->get('youtube')
      ->error('Failed to create YouTube thumbnail directory: %dir', [
      '%dir' => $youtube_thumb_uri,
    ]);
    return FALSE;
  }
  $data = $image_request
    ->getBody(TRUE);
  $destination = youtube_build_thumbnail_uri($video_id);

  // Save the thumbnail and add to Drupal managed files.
  $file = file_save_data($data, $destination, FileSystemInterface::EXISTS_REPLACE);
  if (!$file) {
    \Drupal::service('logger.factory')
      ->get('youtube')
      ->error('Unable to save youtube thumbnail to filesystem for video %id', [
      '%id' => $video_id,
    ]);
    return FALSE;
  }
  return $file;
}

/**
 * Deletes all existing thumbnail image files.
 *
 * This is a submit callback for the "Refresh" option in the configuration form.
 */
function youtube_thumb_delete_all($form, &$form_state) {
  $youtube_thumb_uri = youtube_build_thumbnail_uri();
  $file_system = \Drupal::service('file_system');
  if (!$file_system
    ->prepareDirectory($youtube_thumb_uri)) {
    return \Drupal::messenger()
      ->addMessage(t('No files deleted.'));
  }
  $files = $file_system
    ->scanDirectory($youtube_thumb_uri, '/^.*\\.(jpg|png)$/');
  foreach ($files as $raw_file) {
    $fid = \Drupal::database()
      ->select('file_managed')
      ->fields('file_managed', [
      'fid',
    ])
      ->condition('uri', '%' . $raw_file->uri, 'LIKE')
      ->execute()
      ->fetchField();
    if ($fid) {
      $managed_file = File::load($fid);
    }
    if (!$managed_file) {
      $file_system
        ->delete($raw_file->uri);
    }
    else {
      $managed_file
        ->delete();
    }
  }
  drupal_flush_all_caches();
}

/**
 * Builds the URI to a given thumbnail or the module's thumbnail directory.
 *
 * @param string $video_id
 *   (optional) The video ID to build the thumbnail URI for.
 *
 * @return string
 *   If a $video_id was supplied, the URI to that video's thumbnail. Otherwise
 *   the URI to the module's thumbnail directory.
 */
function youtube_build_thumbnail_uri($video_id = NULL) {
  $youtube_thumb_dir = \Drupal::config('youtube.settings')
    ->get('youtube_thumb_dir');
  $youtube_thumb_dir = empty($youtube_thumb_dir) ? 'youtube' : $youtube_thumb_dir;
  $youtube_thumb_uri = file_build_uri($youtube_thumb_dir);
  if ($video_id) {
    return $youtube_thumb_uri . '/' . $video_id . '.jpg';
  }
  return $youtube_thumb_uri;
}

/**
 * Get YouTube image path by building correctly formed URL.
 *
 * @param string $video_id
 *   The ID of the video to grab the thumbnail from.
 * @param string $version
 *   Which version of the thumbnail to grab. See $versions for options.
 *
 * @return string|bool
 *   The youtube.com image path to the specified version/video. FALSE if the
 *   supplied version is not an option.
 */
function youtube_build_remote_image_path($video_id = NULL, $version = '0') {

  // The different versions of the image made available by YouTube.
  // http://stackoverflow.com/a/2068371
  $versions = [
    '0',
    'hqdefault',
    'mqdefault',
    'maxresdefault',
    'default',
    '1',
    '2',
    '3',
  ];
  if (!$video_id || !in_array($version, $versions)) {
    return FALSE;
  }
  $version_path = 'http://img.youtube.com/vi/' . $video_id . '/' . $version . '.jpg';
  return Url::fromUri($version_path)
    ->toString();
}

/**
 * Implements hook_token_info_alter().
 *
 * Alters and adds tokens for each youtube field.
 */
function youtube_token_info_alter(&$data) {

  // Get all youtube fields. Gather entity_type and bundle information.
  $youtube_fields = [];
  $entity_field_manager = \Drupal::service('entity_field.manager');
  $field_map = $entity_field_manager
    ->getFieldMap();
  foreach ($field_map as $entity_type_name => $fields) {
    foreach ($fields as $field_name => $field) {
      foreach ($field['bundles'] as $bundle) {
        if ($field['type'] == 'youtube') {
          $youtube_fields[] = [
            'entity_type' => $entity_type_name,
            'bundle' => $bundle,
            'field_name' => $field_name,
          ];
        }
      }
    }
  }
  foreach ($youtube_fields as $field) {
    $field_info = FieldConfig::loadByName($field['entity_type'], $field['bundle'], $field['field_name']);
    $field_label = $field_info ? $field_info
      ->getLabel() : '';

    // Modify the default field token.
    $data['tokens'][$field['entity_type']][$field['field_name']] = [
      'name' => $field_label . t(": Default"),
      'description' => t("The YouTube video field value's Default (or Token if exists) view mode output."),
    ];

    // Add two new tokens.
    $data['tokens'][$field['entity_type']][$field['field_name'] . '__youtube_video_url'] = [
      'name' => $field_label . t(": Video URL"),
      'description' => t("The YouTube video field value's youtube.com URL."),
    ];
    $data['tokens'][$field['entity_type']][$field['field_name'] . '__youtube_image_url'] = [
      'name' => $field_label . t(": Image URL"),
      'description' => t("The YouTube video field value's local image URL."),
    ];
  }
}

/**
 * Implements hook_tokens().
 *
 * @see youtube_tokens_info_alter()
 */
function youtube_tokens($type, $tokens, array $data = [], array $options = []) {
  global $base_url;
  global $base_path;
  $url_options = [
    'absolute' => TRUE,
  ];
  if (isset($options['langcode'])) {
    $url_options['language'] = \Drupal::languageManager()
      ->getLanguage($options['langcode']);
    $langcode = $options['langcode'];
  }
  else {
    $langcode = LanguageInterface::LANGCODE_DEFAULT;
  }
  $sanitize = !empty($options['sanitize']);
  $replacements = [];
  if ($type == 'node' && !empty($data['node'])) {
    $node = $data['node'];
    foreach ($tokens as $name => $original) {
      if (!strpos($name, '__youtube_')) {

        // This isn't a youtube token!
        continue;
      }
      $token_pieces = explode('__', $name);
      if (count($token_pieces) != 2) {
        continue;
      }
      $field_name = $token_pieces[0];
      $token_name = $token_pieces[1];
      switch ($token_name) {
        case 'youtube_video_url':
          $replacements[$original] = '';
          $field = $node->{$field_name};
          if ($video_id = $field->video_id) {
            $replacements[$original] = 'http://www.youtube.com/watch?v=' . $video_id;
          }
          break;
        case 'youtube_image_url':
          $replacements[$original] = '';
          $field = $node->{$field_name};
          if ($video_id = $field->video_id) {
            $file_uri = youtube_build_thumbnail_uri($video_id);
            if (file_exists($file_uri) || youtube_get_remote_image($video_id)) {
              $replacements[$original] = file_create_url($file_uri);
              if ($style_name = \Drupal::config('youtube.settings')
                ->get('youtube_thumb_token_image_style')) {
                $image_style = ImageStyle::load($style_name);
                $derivative_uri = $image_style
                  ->buildUri($file_uri);
                if (!file_exists($derivative_uri)) {
                  $image_style
                    ->createDerivative($file_uri, $derivative_uri);
                }
                $replacements[$original] = file_create_url($derivative_uri);
              }
            }
          }
          break;
      }
    }
  }
  return $replacements;
}

Functions

Namesort descending Description
template_preprocess_youtube_thumbnail Prepares variables for the YouTube Thumbnail template.
template_preprocess_youtube_video Prepares variables for the YouTube Video template.
youtube_build_remote_image_path Get YouTube image path by building correctly formed URL.
youtube_build_thumbnail_uri Builds the URI to a given thumbnail or the module's thumbnail directory.
youtube_get_dimensions Splits height and width when given size, as from youtube_size_options.
youtube_get_remote_image Retrieves the thumbnail image for a given video from YouTube.
youtube_get_video_id Extracts the video_id from the submitted field value.
youtube_menu_link_defaults Implements hook_menu_link_defaults().
youtube_size_options Returns a list of standard YouTube video sizes.
youtube_theme Implements hook_theme().
youtube_thumb_delete_all Deletes all existing thumbnail image files.
youtube_tokens Implements hook_tokens().
youtube_token_info_alter Implements hook_token_info_alter().