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
}