FileItem.php 12.5 KB
Newer Older
1 2
<?php

3
namespace Drupal\file\Plugin\Field\FieldType;
4

5
use Drupal\Component\Utility\Bytes;
6
use Drupal\Component\Render\PlainTextOutput;
7
use Drupal\Component\Utility\Environment;
8 9
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
10
use Drupal\Core\Field\FieldStorageDefinitionInterface;
11
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
12
use Drupal\Core\File\FileSystemInterface;
13
use Drupal\Core\Form\FormStateInterface;
14
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
15
use Drupal\Core\TypedData\DataDefinition;
16 17 18 19 20 21 22 23

/**
 * Plugin implementation of the 'file' field type.
 *
 * @FieldType(
 *   id = "file",
 *   label = @Translation("File"),
 *   description = @Translation("This field stores the ID of a file as an integer value."),
24
 *   category = @Translation("Reference"),
25 26
 *   default_widget = "file_generic",
 *   default_formatter = "file_default",
27
 *   list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
28
 *   constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
29 30
 * )
 */
31
class FileItem extends EntityReferenceItem {
32

33 34 35
  /**
   * {@inheritdoc}
   */
36
  public static function defaultStorageSettings() {
37
    return [
38
      'target_type' => 'file',
39 40
      'display_field' => FALSE,
      'display_default' => FALSE,
41
      'uri_scheme' => \Drupal::config('system.file')->get('default_scheme'),
42
    ] + parent::defaultStorageSettings();
43 44 45 46 47
  }

  /**
   * {@inheritdoc}
   */
48
  public static function defaultFieldSettings() {
49
    return [
50
      'file_extensions' => 'txt',
51
      'file_directory' => '[date:custom:Y]-[date:custom:m]',
52 53
      'max_filesize' => '',
      'description_field' => 0,
54
    ] + parent::defaultFieldSettings();
55 56
  }

57 58 59
  /**
   * {@inheritdoc}
   */
60
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
61 62 63
    return [
      'columns' => [
        'target_id' => [
64 65 66
          'description' => 'The ID of the file entity.',
          'type' => 'int',
          'unsigned' => TRUE,
67 68
        ],
        'display' => [
69 70 71 72 73
          'description' => 'Flag to control whether this file should be displayed when viewing content.',
          'type' => 'int',
          'size' => 'tiny',
          'unsigned' => TRUE,
          'default' => 1,
74 75
        ],
        'description' => [
76 77
          'description' => 'A description of the file.',
          'type' => 'text',
78 79 80 81 82 83 84
        ],
      ],
      'indexes' => [
        'target_id' => ['target_id'],
      ],
      'foreign keys' => [
        'target_id' => [
85
          'table' => 'file_managed',
86 87 88 89
          'columns' => ['target_id' => 'fid'],
        ],
      ],
    ];
90 91 92 93 94
  }

  /**
   * {@inheritdoc}
   */
95
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
96
    $properties = parent::propertyDefinitions($field_definition);
97

98
    $properties['display'] = DataDefinition::create('boolean')
99 100
      ->setLabel(t('Display'))
      ->setDescription(t('Flag to control whether this file should be displayed when viewing content'));
101

102
    $properties['description'] = DataDefinition::create('string')
103
      ->setLabel(t('Description'));
104

105
    return $properties;
106 107 108 109 110
  }

  /**
   * {@inheritdoc}
   */
111
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
112
    $element = [];
113

114
    $element['#attached']['library'][] = 'file/drupal.file';
115

116
    $element['display_field'] = [
117 118
      '#type' => 'checkbox',
      '#title' => t('Enable <em>Display</em> field'),
119
      '#default_value' => $this->getSetting('display_field'),
120
      '#description' => t('The display option allows users to choose if a file should be shown when viewing the content.'),
121 122
    ];
    $element['display_default'] = [
123 124
      '#type' => 'checkbox',
      '#title' => t('Files displayed by default'),
125
      '#default_value' => $this->getSetting('display_default'),
126
      '#description' => t('This setting only has an effect if the display option is enabled.'),
127 128 129 130 131 132
      '#states' => [
        'visible' => [
          ':input[name="settings[display_field]"]' => ['checked' => TRUE],
        ],
      ],
    ];
