You are here

class FileLinkItem in File Link 2.0.x

Same name and namespace in other branches
  1. 8 src/Plugin/Field/FieldType/FileLinkItem.php \Drupal\file_link\Plugin\Field\FieldType\FileLinkItem

Implements a 'file_link' plugin field type.

This field type is an extension of 'link' filed-type that points only to files, not to directories and, additionally, stores some meta-data related to targeted file, like size and mime-type.

Plugin annotation


@FieldType(
  id = "file_link",
  label = @Translation("File Link"),
  description = @Translation("Stores a URL string pointing to a file, optional varchar link text, and file metadata, like size and mime-type."),
  default_widget = "file_link_default",
  default_formatter = "link",
  constraints = {
     "LinkAccess" = {},
     "LinkToFile" = {},
     "LinkExternalProtocols" = {},
     "LinkNotExistingInternal" = {}
  }
)

Hierarchy

Expanded class hierarchy of FileLinkItem

2 files declare their use of FileLinkItem
FileLinkDeferredTest.php in tests/src/Kernel/FileLinkDeferredTest.php
LinkToFileConstraint.php in src/Plugin/Validation/Constraint/LinkToFileConstraint.php

File

src/Plugin/Field/FieldType/FileLinkItem.php, line 42

Namespace

Drupal\file_link\Plugin\Field\FieldType
View source
class FileLinkItem extends LinkItem implements FileLinkInterface {

  /**
   * The HTTP response of the last client request, if any.
   *
   * @var \Psr\Http\Message\ResponseInterface
   */
  protected $response = NULL;

  /**
   * The exception throw by the the last HTTP client request, if any.
   *
   * @var \GuzzleHttp\Exception\RequestException
   */
  protected $exception = NULL;

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The HTTP client service.
   *
   * @var \GuzzleHttp\Client
   */
  protected $httpClient;

  /**
   * The queue to use when deferring the request to get the metadata.
   *
   * @var \Drupal\Core\Queue\QueueInterface
   */
  protected $queue;

  /**
   * Save the needs parsing state as a property.
   *
   * @var bool
   */
  protected $needsParsing = FALSE;

  /**
   * Keep the already queued entities in a static cache.
   *
   * @var array
   */
  protected static $queued = [];

  /**
   * {@inheritdoc}
   */
  public static function defaultFieldSettings() {
    return [
      'file_extensions' => 'txt',
      'no_extension' => FALSE,
      'deferred_request' => FALSE,
    ] + parent::defaultFieldSettings();
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    return parent::propertyDefinitions($field_definition) + [
      'size' => DataDefinition::create('integer')
        ->setLabel(t('Size')),
      'format' => DataDefinition::create('string')
        ->setLabel(t('Format')),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    $schema = parent::schema($field_definition);
    $schema['columns']['size'] = [
      'description' => 'The size of the file.',
      'type' => 'int',
      'size' => 'big',
      'unsigned' => TRUE,
    ];
    $schema['columns']['format'] = [
      'description' => 'The format of the file.',
      'type' => 'varchar',
      'length' => 255,
    ];
    $schema['indexes']['size'] = [
      'size',
    ];
    $schema['indexes']['format'] = [
      'format',
    ];
    return $schema;
  }

  /**
   * {@inheritdoc}
   */
  public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
    $element = parent::fieldSettingsForm($form, $form_state);

    // Make the extension list a little more human-friendly by comma-separation.
    $extensions = str_replace(' ', ', ', $this
      ->getSetting('file_extensions'));
    $element['file_extensions'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Allowed file extensions'),
      '#default_value' => $extensions,
      '#description' => $this
        ->t('Separate extensions with a space or comma and do not include the leading dot. Leave empty to allow any extension.'),
      // Use the 'file' field type validator.
      '#element_validate' => [
        [
          FileItem::class,
          'validateExtensions',
        ],
      ],
      '#maxlength' => 256,
    ];
    $element['no_extension'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Allow URLs without file extension'),
      '#description' => $this
        ->t('The link can refer a document such as a wiki page or a dynamic generated page that has no extension. Check this if you want to allow such URLs.'),
      '#default_value' => $this
        ->getSetting('no_extension'),
    ];
    $element['deferred_request'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Defer requests to cron'),
      '#description' => $this
        ->t('The link will not be checked and validated immediately, instead the entity will be updated by cron and the size and format will be determined at that time. Check the logs for errors'),
      '#default_value' => $this
        ->getSetting('deferred_request'),
    ];
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
    $values = parent::generateSampleValue($field_definition);
    $values['size'] = 1234567;
    $values['format'] = 'image/png';
    return $values;
  }

