You are here

filefield_paths.module in File (Field) Paths 8

Contains core functions for the File (Field) Paths module.

File

filefield_paths.module
View source
<?php

/**
 * @file
 * Contains core functions for the File (Field) Paths module.
 */
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Symfony\Component\HttpFoundation\Response;

// @TODO - Turn this into a plugin.
require_once __DIR__ . '/filefield_paths.inc';

/**
 * Implements hook_entity_base_field_info().
 */
function filefield_paths_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type
    ->id() == 'file') {
    $fields['origname'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Original filename'))
      ->setDescription(t('Original name of the file with no path components.'));
  }
  return $fields;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function filefield_paths_form_field_config_edit_form_alter(array &$form, FormStateInterface $form_state) {

  /** @var Drupal\field\Entity\FieldConfig $field */
  $field = $form_state
    ->getFormObject()
    ->getEntity();
  $class = $field
    ->getClass();
  if (class_exists($class) && new $class($field
    ->getItemDefinition()) instanceof FileFieldItemList) {
    $entity_info = \Drupal::entityTypeManager()
      ->getDefinition($field
      ->getTargetEntityTypeId());
    $settings = $field
      ->getThirdPartySettings('filefield_paths');
    $form['settings']['filefield_paths'] = [
      '#type' => 'container',
      '#tree' => TRUE,
      '#weight' => 2,
      '#parents' => [
        'third_party_settings',
        'filefield_paths',
      ],
    ];
    $form['settings']['filefield_paths']['enabled'] = [
      '#type' => 'checkbox',
      '#title' => t('Enable File (Field) Paths?'),
      '#default_value' => isset($settings['enabled']) ? $settings['enabled'] : TRUE,
      '#description' => t('File (Field) Paths provides advanced file path and naming options.'),
    ];

    // Hide standard File directory field.
    $form['settings']['file_directory']['#states'] = [
      'visible' => [
        ':input[name="third_party_settings[filefield_paths][enabled]"]' => [
          'checked' => FALSE,
        ],
      ],
    ];

    // File (Field) Paths details element.
    $form['settings']['filefield_paths']['details'] = [
      '#type' => 'details',
      '#title' => t('File (Field) Path settings'),
      '#weight' => 3,
      '#tree' => TRUE,
      '#states' => [
        'visible' => [
          ':input[name="third_party_settings[filefield_paths][enabled]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
      '#parents' => [
        'third_party_settings',
        'filefield_paths',
      ],
    ];

    // Additional File (Field) Paths widget fields.
    $settings_fields = \Drupal::moduleHandler()
      ->invokeAll('filefield_paths_field_settings', [
      $form,
    ]);
    foreach ($settings_fields as $name => $settings_field) {

      // Attach widget fields.
      $form['settings']['filefield_paths']['details'][$name] = [
        '#type' => 'container',
      ];

      // Attach widget field form elements.
      if (isset($settings_field['form']) && is_array($settings_field['form'])) {
        foreach (array_keys($settings_field['form']) as $delta => $key) {
          $form['settings']['filefield_paths']['details'][$name][$key] = $settings_field['form'][$key];
          if (\Drupal::moduleHandler()
            ->moduleExists('token')) {
            $form['settings']['filefield_paths']['details'][$name][$key]['#element_validate'][] = 'token_element_validate';
            $form['settings']['filefield_paths']['details'][$name][$key]['#token_types'] = [
              'date',
              'file',
            ];
            $token_type = \Drupal::service('token.entity_mapper')
              ->getTokenTypeForEntityType($entity_info
              ->id());
            if (!empty($token_type)) {
              $form['settings']['filefield_paths']['details'][$name][$key]['#token_types'][] = $token_type;
            }
          }

          // Fetch stored value from instance.
          if (isset($settings[$name][$key])) {
            $form['settings']['filefield_paths']['details'][$name][$key]['#default_value'] = $settings[$name][$key];
          }
        }

        // Field options.
        $form['settings']['filefield_paths']['details'][$name]['options'] = [
          '#type' => 'details',
          '#title' => t('@title options', [
            '@title' => $settings_field['title'],
          ]),
          '#weight' => 1,
          '#attributes' => [
            'class' => [
              "{$name} cleanup",
            ],
          ],
        ];

        // Cleanup slashes (/).
        $form['settings']['filefield_paths']['details'][$name]['options']['slashes'] = [
          '#type' => 'checkbox',
          '#title' => t('Remove slashes (/) from tokens'),
          '#default_value' => isset($settings[$name]['options']['slashes']) ? $settings[$name]['options']['slashes'] : FALSE,
          '#description' => t('If checked, any slashes (/) in tokens will be removed from %title.', [
            '%title' => $settings_field['title'],
          ]),
        ];

        // Cleanup field with Pathauto module.
        $form['settings']['filefield_paths']['details'][$name]['options']['pathauto'] = [
          '#type' => 'checkbox',
          '#title' => t('Cleanup using Pathauto'),
          '#default_value' => isset($settings[$name]['options']['pathauto']) && \Drupal::moduleHandler()
            ->moduleExists('pathauto') ? $settings[$name]['options']['pathauto'] : FALSE,
          '#description' => t('Cleanup %title using Pathauto.', [
            '%title' => $settings_field['title'],
          ]),
          '#disabled' => TRUE,
        ];
        if (\Drupal::moduleHandler()
          ->moduleExists('pathauto')) {
          unset($form['settings']['filefield_paths']['details'][$name]['options']['pathauto']['#disabled']);
          $form['settings']['filefield_paths']['details'][$name]['options']['pathauto']['#description'] = t('Cleanup %title using <a href="@pathauto">Pathauto settings</a>.', [
            '%title' => $settings_field['title'],
            '@pathauto' => Url::fromRoute('pathauto.settings.form')
              ->toString(),
          ]);
        }

        // Transliterate field.
        $form['settings']['filefield_paths']['details'][$name]['options']['transliterate'] = [
          '#type' => 'checkbox',
          '#title' => t('Transliterate'),
          '#default_value' => isset($settings[$name]['options']['transliterate']) ? $settings[$name]['options']['transliterate'] : 0,
          '#description' => t('Provides one-way string transliteration (romanization) and cleans the %title during upload by replacing unwanted characters.', [
            '%title' => $settings_field['title'],
          ]),
        ];

        // Replacement patterns for field.
        if (\Drupal::moduleHandler()
          ->moduleExists('token')) {
          $form['settings']['filefield_paths']['details']['token_tree'] = [
            '#theme' => 'token_tree_link',
            '#token_types' => [
              'file',
            ],
            '#weight' => 10,
          ];
          if (!empty($token_type)) {
            $form['settings']['filefield_paths']['details']['token_tree']['#token_types'][] = $token_type;
          }
        }

        // Redirect.
        $form['settings']['filefield_paths']['details']['redirect'] = [
          '#type' => 'checkbox',
          '#title' => t('Create Redirect'),
          '#description' => t('Create a redirect to the new location when a previously uploaded file is moved.'),
          '#default_value' => isset($settings['redirect']) ? $settings['redirect'] : FALSE,
          '#weight' => 11,
        ];
        if (!\Drupal::moduleHandler()
          ->moduleExists('redirect')) {
          $form['settings']['filefield_paths']['details']['redirect']['#disabled'] = TRUE;
          $form['settings']['filefield_paths']['details']['redirect']['#description'] .= '<br />' . t('Requires the <a href="https://drupal.org/project/redirect" target="_blank">Redirect</a> module.');
        }

        // Retroactive updates.
        $form['settings']['filefield_paths']['details']['retroactive_update'] = [
          '#type' => 'checkbox',
          '#title' => t('Retroactive update'),
          '#description' => t('Move and rename previously uploaded files.') . '<div>' . t('<strong class="warning">Warning:</strong> This feature should only be used on developmental servers or with extreme caution.') . '</div>',
          '#weight' => 12,
        ];

        // Active updating.
        $form['settings']['filefield_paths']['details']['active_updating'] = [
          '#type' => 'checkbox',
          '#title' => t('Active updating'),
          '#default_value' => isset($settings['active_updating']) ? $settings['active_updating'] : FALSE,
          '#description' => t('Actively move and rename previously uploaded files as required.') . '<div>' . t('<strong class="warning">Warning:</strong> This feature should only be used on developmental servers or with extreme caution.') . '</div>',
          '#weight' => 13,
        ];
      }
    }
    $form['actions']['submit']['#submit'][] = 'filefield_paths_form_submit';
  }
}

/**
 * Implements hook_form_alter().
 */
function filefield_paths_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {

  // Force all File (Field) Paths uploads to go to the temporary file system
  // prior to being processed.
  if (isset($element['#type']) && $element['#type'] == 'managed_file') {
    $settings = $context['items']
      ->getFieldDefinition()
      ->getThirdPartySettings('filefield_paths');
    if (isset($settings['enabled']) && $settings['enabled']) {
      $element['#upload_location'] = \Drupal::config('filefield_paths.settings')
        ->get('temp_location');
    }
  }
}

/**
 * Submit callback for File (Field) Paths settings form.
 *
 * @param array $form
 *   TODO.
 * @param FormStateInterface $form_state
 *   TODO.
 */
function filefield_paths_form_submit($form, FormStateInterface $form_state) {
  $settings = $form_state
    ->getValue('third_party_settings')['filefield_paths'];

  // Retroactive updates.
  if ($settings['enabled'] && $settings['retroactive_update']) {
    if (filefield_paths_batch_update($form_state
      ->getFormObject()
      ->getEntity())) {
      $response = batch_process($form_state
        ->getRedirect());
      if ($response instanceof Response) {
        $response
          ->send();
      }
    }
  }
}

/**
 * Set batch process to update File (Field) Paths.
 *
 * @param FieldConfig $field_config
 *   TODO.
 *
 * @return bool
 *   TODO.
 */
function filefield_paths_batch_update(FieldConfig $field_config) {
  $entity_info = \Drupal::entityTypeManager()
    ->getDefinition($field_config
    ->getTargetEntityTypeId());
  $query = \Drupal::entityQuery($field_config
    ->getTargetEntityTypeId());
  $result = $query
    ->condition($entity_info
    ->getKey('bundle'), $field_config
    ->getTargetBundle())
    ->condition("{$field_config->getName()}.target_id", '', '<>')
    ->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT')
    ->execute();

  // If there are no results, do not set a batch as there is nothing to process.
  if (empty($result)) {
    return FALSE;
  }

  // Create batch.
  $batch = [
    'title' => t('Updating File (Field) Paths'),
    'operations' => [
      [
        '_filefield_paths_batch_update_process',
        [
          $result,
          $field_config,
        ],
      ],
    ],
  ];
  batch_set($batch);
  return TRUE;
}

/**
 * Batch callback for File (Field) Paths retroactive updates.
 *
 * @param array $objects
 *   TODO.
 * @param FieldConfig $field_config
 *   TODO.
 * @param array $context
 *   TODO.
 */
function _filefield_paths_batch_update_process($objects, FieldConfig $field_config, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($objects);
    $context['sandbox']['objects'] = $objects;
  }

  /** @var Drupal\Core\Entity\ContentEntityStorageBase $entity_storage */
  $entity_storage = \Drupal::entityTypeManager()
    ->getStorage($field_config
    ->getTargetEntityTypeId());

  // Process nodes by groups of 5.
  $count = min(5, count($context['sandbox']['objects']));
  for ($i = 1; $i <= $count; $i++) {

    // For each oid, load the object, update the files and save it.
    $oid = array_shift($context['sandbox']['objects']);
    $entity = $entity_storage
      ->load($oid);

    // Enable active updating if it isn't already enabled.
    $active_updating = $field_config
      ->getThirdPartySetting('filefield_paths', 'active_updating');
    if (!$active_updating) {
      $field_config
        ->setThirdPartySetting('filefield_paths', 'active_updating', TRUE);
      $field_config
        ->save();
    }
    $entity->original = $entity;
    filefield_paths_entity_update($entity);

    // Restore active updating to it's previous state if necessary.
    if (!$active_updating) {
      $field_config
        ->setThirdPartySetting('filefield_paths', 'active_updating', $active_updating);
      $field_config
        ->save();
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
  }

  // Inform the batch engine that we are not finished,
  // and provide an estimation of the completion level we reached.
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Implements hook_entity_insert().
 */
function filefield_paths_entity_insert(EntityInterface $entity) {
  filefield_paths_entity_update($entity);
}

/**
 * Implements hook_entity_update().
 */
function filefield_paths_entity_update(EntityInterface $entity) {
  if ($entity instanceof ContentEntityInterface) {
    foreach ($entity
      ->getFields() as $field) {
      if ($field instanceof FileFieldItemList) {

        /** @var FieldConfig $definition */
        $definition = $field
          ->getFieldDefinition();

        // Ignore base fields.
        if ($definition instanceof ThirdPartySettingsInterface) {
          $settings = $definition
            ->getThirdPartySettings('filefield_paths');
          if (isset($settings['enabled']) && $settings['enabled']) {

            // Invoke hook_filefield_paths_process_file().
            foreach (\Drupal::moduleHandler()
              ->getImplementations('filefield_paths_process_file') as $module) {
              if (function_exists($function = "{$module}_filefield_paths_process_file")) {
                $function($entity, $field, $settings);
              }
            }
          }
        }
      }
    }
  }
}

/**
 * Implements hook_file_presave().
 */
function filefield_paths_file_presave($file) {

  // Store original filename in the database.
  if ($file->origname
    ->isEmpty() && !$file->filename
    ->isEmpty()) {
    $file->origname = $file->filename;
  }
}

/**
 * Creates a redirect for a moved File field.
 *
 * @param string $source
 *   The source file URL.
 * @param string $path
 *   The moved file URL.
 * @param \Drupal\Core\Language\Language $language
 *   The language of the source file.
 *
 * @deprecated in filefield_paths:1.0.0 and will be removed before
 *   filefield_paths:2.0.0. Use
 *   \Drupal\filefield_paths\RedirectInterface::createRedirect() instead.
 */
function _filefield_paths_create_redirect($source, $path, Language $language) {
  @trigger_error('_filefield_paths_create_redirect() is deprecated in filefield_paths:1.0.0 and will be removed before filefield_paths:2.0.0. Use \\Drupal\\filefield_paths\\RedirectInterface::createRedirect() instead.', E_USER_DEPRECATED);
  \Drupal::service('filefield_paths.redirect')
    ->createRedirect($source, $path, $language);
}

/**
 * Process and cleanup strings.
 *
 * @param string $value
 *   Todo.
 * @param string $data
 *   Todo.
 * @param array $settings
 *   Todo.
 *
 * @return string
 *   Todo.
 */
function filefield_paths_process_string($value, $data, array $settings = []) {
  $transliterate = $settings['transliterate'];
  $pathauto = \Drupal::moduleHandler()
    ->moduleExists('pathauto') && isset($settings['pathauto']) && $settings['pathauto'] == TRUE;
  $remove_slashes = !empty($settings['slashes']);

  // If '/' is to be removed from tokens, token replacement need to happen after
  // splitting the paths to subdirs, otherwise tokens containing '/' will be
  // part of the final path.
  if (!$remove_slashes) {
    $value = \Drupal::service('token')
      ->replace($value, $data, [
      'clear' => TRUE,
    ]);
  }
  $paths = explode('/', $value);
  foreach ($paths as $i => &$path) {
    if ($remove_slashes) {
      $path = \Drupal::service('token')
        ->replace($path, $data, [
        'clear' => TRUE,
      ]);
    }
    if ($pathauto == TRUE) {
      if ('file_name' == $settings['context'] && count($paths) == $i + 1) {
        $pathinfo = pathinfo($path);
        $basename = \Drupal::service('file_system')
          ->basename($path);
        $extension = preg_match('/\\.[^.]+$/', $basename, $matches) ? $matches[0] : NULL;
        $pathinfo['filename'] = !is_null($extension) ? mb_substr($basename, 0, mb_strlen($basename) - mb_strlen($extension)) : $basename;
        if ($remove_slashes) {
          $path = '';
          if (!empty($pathinfo['dirname']) && $pathinfo['dirname'] !== '.') {
            $path .= $pathinfo['dirname'] . '/';
          }
          $path .= $pathinfo['filename'];
          $path = \Drupal::service('pathauto.alias_cleaner')
            ->cleanstring($path);
          if (!empty($pathinfo['extension'])) {
            $path .= '.' . \Drupal::service('pathauto.alias_cleaner')
              ->cleanstring($pathinfo['extension']);
          }
          $path = str_replace('/', '', $path);
        }
        else {
          $path = str_replace($pathinfo['filename'], \Drupal::service('pathauto.alias_cleaner')
            ->cleanstring($pathinfo['filename']), $path);
        }
      }
      else {
        $path = \Drupal::service('pathauto.alias_cleaner')
          ->cleanstring($path);
      }
    }
    elseif ($remove_slashes) {
      $path = str_replace('/', '', $path);
    }

    // Transliterate string.
    if ($transliterate == TRUE) {
      $path = \Drupal::service('transliteration')
        ->transliterate($path);
    }
  }
  $value = implode('/', $paths);

  // Ensure that there are no double-slash sequences due to empty token values.
  $value = preg_replace('/\\/+/', '/', $value);
  return $value;
}

Functions

Namesort descending Description
filefield_paths_batch_update Set batch process to update File (Field) Paths.
filefield_paths_entity_base_field_info Implements hook_entity_base_field_info().
filefield_paths_entity_insert Implements hook_entity_insert().
filefield_paths_entity_update Implements hook_entity_update().
filefield_paths_field_widget_form_alter Implements hook_form_alter().
filefield_paths_file_presave Implements hook_file_presave().
filefield_paths_form_field_config_edit_form_alter Implements hook_form_FORM_ID_alter().
filefield_paths_form_submit Submit callback for File (Field) Paths settings form.
filefield_paths_process_string Process and cleanup strings.
_filefield_paths_batch_update_process Batch callback for File (Field) Paths retroactive updates.
_filefield_paths_create_redirect Deprecated Creates a redirect for a moved File field.