133

134
    $scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
135
    $element['uri_scheme'] = [
136 137 138
      '#type' => 'radios',
      '#title' => t('Upload destination'),
      '#options' => $scheme_options,
139
      '#default_value' => $this->getSetting('uri_scheme'),
140 141
      '#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
      '#disabled' => $has_data,
142
    ];
143 144 145 146 147 148 149

    return $element;
  }

  /**
   * {@inheritdoc}
   */
150
  public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
151
    $element = [];
152
    $settings = $this->getSettings();
153

154
    $element['file_directory'] = [
155 156 157 158
      '#type' => 'textfield',
      '#title' => t('File directory'),
      '#default_value' => $settings['file_directory'],
      '#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'),
159
      '#element_validate' => [[get_class($this), 'validateDirectory']],
160
      '#weight' => 3,
161
    ];
162 163 164

    // Make the extension list a little more human-friendly by comma-separation.
    $extensions = str_replace(' ', ', ', $settings['file_extensions']);
165
    $element['file_extensions'] = [
166 167 168 169
      '#type' => 'textfield',
      '#title' => t('Allowed file extensions'),
      '#default_value' => $extensions,
      '#description' => t('Separate extensions with a space or comma and do not include the leading dot.'),
170
      '#element_validate' => [[get_class($this), 'validateExtensions']],
171
      '#weight' => 1,
172
      '#maxlength' => 256,
173 174 175
      // By making this field required, we prevent a potential security issue
      // that would allow files of any type to be uploaded.
      '#required' => TRUE,
176
    ];
177

178
    $element['max_filesize'] = [
179 180 181
      '#type' => 'textfield',
      '#title' => t('Maximum upload size'),
      '#default_value' => $settings['max_filesize'],
182
      '#description' => t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit <strong>%limit</strong>).', ['%limit' => format_size(Environment::getUploadMaxSize())]),
183
      '#size' => 10,
184
      '#element_validate' => [[get_class($this), 'validateMaxFilesize']],
185
      '#weight' => 5,
186
    ];
187

188
    $element['description_field'] = [
189 190 191 192 193
      '#type' => 'checkbox',
      '#title' => t('Enable <em>Description</em> field'),
      '#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
      '#description' => t('The description field allows users to enter a description about the uploaded file.'),
      '#weight' => 11,
194
    ];
195 196 197 198 199 200 201 202 203 204 205 206

    return $element;
  }

  /**
   * Form API callback
   *
   * Removes slashes from the beginning and end of the destination value and
   * ensures that the file directory path is not included at the beginning of the
   * value.
   *
   * This function is assigned as an #element_validate callback in
207
   * fieldSettingsForm().
208
   */
209
  public static function validateDirectory($element, FormStateInterface $form_state) {
210 211
    // Strip slashes from the beginning and end of $element['file_directory'].
    $value = trim($element['#value'], '\\/');
212
    $form_state->setValueForElement($element, $value);
213 214 215 216 217 218
  }

  /**
   * Form API callback.
   *
   * This function is assigned as an #element_validate callback in
219
   * fieldSettingsForm().
220 221 222 223 224
   *
   * This doubles as a convenience clean-up function and a validation routine.
   * Commas are allowed by the end-user, but ultimately the value will be stored
   * as a space-separated list for compatibility with file_validate_extensions().
   */
225
  public static function validateExtensions($element, FormStateInterface $form_state) {
226 227 228 229 230
    if (!empty($element['#value'])) {
      $extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
      $extensions = array_filter(explode(' ', $extensions));
      $extensions = implode(' ', array_unique($extensions));
      if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
231
        $form_state->setError($element, t('The list of allowed extensions is not valid, be sure to exclude leading dots and to separate extensions with a comma or space.'));
232 233
      }
      else {
234
        $form_state->setValueForElement($element, $extensions);
235 236 237 238 239 240 241 242
      }
    }
  }

  /**
   * Form API callback.
   *
   * Ensures that a size has been entered and that it can be parsed by
243
   * \Drupal\Component\Utility\Bytes::toInt().
244 245
   *
   * This function is assigned as an #element_validate callback in
246
   * fieldSettingsForm().
247
   */