  /**
   * {@inheritdoc}
   */
  public function preSave() {
    parent::preSave();

    // Skip performing HTTP requests, useful when running bulk imports.
    if (Settings::get('file_link.disable_http_requests', FALSE)) {
      return;
    }
    $entity = $this
      ->getEntity();
    $storage = $this
      ->getEntityTypeManager()
      ->getStorage($entity
      ->getEntityTypeId());

    /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
    $original = $entity
      ->isNew() ? NULL : $storage
      ->loadUnchanged($entity
      ->id());
    $field_name = $this
      ->getFieldDefinition()
      ->getName();
    $original_uri = NULL;
    $size = NULL;
    $format = NULL;
    if ($original !== NULL) {
      $item = $original
        ->get($field_name)
        ->get($this
        ->getName());
      if ($item != NULL) {
        $values = $item
          ->getValue();
        $original_uri = $values['uri'];
        $size = $values['size'];
        $format = $values['format'];
      }
    }

    // We parse the metadata in any of the next cases:
    // - The host entity is new.
    // - The 'file_link' URI has changed.
    // - Stored metadata is empty, possible due to an previous failure. We try
    //   again to parse, hoping the connection was fixed in the meantime.
    $this->needsParsing = $entity
      ->isNew() || $this->uri !== $original_uri || empty($size) || empty($format);
    if ($this->needsParsing) {
      if ($this
        ->needsQueue()) {

        // We set the needs_parsing property and check it in ::postSave.
        // Now just reset the values, cron will set them later.
        $this
          ->writePropertyValue('size', NULL);
        $this
          ->writePropertyValue('format', NULL);
        return;
      }

      // Don't throw exceptions on HTTP level errors (e.g. 404, 403, etc).
      $options = [
        'exceptions' => FALSE,
        'allow_redirects' => [
          'strict' => TRUE,
        ],
      ];
      $url = Url::fromUri($this->uri, [
        'absolute' => TRUE,
      ])
        ->toString();

      // Clear any previous stored results (response and/or exception).
      $this
        ->clearResponse();
      $this
        ->clearException();
      try {

        // Perform only a HEAD method to save bandwidth.
        $this
          ->setResponse($this
          ->getHttpClient()
          ->head($url, $options));
      } catch (RequestException $request_exception) {
        $this
          ->setException($request_exception);
      }
      $format = NULL;
      $size = 0;
      if (!$this
        ->getException() && ($response = $this
        ->getResponse()) && $this
        ->isSupportedResponse($response)) {
        if ($response
          ->hasHeader('Content-Type')) {

          // The format may have the pattern 'text/html; charset=UTF-8'. In this
          // case, keep only the first relevant part.
          $format = explode(';', $response
            ->getHeaderLine('Content-Type'))[0];
        }
        else {
          $format = NULL;
        }
        if ($response
          ->hasHeader('Content-Length')) {
          $size = (int) $response
            ->getHeaderLine('Content-Length');
        }
        else {

          // The server didn't sent the Content-Length header. In this case,
          // perform a full GET and measure the size of the returned body.
          $response = $this
            ->getHttpClient()
            ->get($url, $options);
          $size = (int) $response
            ->getBody()
            ->getSize();
          $this
            ->setResponse($response);
        }
        $this
          ->writePropertyValue('size', $size);
        $this
          ->writePropertyValue('format', $format);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function postSave($update) {
    if ($this->needsParsing && $this
      ->needsQueue()) {

      // We need to queue the entity update in postSave because we need to
      // know the entity id so that we can load it in cron and re-save it.
      $entity = $this
        ->getEntity();
      $rev = $entity instanceof RevisionableInterface ? $entity
        ->getRevisionId() : NULL;
      $item = new FileLinkQueueItem($entity
        ->getEntityTypeId(), $entity
        ->id(), $entity
        ->language()
        ->getId(), $rev);
      if (!in_array($item
        ->getKey(), static::$queued)) {
        $this
          ->getQueue()
          ->createItem($item);

        // Save the queued entity in a static cache so that we don't queue it
        // more than once when using a multi-value field.
        static::$queued[] = $item
          ->getKey();
      }
    }
    return parent::postSave($update);
  }

  /**
   * {@inheritdoc}
   */
  public function getSize() {
    return $this
      ->get('size')
      ->getValue();
  }

  /**
   * {@inheritdoc}
   */
  public function getFormat() {
    return $this
      ->get('format')
      ->getValue();
  }

  /**
   * {@inheritdoc}
   */
  public function setResponse(ResponseInterface $response) {
    $this->response = $response;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getResponse() {
    return $this->response;
  }

  /**
   * {@inheritdoc}
   */
  public function clearResponse() {
    $this->response = NULL;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setException(RequestException $exception) {
    $this->exception = $exception;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getException() {
    return $this->exception;
  }

  /**
   * {@inheritdoc}
   */
  public function clearException() {
    $this->exception = NULL;
    return $this;
  }

  /**
   * Returns the entity type manager service.
   *
   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
   *   The entity type manager service.
   */
  protected function getEntityTypeManager() {
    if (!isset($this->entityTypeManager)) {
      $this->entityTypeManager = \Drupal::entityTypeManager();
    }
    return $this->entityTypeManager;
  }

  /**
   * Returns the HTTP client service.
   *
   * @return \GuzzleHttp\Client
   *   The Guzzle client.
   */
  protected function getHttpClient() {
    if (!isset($this->httpClient)) {
      $this->httpClient = \Drupal::httpClient();
    }
    return $this->httpClient;
  }

  /**
   * Returns the queue.
   *
   * @return \Drupal\Core\Queue\QueueInterface
   *   The queue for deferred entity updates.
   */
  protected function getQueue() : QueueInterface {
    if (!isset($this->queue)) {
      $this->queue = \Drupal::queue('file_link_metadata_update');
      $this->queue
        ->createQueue();
    }
    return $this->queue;
  }

  /**
   * Check to see if a queue is needed.
   *
   * @return bool
   *   True if the field is set up to defer requests and cron is not processing.
   */
  protected function needsQueue() : bool {
    return !FileLinkMetadataUpdate::isProcessing() && $this
      ->getFieldDefinition()
      ->getSetting('deferred_request');
  }

  /**
   * Check whereas given response is supported by field type.
   *
   * @param \Psr\Http\Message\ResponseInterface $response
   *   Response object.
   *
   * @return bool
   *   TRUE if supported, FALSE otherwise.
   */
  protected function isSupportedResponse(ResponseInterface $response) {
    return in_array($response
      ->getStatusCode(), [
      '200',
      '301',
      '302',
    ]);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
FieldItemBase::calculateDependencies public static function Calculates dependencies for field items. Overrides FieldItemInterface::calculateDependencies 2
FieldItemBase::calculateStorageDependencies public static function Calculates dependencies for field items on the storage level. Overrides FieldItemInterface::calculateStorageDependencies 1
FieldItemBase::defaultStorageSettings public static function Defines the storage-level settings for this plugin. Overrides FieldItemInterface::defaultStorageSettings 10
FieldItemBase::delete public function Defines custom delete behavior for field values. Overrides FieldItemInterface::delete 2
FieldItemBase::deleteRevision public function Defines custom revision delete behavior for field values. Overrides FieldItemInterface::deleteRevision
FieldItemBase::fieldSettingsFromConfigData public static function Returns a settings array in the field type's canonical representation. Overrides FieldItemInterface::fieldSettingsFromConfigData 1
FieldItemBase::fieldSettingsToConfigData public static function Returns a settings array that can be stored as a configuration value. Overrides FieldItemInterface::fieldSettingsToConfigData 1
FieldItemBase::getEntity public function Gets the entity that field belongs to. Overrides FieldItemInterface::getEntity
FieldItemBase::getFieldDefinition public function Gets the field definition. Overrides FieldItemInterface::getFieldDefinition
FieldItemBase::getLangcode public function Gets the langcode of the field values held in the object. Overrides FieldItemInterface::getLangcode
FieldItemBase::getSetting protected function Returns the value of a field setting.
FieldItemBase::getSettings protected function Returns the array of field settings.
FieldItemBase::onDependencyRemoval public static function Informs the plugin that a dependency of the field will be deleted. Overrides FieldItemInterface::onDependencyRemoval 1
FieldItemBase::storageSettingsForm public function Returns a form for the storage-level settings. Overrides FieldItemInterface::storageSettingsForm 8
FieldItemBase::storageSettingsFromConfigData public static function Returns a settings array in the field type's canonical representation. Overrides FieldItemInterface::storageSettingsFromConfigData 2
FieldItemBase::storageSettingsToConfigData public static function Returns a settings array that can be stored as a configuration value. Overrides FieldItemInterface::storageSettingsToConfigData 2
FieldItemBase::view public function Returns a renderable array for a single field item. Overrides FieldItemInterface::view
FieldItemBase::writePropertyValue protected function Different to the parent Map class, we avoid creating property objects as far as possible in order to optimize performance. Thus we just update $this->values if no property object has been created yet. Overrides Map::writePropertyValue
FieldItemBase::__construct public function Constructs a TypedData object given its definition and context. Overrides TypedData::__construct 1
FieldItemBase::__get public function Magic method: Gets a property value. Overrides FieldItemInterface::__get 2
FieldItemBase::__isset public function Magic method: Determines whether a property is set. Overrides FieldItemInterface::__isset
FieldItemBase::__set public function Magic method: Sets a property value. Overrides FieldItemInterface::__set 1
FieldItemBase::__unset public function Magic method: Unsets a property. Overrides FieldItemInterface::__unset
FileLinkItem::$entityTypeManager protected property The entity type manager service.
FileLinkItem::$exception protected property The exception throw by the the last HTTP client request, if any.
FileLinkItem::$httpClient protected property The HTTP client service.
FileLinkItem::$needsParsing protected property Save the needs parsing state as a property.
FileLinkItem::$queue protected property The queue to use when deferring the request to get the metadata.
FileLinkItem::$queued protected static property Keep the already queued entities in a static cache.
FileLinkItem::$response protected property The HTTP response of the last client request, if any.
FileLinkItem::clearException public function Clears a previous stored Guzzle exception. Overrides FileLinkInterface::clearException
FileLinkItem::clearResponse public function Clears a previous stored HTTP response. Overrides FileLinkInterface::clearResponse
FileLinkItem::defaultFieldSettings public static function Defines the field-level settings for this plugin. Overrides LinkItem::defaultFieldSettings
FileLinkItem::fieldSettingsForm public function Returns a form for the field-level settings. Overrides LinkItem::fieldSettingsForm
FileLinkItem::generateSampleValue public static function Generates placeholder field values. Overrides LinkItem::generateSampleValue
FileLinkItem::getEntityTypeManager protected function Returns the entity type manager service.
FileLinkItem::getException public function Gets the last Guzzle client exception. Overrides FileLinkInterface::getException
FileLinkItem::getFormat public function Get file format. Overrides FileLinkInterface::getFormat
FileLinkItem::getHttpClient protected function Returns the HTTP client service.
FileLinkItem::getQueue protected function Returns the queue.
FileLinkItem::getResponse public function Gets the latest stored HTTP response. Overrides FileLinkInterface::getResponse
FileLinkItem::getSize public function Get raw file size. Overrides FileLinkInterface::getSize
FileLinkItem::isSupportedResponse protected function Check whereas given response is supported by field type.
FileLinkItem::needsQueue protected function Check to see if a queue is needed.
FileLinkItem::postSave public function Defines custom post-save behavior for field values. Overrides FieldItemBase::postSave
FileLinkItem::preSave public function Defines custom presave behavior for field values. Overrides FieldItemBase::preSave
FileLinkItem::propertyDefinitions public static function Defines field item properties. Overrides LinkItem::propertyDefinitions
FileLinkItem::schema public static function Returns the schema for the field. Overrides LinkItem::schema
FileLinkItem::setException public function Sets the exception throw by the last HTTP client request. Overrides FileLinkInterface::setException
FileLinkItem::setResponse public function Sets the latest HTTP response. Overrides FileLinkInterface::setResponse
LinkItem::getUrl public function Gets the URL object. Overrides LinkItemInterface::getUrl
LinkItem::isEmpty public function Determines whether the data structure is empty. Overrides Map::isEmpty
LinkItem::isExternal public function Determines if a link is external. Overrides LinkItemInterface::isExternal
LinkItem::mainPropertyName public static function Returns the name of the main property, if any. Overrides FieldItemBase::mainPropertyName
LinkItem::setValue public function Sets the data value. Overrides FieldItemBase::setValue
LinkItemInterface::LINK_EXTERNAL constant Specifies whether the field supports only external URLs.
LinkItemInterface::LINK_GENERIC constant Specifies whether the field supports both internal and external URLs.
LinkItemInterface::LINK_INTERNAL constant Specifies whether the field supports only internal URLs.
Map::$definition protected property The data definition. Overrides TypedData::$definition
Map::$properties protected property The array of properties.
Map::$values protected property An array of values for the contained properties.
Map::applyDefaultValue public function Applies the default value. Overrides TypedData::applyDefaultValue 4
Map::get public function Gets a property object. Overrides ComplexDataInterface::get
Map::getIterator public function
Map::getProperties public function Gets an array of property objects. Overrides ComplexDataInterface::getProperties
Map::getString public function Returns a string representation of the data. Overrides TypedData::getString
Map::getValue public function Gets the data value. Overrides TypedData::getValue 1
Map::onChange public function Overrides TraversableTypedDataInterface::onChange 4
Map::set public function Sets a property value. Overrides ComplexDataInterface::set
Map::toArray public function Returns an array of all property values. Overrides ComplexDataInterface::toArray 1
Map::__clone public function Magic method: Implements a deep clone.
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
TypedData::$name protected property The property name.
TypedData::$parent protected property The parent typed data object.
TypedData::createInstance public static function Constructs a TypedData object given its definition and context. Overrides TypedDataInterface::createInstance
TypedData::getConstraints public function Gets a list of validation constraints. Overrides TypedDataInterface::getConstraints 9
TypedData::getDataDefinition public function Gets the data definition. Overrides TypedDataInterface::getDataDefinition
TypedData::getName public function Returns the name of a property or item. Overrides TypedDataInterface::getName
TypedData::getParent public function Returns the parent data structure; i.e. either complex data or a list. Overrides TypedDataInterface::getParent
TypedData::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition
TypedData::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
TypedData::getPropertyPath public function Returns the property path of the data. Overrides TypedDataInterface::getPropertyPath
TypedData::getRoot public function Returns the root of the typed data tree. Overrides TypedDataInterface::getRoot
TypedData::setContext public function Sets the context of a property or item via a context aware parent. Overrides TypedDataInterface::setContext
TypedData::validate public function Validates the currently set data value. Overrides TypedDataInterface::validate
TypedDataTrait::$typedDataManager protected property The typed data manager used for creating the data types.
TypedDataTrait::getTypedDataManager public function Gets the typed data manager. 2
TypedDataTrait::setTypedDataManager public function Sets the typed data manager. 2