248
  public static function validateMaxFilesize($element, FormStateInterface $form_state) {
249
    if (!empty($element['#value']) && !is_numeric(Bytes::toInt($element['#value']))) {
250
      $form_state->setError($element, t('The "@name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', ['@name' => $element['title']]));
251 252 253 254
    }
  }

  /**
255
   * Determines the URI for a file field.
256
   *
257
   * @param array $data
258
   *   An array of token objects to pass to Token::replace().
259
   *
260
   * @return string
261 262
   *   An unsanitized file directory URI with tokens replaced. The result of
   *   the token replacement is then converted to plain text and returned.
263
   *
264
   * @see \Drupal\Core\Utility\Token::replace()
265
   */
266
  public function getUploadLocation($data = []) {
267 268 269 270 271 272 273 274 275
    return static::doGetUploadLocation($this->getSettings(), $data);
  }

  /**
   * Determines the URI for a file field.
   *
   * @param array $settings
   *   The array of field settings.
   * @param array $data
276
   *   An array of token objects to pass to Token::replace().
277 278 279 280
   *
   * @return string
   *   An unsanitized file directory URI with tokens replaced. The result of
   *   the token replacement is then converted to plain text and returned.
281 282
   *
   * @see \Drupal\Core\Utility\Token::replace()
283 284
   */
  protected static function doGetUploadLocation(array $settings, $data = []) {
285 286
    $destination = trim($settings['file_directory'], '/');

287 288 289
    // Replace tokens. As the tokens might contain HTML we convert it to plain
    // text.
    $destination = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($destination, $data));
290 291 292 293 294 295
    return $settings['uri_scheme'] . '://' . $destination;
  }

  /**
   * Retrieves the upload validators for a file field.
   *
296
   * @return array
297 298 299 300
   *   An array suitable for passing to file_save_upload() or the file field
   *   element's '#upload_validators' property.
   */
  public function getUploadValidators() {
301
    $validators = [];
302
    $settings = $this->getSettings();
303 304

    // Cap the upload size according to the PHP limit.
305
    $max_filesize = Bytes::toInt(Environment::getUploadMaxSize());
306
    if (!empty($settings['max_filesize'])) {
307
      $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
308 309 310
    }

    // There is always a file size limit due to the PHP server limit.
311
    $validators['file_validate_size'] = [$max_filesize];
312 313 314

    // Add the extension check if necessary.
    if (!empty($settings['file_extensions'])) {
315
      $validators['file_validate_extensions'] = [$settings['file_extensions']];
316 317 318 319 320
    }

    return $validators;
  }

321 322 323 324 325 326 327
  /**
   * {@inheritdoc}
   */
  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
    $random = new Random();
    $settings = $field_definition->getSettings();

328
    // Prepare destination.
329
    $dirname = static::doGetUploadLocation($settings);
330
    \Drupal::service('file_system')->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY);
331

332
    // Generate a file entity.
333
    $destination = $dirname . '/' . $random->name(10, TRUE) . '.txt';
334
    $data = $random->paragraphs(3);
335
    $file = file_save_data($data, $destination, FileSystemInterface::EXISTS_ERROR);
336
    $values = [
337
      'target_id' => $file->id(),
338
      'display' => (int) $settings['display_default'],
339
      'description' => $random->sentences(10),
340
    ];
341 342 343
    return $values;
  }

344 345 346 347 348 349 350
  /**
   * Determines whether an item should be displayed when rendering the field.
   *
   * @return bool
   *   TRUE if the item should be displayed, FALSE if not.
   */
  public function isDisplayed() {
351
    if ($this->getSetting('display_field')) {
352 353 354 355 356
      return (bool) $this->display;
    }
    return TRUE;
  }

357 358 359 360 361 362 363
  /**
   * {@inheritdoc}
   */
  public static function getPreconfiguredOptions() {
    return [];
  }

364
}