Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / file / file.module
1 <?php
2
3 /**
4  * @file
5  * Defines a "managed_file" Form API field and a "file" field for Field module.
6  */
7
8 use Drupal\Core\Datetime\Entity\DateFormat;
9 use Drupal\Core\Field\FieldDefinitionInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Messenger\MessengerInterface;
12 use Drupal\Core\Render\BubbleableMetadata;
13 use Drupal\Core\Render\Element;
14 use Drupal\Core\Routing\RouteMatchInterface;
15 use Drupal\Core\Url;
16 use Drupal\file\Entity\File;
17 use Drupal\file\FileInterface;
18 use Drupal\Component\Utility\NestedArray;
19 use Drupal\Component\Utility\Unicode;
20 use Drupal\Core\Entity\EntityStorageInterface;
21 use Drupal\Core\Template\Attribute;
22
23 /**
24  * The regex pattern used when checking for insecure file types.
25  */
26 define('FILE_INSECURE_EXTENSION_REGEX', '/\.(php|pl|py|cgi|asp|js)(\.|$)/i');
27
28 // Load all Field module hooks for File.
29 require_once __DIR__ . '/file.field.inc';
30
31 /**
32  * Implements hook_help().
33  */
34 function file_help($route_name, RouteMatchInterface $route_match) {
35   switch ($route_name) {
36     case 'help.page.file':
37       $output = '';
38       $output .= '<h3>' . t('About') . '</h3>';
39       $output .= '<p>' . t('The File module allows you to create fields that contain files. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":file_documentation">online documentation for the File module</a>.', [':field' => \Drupal::url('help.page', ['name' => 'field']), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', ['name' => 'field_ui']) : '#', ':file_documentation' => 'https://www.drupal.org/documentation/modules/file']) . '</p>';
40       $output .= '<h3>' . t('Uses') . '</h3>';
41       $output .= '<dl>';
42       $output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
43       $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the file field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', ['name' => 'field_ui']) : '#']) . '</dd>';
44       $output .= '<dt>' . t('Allowing file extensions') . '</dt>';
45       $output .= '<dd>' . t('In the field settings, you can define the allowed file extensions (for example <em>pdf docx psd</em>) for the files that will be uploaded with the file field.') . '</dd>';
46       $output .= '<dt>' . t('Storing files') . '</dt>';
47       $output .= '<dd>' . t('Uploaded files can either be stored as <em>public</em> or <em>private</em>, depending on the <a href=":file-system">File system settings</a>. For more information, see the <a href=":system-help">System module help page</a>.', [':file-system' => \Drupal::url('system.file_system_settings'), ':system-help' => \Drupal::url('help.page', ['name' => 'system'])]) . '</dd>';
48       $output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
49       $output .= '<dd>' . t('The maximum file size that users can upload is limited by PHP settings of the server, but you can restrict by entering the desired value as the <em>Maximum upload size</em> setting. The maximum file size is automatically displayed to users in the help text of the file field.') . '</dd>';
50       $output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
51       $output .= '<dd>' . t('In the field settings, you can allow users to toggle whether individual files are displayed. In the display settings, you can then choose one of the following formats: <ul><li><em>Generic file</em> displays links to the files and adds icons that symbolize the file extensions. If <em>descriptions</em> are enabled and have been submitted, then the description is displayed instead of the file name.</li><li><em>URL to file</em> displays the full path to the file as plain text.</li><li><em>Table of files</em> lists links to the files and the file sizes in a table.</li><li><em>RSS enclosure</em> only displays the first file, and only in a RSS feed, formatted according to the RSS 2.0 syntax for enclosures.</li></ul> A file can still be linked to directly by its URI even if it is not displayed.') . '</dd>';
52       $output .= '</dl>';
53       return $output;
54   }
55 }
56
57 /**
58  * Implements hook_field_widget_info_alter().
59  */
60 function file_field_widget_info_alter(array &$info) {
61   // Allows using the 'uri' widget for the 'file_uri' field type, which uses it
62   // as the default widget.
63   // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem
64   $info['uri']['field_types'][] = 'file_uri';
65 }
66
67 /**
68  * Loads file entities from the database.
69  *
70  * @param array|null $fids
71  *   (optional) An array of entity IDs. If omitted or NULL, all entities are
72  *   loaded.
73  * @param bool $reset
74  *   (optional) Whether to reset the internal file_load_multiple() cache.
75  *   Defaults to FALSE.
76  *
77  * @return array
78  *   An array of file entities, indexed by fid.
79  *
80  * @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
81  *   Use \Drupal\file\Entity\File::loadMultiple().
82  *
83  * @see hook_ENTITY_TYPE_load()
84  * @see file_load()
85  * @see entity_load()
86  * @see \Drupal\Core\Entity\Query\EntityQueryInterface
87  */
88 function file_load_multiple(array $fids = NULL, $reset = FALSE) {
89   if ($reset) {
90     \Drupal::entityManager()->getStorage('file')->resetCache($fids);
91   }
92   return File::loadMultiple($fids);
93 }
94
95 /**
96  * Loads a single file entity from the database.
97  *
98  * @param int $fid
99  *   A file ID.
100  * @param bool $reset
101  *   (optional) Whether to reset the internal file_load_multiple() cache.
102  *   Defaults to FALSE.
103  *
104  * @return \Drupal\file\FileInterface|null
105  *   A file entity or NULL if the file was not found.
106  *
107  * @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
108  *   Use \Drupal\file\Entity\File::load().
109  *
110  * @see hook_ENTITY_TYPE_load()
111  * @see file_load_multiple()
112  */
113 function file_load($fid, $reset = FALSE) {
114   if ($reset) {
115     \Drupal::entityManager()->getStorage('file')->resetCache([$fid]);
116   }
117   return File::load($fid);
118 }
119
120 /**
121  * Copies a file to a new location and adds a file record to the database.
122  *
123  * This function should be used when manipulating files that have records
124  * stored in the database. This is a powerful function that in many ways
125  * performs like an advanced version of copy().
126  * - Checks if $source and $destination are valid and readable/writable.
127  * - If file already exists in $destination either the call will error out,
128  *   replace the file or rename the file based on the $replace parameter.
129  * - If the $source and $destination are equal, the behavior depends on the
130  *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
131  *   will rename the file until the $destination is unique.
132  * - Adds the new file to the files database. If the source file is a
133  *   temporary file, the resulting file will also be a temporary file. See
134  *   file_save_upload() for details on temporary files.
135  *
136  * @param \Drupal\file\FileInterface $source
137  *   A file entity.
138  * @param string $destination
139  *   A string containing the destination that $source should be
140  *   copied to. This must be a stream wrapper URI.
141  * @param int $replace
142  *   (optional) Replace behavior when the destination file already exists.
143  *   Possible values include:
144  *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
145  *     the destination name exists, then its database entry will be updated. If
146  *     no database entry is found, then a new one will be created.
147  *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
148  *     filename is unique.
149  *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
150  *
151  * @return \Drupal\file\FileInterface|false
152  *   File entity if the copy is successful, or FALSE in the event of an error.
153  *
154  * @see file_unmanaged_copy()
155  * @see hook_file_copy()
156  */
157 function file_copy(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
158   if (!file_valid_uri($destination)) {
159     if (($realpath = \Drupal::service('file_system')->realpath($source->getFileUri())) !== FALSE) {
160       \Drupal::logger('file')->notice('File %file (%realpath) could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%realpath' => $realpath, '%destination' => $destination]);
161     }
162     else {
163       \Drupal::logger('file')->notice('File %file could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%destination' => $destination]);
164     }
165     \Drupal::messenger()->addError(t('The specified file %file could not be copied because the destination is invalid. More information is available in the system log.', ['%file' => $source->getFileUri()]));
166     return FALSE;
167   }
168
169   if ($uri = file_unmanaged_copy($source->getFileUri(), $destination, $replace)) {
170     $file = $source->createDuplicate();
171     $file->setFileUri($uri);
172     $file->setFilename(drupal_basename($uri));
173     // If we are replacing an existing file re-use its database record.
174     // @todo Do not create a new entity in order to update it. See
175     //   https://www.drupal.org/node/2241865.
176     if ($replace == FILE_EXISTS_REPLACE) {
177       $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
178       if (count($existing_files)) {
179         $existing = reset($existing_files);
180         $file->fid = $existing->id();
181         $file->setOriginalId($existing->id());
182         $file->setFilename($existing->getFilename());
183       }
184     }
185     // If we are renaming around an existing file (rather than a directory),
186     // use its basename for the filename.
187     elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
188       $file->setFilename(drupal_basename($destination));
189     }
190
191     $file->save();
192
193     // Inform modules that the file has been copied.
194     \Drupal::moduleHandler()->invokeAll('file_copy', [$file, $source]);
195
196     return $file;
197   }
198   return FALSE;
199 }
200
201 /**
202  * Moves a file to a new location and update the file's database entry.
203  *
204  * - Checks if $source and $destination are valid and readable/writable.
205  * - Performs a file move if $source is not equal to $destination.
206  * - If file already exists in $destination either the call will error out,
207  *   replace the file or rename the file based on the $replace parameter.
208  * - Adds the new file to the files database.
209  *
210  * @param \Drupal\file\FileInterface $source
211  *   A file entity.
212  * @param string $destination
213  *   A string containing the destination that $source should be moved
214  *   to. This must be a stream wrapper URI.
215  * @param int $replace
216  *   (optional) The replace behavior when the destination file already exists.
217  *   Possible values include:
218  *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
219  *     the destination name exists then its database entry will be updated and
220  *     $source->delete() called after invoking hook_file_move(). If no database
221  *     entry is found, then the source files record will be updated.
222  *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
223  *     filename is unique.
224  *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
225  *
226  * @return \Drupal\file\FileInterface|false
227  *   Resulting file entity for success, or FALSE in the event of an error.
228  *
229  * @see file_unmanaged_move()
230  * @see hook_file_move()
231  */
232 function file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
233   if (!file_valid_uri($destination)) {
234     if (($realpath = \Drupal::service('file_system')->realpath($source->getFileUri())) !== FALSE) {
235       \Drupal::logger('file')->notice('File %file (%realpath) could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%realpath' => $realpath, '%destination' => $destination]);
236     }
237     else {
238       \Drupal::logger('file')->notice('File %file could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%destination' => $destination]);
239     }
240     \Drupal::messenger()->addError(t('The specified file %file could not be moved because the destination is invalid. More information is available in the system log.', ['%file' => $source->getFileUri()]));
241     return FALSE;
242   }
243
244   if ($uri = file_unmanaged_move($source->getFileUri(), $destination, $replace)) {
245     $delete_source = FALSE;
246
247     $file = clone $source;
248     $file->setFileUri($uri);
249     // If we are replacing an existing file re-use its database record.
250     if ($replace == FILE_EXISTS_REPLACE) {
251       $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
252       if (count($existing_files)) {
253         $existing = reset($existing_files);
254         $delete_source = TRUE;
255         $file->fid = $existing->id();
256         $file->uuid = $existing->uuid();
257       }
258     }
259     // If we are renaming around an existing file (rather than a directory),
260     // use its basename for the filename.
261     elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
262       $file->setFilename(drupal_basename($destination));
263     }
264
265     $file->save();
266
267     // Inform modules that the file has been moved.
268     \Drupal::moduleHandler()->invokeAll('file_move', [$file, $source]);
269
270     // Delete the original if it's not in use elsewhere.
271     if ($delete_source && !\Drupal::service('file.usage')->listUsage($source)) {
272       $source->delete();
273     }
274
275     return $file;
276   }
277   return FALSE;
278 }
279
280 /**
281  * Checks that a file meets the criteria specified by the validators.
282  *
283  * After executing the validator callbacks specified hook_file_validate() will
284  * also be called to allow other modules to report errors about the file.
285  *
286  * @param \Drupal\file\FileInterface $file
287  *   A file entity.
288  * @param array $validators
289  *   (optional) An associative array of callback functions used to validate
290  *   the file. The keys are function names and the values arrays of callback
291  *   parameters which will be passed in after the file entity. The functions
292  *   should return an array of error messages; an empty array indicates that
293  *   the file passed validation. The callback functions will be called in the
294  *   order specified in the array, then the hook hook_file_validate()
295  *   will be invoked so other modules can validate the new file.
296  *
297  * @return array
298  *   An array containing validation error messages.
299  *
300  * @see hook_file_validate()
301  */
302 function file_validate(FileInterface $file, $validators = []) {
303   // Call the validation functions specified by this function's caller.
304   $errors = [];
305   foreach ($validators as $function => $args) {
306     if (function_exists($function)) {
307       array_unshift($args, $file);
308       $errors = array_merge($errors, call_user_func_array($function, $args));
309     }
310   }
311
312   // Let other modules perform validation on the new file.
313   return array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
314 }
315
316 /**
317  * Checks for files with names longer than can be stored in the database.
318  *
319  * @param \Drupal\file\FileInterface $file
320  *   A file entity.
321  *
322  * @return array
323  *   An empty array if the file name length is smaller than the limit or an
324  *   array containing an error message if it's not or is empty.
325  */
326 function file_validate_name_length(FileInterface $file) {
327   $errors = [];
328
329   if (!$file->getFilename()) {
330     $errors[] = t("The file's name is empty. Please give a name to the file.");
331   }
332   if (strlen($file->getFilename()) > 240) {
333     $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
334   }
335   return $errors;
336 }
337
338 /**
339  * Checks that the filename ends with an allowed extension.
340  *
341  * @param \Drupal\file\FileInterface $file
342  *   A file entity.
343  * @param string $extensions
344  *   A string with a space separated list of allowed extensions.
345  *
346  * @return array
347  *   An empty array if the file extension is allowed or an array containing an
348  *   error message if it's not.
349  *
350  * @see hook_file_validate()
351  */
352 function file_validate_extensions(FileInterface $file, $extensions) {
353   $errors = [];
354
355   $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
356   if (!preg_match($regex, $file->getFilename())) {
357     $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', ['%files-allowed' => $extensions]);
358   }
359   return $errors;
360 }
361
362 /**
363  * Checks that the file's size is below certain limits.
364  *
365  * @param \Drupal\file\FileInterface $file
366  *   A file entity.
367  * @param int $file_limit
368  *   (optional) The maximum file size in bytes. Zero (the default) indicates
369  *   that no limit should be enforced.
370  * @param int $user_limit
371  *   (optional) The maximum number of bytes the user is allowed. Zero (the
372  *   default) indicates that no limit should be enforced.
373  *
374  * @return array
375  *   An empty array if the file size is below limits or an array containing an
376  *   error message if it's not.
377  *
378  * @see hook_file_validate()
379  */
380 function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit = 0) {
381   $user = \Drupal::currentUser();
382   $errors = [];
383
384   if ($file_limit && $file->getSize() > $file_limit) {
385     $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', ['%filesize' => format_size($file->getSize()), '%maxsize' => format_size($file_limit)]);
386   }
387
388   // Save a query by only calling spaceUsed() when a limit is provided.
389   if ($user_limit && (\Drupal::entityManager()->getStorage('file')->spaceUsed($user->id()) + $file->getSize()) > $user_limit) {
390     $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', ['%filesize' => format_size($file->getSize()), '%quota' => format_size($user_limit)]);
391   }
392
393   return $errors;
394 }
395
396 /**
397  * Checks that the file is recognized as a valid image.
398  *
399  * @param \Drupal\file\FileInterface $file
400  *   A file entity.
401  *
402  * @return array
403  *   An empty array if the file is a valid image or an array containing an error
404  *   message if it's not.
405  *
406  * @see hook_file_validate()
407  */
408 function file_validate_is_image(FileInterface $file) {
409   $errors = [];
410
411   $image_factory = \Drupal::service('image.factory');
412   $image = $image_factory->get($file->getFileUri());
413   if (!$image->isValid()) {
414     $supported_extensions = $image_factory->getSupportedExtensions();
415     $errors[] = t('The image file is invalid or the image type is not allowed. Allowed types: %types', ['%types' => implode(', ', $supported_extensions)]);
416   }
417
418   return $errors;
419 }
420
421 /**
422  * Verifies that image dimensions are within the specified maximum and minimum.
423  *
424  * Non-image files will be ignored. If an image toolkit is available the image
425  * will be scaled to fit within the desired maximum dimensions.
426  *
427  * @param \Drupal\file\FileInterface $file
428  *   A file entity. This function may resize the file affecting its size.
429  * @param string|int $maximum_dimensions
430  *   (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
431  *   '85x85'. If an image toolkit is installed, the image will be resized down
432  *   to these dimensions. A value of zero (the default) indicates no restriction
433  *   on size, so no resizing will be attempted.
434  * @param string|int $minimum_dimensions
435  *   (optional) A string in the form WIDTHxHEIGHT. This will check that the
436  *   image meets a minimum size. A value of zero (the default) indicates that
437  *   there is no restriction on size.
438  *
439  * @return array
440  *   An empty array if the file meets the specified dimensions, was resized
441  *   successfully to meet those requirements or is not an image. If the image
442  *   does not meet the requirements or an attempt to resize it fails, an array
443  *   containing the error message will be returned.
444  *
445  * @see hook_file_validate()
446  */
447 function file_validate_image_resolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
448   $errors = [];
449
450   // Check first that the file is an image.
451   $image_factory = \Drupal::service('image.factory');
452   $image = $image_factory->get($file->getFileUri());
453
454   if ($image->isValid()) {
455     $scaling = FALSE;
456     if ($maximum_dimensions) {
457       // Check that it is smaller than the given dimensions.
458       list($width, $height) = explode('x', $maximum_dimensions);
459       if ($image->getWidth() > $width || $image->getHeight() > $height) {
460         // Try to resize the image to fit the dimensions.
461         if ($image->scale($width, $height)) {
462           $scaling = TRUE;
463           $image->save();
464           if (!empty($width) && !empty($height)) {
465             $message = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
466               [
467                 '%dimensions' => $maximum_dimensions,
468                 '%new_width' => $image->getWidth(),
469                 '%new_height' => $image->getHeight(),
470               ]);
471           }
472           elseif (empty($width)) {
473             $message = t('The image was resized to fit within the maximum allowed height of %height pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
474               [
475                 '%height' => $height,
476                 '%new_width' => $image->getWidth(),
477                 '%new_height' => $image->getHeight(),
478               ]);
479           }
480           elseif (empty($height)) {
481             $message = t('The image was resized to fit within the maximum allowed width of %width pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
482               [
483                 '%width' => $width,
484                 '%new_width' => $image->getWidth(),
485                 '%new_height' => $image->getHeight(),
486               ]);
487           }
488           \Drupal::messenger()->addStatus($message);
489         }
490         else {
491           $errors[] = t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
492         }
493       }
494     }
495
496     if ($minimum_dimensions) {
497       // Check that it is larger than the given dimensions.
498       list($width, $height) = explode('x', $minimum_dimensions);
499       if ($image->getWidth() < $width || $image->getHeight() < $height) {
500         if ($scaling) {
501           $errors[] = t('The resized image is too small. The minimum dimensions are %dimensions pixels and after resizing, the image size will be %widthx%height pixels.',
502             [
503               '%dimensions' => $minimum_dimensions,
504               '%width' => $image->getWidth(),
505               '%height' => $image->getHeight(),
506             ]);
507         }
508         else {
509           $errors[] = t('The image is too small. The minimum dimensions are %dimensions pixels and the image size is %widthx%height pixels.',
510             [
511               '%dimensions' => $minimum_dimensions,
512               '%width' => $image->getWidth(),
513               '%height' => $image->getHeight(),
514             ]);
515         }
516       }
517     }
518   }
519
520   return $errors;
521 }
522
523 /**
524  * Saves a file to the specified destination and creates a database entry.
525  *
526  * @param string $data
527  *   A string containing the contents of the file.
528  * @param string|null $destination
529  *   (optional) A string containing the destination URI. This must be a stream
530  *   wrapper URI. If no value or NULL is provided, a randomized name will be
531  *   generated and the file will be saved using Drupal's default files scheme,
532  *   usually "public://".
533  * @param int $replace
534  *   (optional) The replace behavior when the destination file already exists.
535  *   Possible values include:
536  *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
537  *     the destination name exists, then its database entry will be updated. If
538  *     no database entry is found, then a new one will be created.
539  *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
540  *     filename is unique.
541  *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
542  *
543  * @return \Drupal\file\FileInterface|false
544  *   A file entity, or FALSE on error.
545  *
546  * @see file_unmanaged_save_data()
547  */
548 function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
549   $user = \Drupal::currentUser();
550
551   if (empty($destination)) {
552     $destination = file_default_scheme() . '://';
553   }
554   if (!file_valid_uri($destination)) {
555     \Drupal::logger('file')->notice('The data could not be saved because the destination %destination is invalid. This may be caused by improper use of file_save_data() or a missing stream wrapper.', ['%destination' => $destination]);
556     \Drupal::messenger()->addError(t('The data could not be saved because the destination is invalid. More information is available in the system log.'));
557     return FALSE;
558   }
559
560   if ($uri = file_unmanaged_save_data($data, $destination, $replace)) {
561     // Create a file entity.
562     $file = File::create([
563       'uri' => $uri,
564       'uid' => $user->id(),
565       'status' => FILE_STATUS_PERMANENT,
566     ]);
567     // If we are replacing an existing file re-use its database record.
568     // @todo Do not create a new entity in order to update it. See
569     //   https://www.drupal.org/node/2241865.
570     if ($replace == FILE_EXISTS_REPLACE) {
571       $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
572       if (count($existing_files)) {
573         $existing = reset($existing_files);
574         $file->fid = $existing->id();
575         $file->setOriginalId($existing->id());
576         $file->setFilename($existing->getFilename());
577       }
578     }
579     // If we are renaming around an existing file (rather than a directory),
580     // use its basename for the filename.
581     elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
582       $file->setFilename(drupal_basename($destination));
583     }
584
585     $file->save();
586     return $file;
587   }
588   return FALSE;
589 }
590
591 /**
592  * Examines a file entity and returns appropriate content headers for download.
593  *
594  * @param \Drupal\file\FileInterface $file
595  *   A file entity.
596  *
597  * @return array
598  *   An associative array of headers, as expected by
599  *   \Symfony\Component\HttpFoundation\StreamedResponse.
600  */
601 function file_get_content_headers(FileInterface $file) {
602   $type = Unicode::mimeHeaderEncode($file->getMimeType());
603
604   return [
605     'Content-Type' => $type,
606     'Content-Length' => $file->getSize(),
607     'Cache-Control' => 'private',
608   ];
609 }
610
611 /**
612  * Implements hook_theme().
613  */
614 function file_theme() {
615   return [
616     // From file.module.
617     'file_link' => [
618       'variables' => ['file' => NULL, 'description' => NULL, 'attributes' => []],
619     ],
620     'file_managed_file' => [
621       'render element' => 'element',
622     ],
623     'file_audio' => [
624       'variables' => ['files' => [], 'attributes' => NULL],
625     ],
626     'file_video' => [
627       'variables' => ['files' => [], 'attributes' => NULL],
628     ],
629
630     // From file.field.inc.
631     'file_widget_multiple' => [
632       'render element' => 'element',
633       'file' => 'file.field.inc',
634     ],
635     'file_upload_help' => [
636       'variables' => ['description' => NULL, 'upload_validators' => NULL, 'cardinality' => NULL],
637       'file' => 'file.field.inc',
638     ],
639   ];
640 }
641
642 /**
643  * Implements hook_file_download().
644  */
645 function file_file_download($uri) {
646   // Get the file record based on the URI. If not in the database just return.
647   /** @var \Drupal\file\FileInterface[] $files */
648   $files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
649   if (count($files)) {
650     foreach ($files as $item) {
651       // Since some database servers sometimes use a case-insensitive comparison
652       // by default, double check that the filename is an exact match.
653       if ($item->getFileUri() === $uri) {
654         $file = $item;
655         break;
656       }
657     }
658   }
659   if (!isset($file)) {
660     return;
661   }
662
663   // Find out if a temporary file is still used in the system.
664   if ($file->isTemporary()) {
665     $usage = \Drupal::service('file.usage')->listUsage($file);
666     if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
667       // Deny access to temporary files without usage that are not owned by the
668       // same user. This prevents the security issue that a private file that
669       // was protected by field permissions becomes available after its usage
670       // was removed and before it is actually deleted from the file system.
671       // Modules that depend on this behavior should make the file permanent
672       // instead.
673       return -1;
674     }
675   }
676
677   // Find out which (if any) fields of this type contain the file.
678   $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
679
680   // Stop processing if there are no references in order to avoid returning
681   // headers for files controlled by other modules. Make an exception for
682   // temporary files where the host entity has not yet been saved (for example,
683   // an image preview on a node/add form) in which case, allow download by the
684   // file's owner.
685   if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
686     return;
687   }
688
689   if (!$file->access('download')) {
690     return -1;
691   }
692
693   // Access is granted.
694   $headers = file_get_content_headers($file);
695   return $headers;
696 }
697
698 /**
699  * Implements hook_cron().
700  */
701 function file_cron() {
702   $age = \Drupal::config('system.file')->get('temporary_maximum_age');
703   $file_storage = \Drupal::entityManager()->getStorage('file');
704
705   // Only delete temporary files if older than $age. Note that automatic cleanup
706   // is disabled if $age set to 0.
707   if ($age) {
708     $fids = Drupal::entityQuery('file')
709       ->condition('status', FILE_STATUS_PERMANENT, '<>')
710       ->condition('changed', REQUEST_TIME - $age, '<')
711       ->range(0, 100)
712       ->execute();
713     $files = $file_storage->loadMultiple($fids);
714     foreach ($files as $file) {
715       $references = \Drupal::service('file.usage')->listUsage($file);
716       if (empty($references)) {
717         if (file_exists($file->getFileUri())) {
718           $file->delete();
719         }
720         else {
721           \Drupal::logger('file system')->error('Could not delete temporary file "%path" during garbage collection', ['%path' => $file->getFileUri()]);
722         }
723       }
724       else {
725         \Drupal::logger('file system')->info('Did not delete temporary file "%path" during garbage collection because it is in use by the following modules: %modules.', ['%path' => $file->getFileUri(), '%modules' => implode(', ', array_keys($references))]);
726       }
727     }
728   }
729 }
730
731 /**
732  * Saves form file uploads.
733  *
734  * The files will be added to the {file_managed} table as temporary files.
735  * Temporary files are periodically cleaned. Use the 'file.usage' service to
736  * register the usage of the file which will automatically mark it as permanent.
737  *
738  * @param array $element
739  *   The FAPI element whose values are being saved.
740  * @param \Drupal\Core\Form\FormStateInterface $form_state
741  *   The current state of the form.
742  * @param null|int $delta
743  *   (optional) The delta of the file to return the file entity.
744  *   Defaults to NULL.
745  * @param int $replace
746  *   (optional) The replace behavior when the destination file already exists.
747  *   Possible values include:
748  *   - FILE_EXISTS_REPLACE: Replace the existing file.
749  *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
750  *     filename is unique.
751  *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
752  *
753  * @return array|\Drupal\file\FileInterface|null|false
754  *   An array of file entities or a single file entity if $delta != NULL. Each
755  *   array element contains the file entity if the upload succeeded or FALSE if
756  *   there was an error. Function returns NULL if no file was uploaded.
757  *
758  * @deprecated in Drupal 8.4.x, will be removed before Drupal 9.0.0.
759  *   For backwards compatibility use core file upload widgets in forms.
760  *
761  * @internal
762  *   This function wraps file_save_upload() to allow correct error handling in
763  *   forms.
764  *
765  * @todo Revisit after https://www.drupal.org/node/2244513.
766  */
767 function _file_save_upload_from_form(array $element, FormStateInterface $form_state, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
768   // Get all errors set before calling this method. This will also clear them
769   // from $_SESSION.
770   $errors_before = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
771
772   $upload_location = isset($element['#upload_location']) ? $element['#upload_location'] : FALSE;
773   $upload_name = implode('_', $element['#parents']);
774   $upload_validators = isset($element['#upload_validators']) ? $element['#upload_validators'] : [];
775
776   $result = file_save_upload($upload_name, $upload_validators, $upload_location, $delta, $replace);
777
778   // Get new errors that are generated while trying to save the upload. This
779   // will also clear them from $_SESSION.
780   $errors_new = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
781   if (!empty($errors_new)) {
782
783     if (count($errors_new) > 1) {
784       // Render multiple errors into a single message.
785       // This is needed because only one error per element is supported.
786       $render_array = [
787         'error' => [
788           '#markup' => t('One or more files could not be uploaded.'),
789         ],
790         'item_list' => [
791           '#theme' => 'item_list',
792           '#items' => $errors_new,
793         ],
794       ];
795       $error_message = \Drupal::service('renderer')->renderPlain($render_array);
796     }
797     else {
798       $error_message = reset($errors_new);
799     }
800
801     $form_state->setError($element, $error_message);
802   }
803
804   // Ensure that errors set prior to calling this method are still shown to the
805   // user.
806   if (!empty($errors_before)) {
807     foreach ($errors_before as $error) {
808       \Drupal::messenger()->addError($error);
809     }
810   }
811
812   return $result;
813 }
814
815 /**
816  * Saves file uploads to a new location.
817  *
818  * The files will be added to the {file_managed} table as temporary files.
819  * Temporary files are periodically cleaned. Use the 'file.usage' service to
820  * register the usage of the file which will automatically mark it as permanent.
821  *
822  * Note that this function does not support correct form error handling. The
823  * file upload widgets in core do support this. It is advised to use these in
824  * any custom form, instead of calling this function.
825  *
826  * @param string $form_field_name
827  *   A string that is the associative array key of the upload form element in
828  *   the form array.
829  * @param array $validators
830  *   (optional) An associative array of callback functions used to validate the
831  *   file. See file_validate() for a full discussion of the array format.
832  *   If the array is empty, it will be set up to call file_validate_extensions()
833  *   with a safe list of extensions, as follows: "jpg jpeg gif png txt doc
834  *   xls pdf ppt pps odt ods odp". To allow all extensions, you must explicitly
835  *   set this array to ['file_validate_extensions' => '']. (Beware: this is not
836  *   safe and should only be allowed for trusted users, if at all.)
837  * @param string|false $destination
838  *   (optional) A string containing the URI that the file should be copied to.
839  *   This must be a stream wrapper URI. If this value is omitted or set to
840  *   FALSE, Drupal's temporary files scheme will be used ("temporary://").
841  * @param null|int $delta
842  *   (optional) The delta of the file to return the file entity.
843  *   Defaults to NULL.
844  * @param int $replace
845  *   (optional) The replace behavior when the destination file already exists.
846  *   Possible values include:
847  *   - FILE_EXISTS_REPLACE: Replace the existing file.
848  *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
849  *     filename is unique.
850  *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
851  *
852  * @return array|\Drupal\file\FileInterface|null|false
853  *   An array of file entities or a single file entity if $delta != NULL. Each
854  *   array element contains the file entity if the upload succeeded or FALSE if
855  *   there was an error. Function returns NULL if no file was uploaded.
856  *
857  * @see _file_save_upload_from_form()
858  *
859  * @todo: move this logic to a service in https://www.drupal.org/node/2244513.
860  */
861 function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
862   $user = \Drupal::currentUser();
863   static $upload_cache;
864
865   $all_files = \Drupal::request()->files->get('files', []);
866   // Make sure there's an upload to process.
867   if (empty($all_files[$form_field_name])) {
868     return NULL;
869   }
870   $file_upload = $all_files[$form_field_name];
871
872   // Return cached objects without processing since the file will have
873   // already been processed and the paths in $_FILES will be invalid.
874   if (isset($upload_cache[$form_field_name])) {
875     if (isset($delta)) {
876       return $upload_cache[$form_field_name][$delta];
877     }
878     return $upload_cache[$form_field_name];
879   }
880
881   // Prepare uploaded files info. Representation is slightly different
882   // for multiple uploads and we fix that here.
883   $uploaded_files = $file_upload;
884   if (!is_array($file_upload)) {
885     $uploaded_files = [$file_upload];
886   }
887
888   $files = [];
889   foreach ($uploaded_files as $i => $file_info) {
890     // Check for file upload errors and return FALSE for this file if a lower
891     // level system error occurred. For a complete list of errors:
892     // See http://php.net/manual/features.file-upload.errors.php.
893     switch ($file_info->getError()) {
894       case UPLOAD_ERR_INI_SIZE:
895       case UPLOAD_ERR_FORM_SIZE:
896         \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
897         $files[$i] = FALSE;
898         continue;
899
900       case UPLOAD_ERR_PARTIAL:
901       case UPLOAD_ERR_NO_FILE:
902         \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
903         $files[$i] = FALSE;
904         continue;
905
906       case UPLOAD_ERR_OK:
907         // Final check that this is a valid upload, if it isn't, use the
908         // default error handler.
909         if (is_uploaded_file($file_info->getRealPath())) {
910           break;
911         }
912
913         // Unknown error
914       default:
915         \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
916         $files[$i] = FALSE;
917         continue;
918
919     }
920     // Begin building file entity.
921     $values = [
922       'uid' => $user->id(),
923       'status' => 0,
924       'filename' => $file_info->getClientOriginalName(),
925       'uri' => $file_info->getRealPath(),
926       'filesize' => $file_info->getSize(),
927     ];
928     $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
929     $file = File::create($values);
930
931     $extensions = '';
932     if (isset($validators['file_validate_extensions'])) {
933       if (isset($validators['file_validate_extensions'][0])) {
934         // Build the list of non-munged extensions if the caller provided them.
935         $extensions = $validators['file_validate_extensions'][0];
936       }
937       else {
938         // If 'file_validate_extensions' is set and the list is empty then the
939         // caller wants to allow any extension. In this case we have to remove the
940         // validator or else it will reject all extensions.
941         unset($validators['file_validate_extensions']);
942       }
943     }
944     else {
945       // No validator was provided, so add one using the default list.
946       // Build a default non-munged safe list for file_munge_filename().
947       $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
948       $validators['file_validate_extensions'] = [];
949       $validators['file_validate_extensions'][0] = $extensions;
950     }
951
952     if (!empty($extensions)) {
953       // Munge the filename to protect against possible malicious extension
954       // hiding within an unknown file type (ie: filename.html.foo).
955       $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
956     }
957
958     // Rename potentially executable files, to help prevent exploits (i.e. will
959     // rename filename.php.foo and filename.php to filename.php.foo.txt and
960     // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
961     // evaluates to TRUE.
962     if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
963       $file->setMimeType('text/plain');
964       // The destination filename will also later be used to create the URI.
965       $file->setFilename($file->getFilename() . '.txt');
966       // The .txt extension may not be in the allowed list of extensions. We have
967       // to add it here or else the file upload will fail.
968       if (!empty($extensions)) {
969         $validators['file_validate_extensions'][0] .= ' txt';
970         \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
971       }
972     }
973
974     // If the destination is not provided, use the temporary directory.
975     if (empty($destination)) {
976       $destination = 'temporary://';
977     }
978
979     // Assert that the destination contains a valid stream.
980     $destination_scheme = file_uri_scheme($destination);
981     if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
982       \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
983       $files[$i] = FALSE;
984       continue;
985     }
986
987     $file->source = $form_field_name;
988     // A file URI may already have a trailing slash or look like "public://".
989     if (substr($destination, -1) != '/') {
990       $destination .= '/';
991     }
992     $file->destination = file_destination($destination . $file->getFilename(), $replace);
993     // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
994     // there's an existing file so we need to bail.
995     if ($file->destination === FALSE) {
996       \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
997       $files[$i] = FALSE;
998       continue;
999     }
1000
1001     // Add in our check of the file name length.
1002     $validators['file_validate_name_length'] = [];
1003
1004     // Call the validation functions specified by this function's caller.
1005     $errors = file_validate($file, $validators);
1006
1007     // Check for errors.
1008     if (!empty($errors)) {
1009       $message = [
1010         'error' => [
1011           '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
1012         ],
1013         'item_list' => [
1014           '#theme' => 'item_list',
1015           '#items' => $errors,
1016         ],
1017       ];
1018       // @todo Add support for render arrays in
1019       // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
1020       // @see https://www.drupal.org/node/2505497.
1021       \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
1022       $files[$i] = FALSE;
1023       continue;
1024     }
1025
1026     $file->setFileUri($file->destination);
1027     if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
1028       \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
1029       \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
1030       $files[$i] = FALSE;
1031       continue;
1032     }
1033
1034     // Set the permissions on the new file.
1035     drupal_chmod($file->getFileUri());
1036
1037     // If we are replacing an existing file re-use its database record.
1038     // @todo Do not create a new entity in order to update it. See
1039     //   https://www.drupal.org/node/2241865.
1040     if ($replace == FILE_EXISTS_REPLACE) {
1041       $existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
1042       if (count($existing_files)) {
1043         $existing = reset($existing_files);
1044         $file->fid = $existing->id();
1045         $file->setOriginalId($existing->id());
1046       }
1047     }
1048
1049     // If we made it this far it's safe to record this file in the database.
1050     $file->save();
1051     $files[$i] = $file;
1052     // Allow an anonymous user who creates a non-public file to see it. See
1053     // \Drupal\file\FileAccessControlHandler::checkAccess().
1054     if ($user->isAnonymous() && $destination_scheme !== 'public') {
1055       $session = \Drupal::request()->getSession();
1056       $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
1057       $allowed_temp_files[$file->id()] = $file->id();
1058       $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
1059     }
1060   }
1061
1062   // Add files to the cache.
1063   $upload_cache[$form_field_name] = $files;
1064
1065   return isset($delta) ? $files[$delta] : $files;
1066 }
1067
1068 /**
1069  * Determines the preferred upload progress implementation.
1070  *
1071  * @return string|false
1072  *   A string indicating which upload progress system is available. Either "apc"
1073  *   or "uploadprogress". If neither are available, returns FALSE.
1074  */
1075 function file_progress_implementation() {
1076   static $implementation;
1077   if (!isset($implementation)) {
1078     $implementation = FALSE;
1079
1080     // We prefer the PECL extension uploadprogress because it supports multiple
1081     // simultaneous uploads. APCu only supports one at a time.
1082     if (extension_loaded('uploadprogress')) {
1083       $implementation = 'uploadprogress';
1084     }
1085     elseif (version_compare(PHP_VERSION, '7', '<') && extension_loaded('apc') && ini_get('apc.rfc1867')) {
1086       $implementation = 'apc';
1087     }
1088   }
1089   return $implementation;
1090 }
1091
1092 /**
1093  * Implements hook_ENTITY_TYPE_predelete() for file entities.
1094  */
1095 function file_file_predelete(File $file) {
1096   // @todo Remove references to a file that is in-use.
1097 }
1098
1099 /**
1100  * Implements hook_tokens().
1101  */
1102 function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
1103   $token_service = \Drupal::token();
1104
1105   $url_options = ['absolute' => TRUE];
1106   if (isset($options['langcode'])) {
1107     $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
1108     $langcode = $options['langcode'];
1109   }
1110   else {
1111     $langcode = NULL;
1112   }
1113
1114   $replacements = [];
1115
1116   if ($type == 'file' && !empty($data['file'])) {
1117     /** @var \Drupal\file\FileInterface $file */
1118     $file = $data['file'];
1119
1120     foreach ($tokens as $name => $original) {
1121       switch ($name) {
1122         // Basic keys and values.
1123         case 'fid':
1124           $replacements[$original] = $file->id();
1125           break;
1126
1127         // Essential file data
1128         case 'name':
1129           $replacements[$original] = $file->getFilename();
1130           break;
1131
1132         case 'path':
1133           $replacements[$original] = $file->getFileUri();
1134           break;
1135
1136         case 'mime':
1137           $replacements[$original] = $file->getMimeType();
1138           break;
1139
1140         case 'size':
1141           $replacements[$original] = format_size($file->getSize());
1142           break;
1143
1144         case 'url':
1145           // Ideally, this would use file_url_transform_relative(), but because
1146           // tokens are also often used in e-mails, it's better to keep absolute
1147           // file URLs. The 'url.site' cache context is associated to ensure the
1148           // correct absolute URL is used in case of a multisite setup.
1149           $replacements[$original] = file_create_url($file->getFileUri());
1150           $bubbleable_metadata->addCacheContexts(['url.site']);
1151           break;
1152
1153         // These tokens are default variations on the chained tokens handled below.
1154         case 'created':
1155           $date_format = DateFormat::load('medium');
1156           $bubbleable_metadata->addCacheableDependency($date_format);
1157           $replacements[$original] = format_date($file->getCreatedTime(), 'medium', '', NULL, $langcode);
1158           break;
1159
1160         case 'changed':
1161           $date_format = DateFormat::load('medium');
1162           $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
1163           $replacements[$original] = format_date($file->getChangedTime(), 'medium', '', NULL, $langcode);
1164           break;
1165
1166         case 'owner':
1167           $owner = $file->getOwner();
1168           $bubbleable_metadata->addCacheableDependency($owner);
1169           $name = $owner->label();
1170           $replacements[$original] = $name;
1171           break;
1172       }
1173     }
1174
1175     if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
1176       $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
1177     }
1178
1179     if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
1180       $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
1181     }
1182
1183     if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
1184       $replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
1185     }
1186   }
1187
1188   return $replacements;
1189 }
1190
1191 /**
1192  * Implements hook_token_info().
1193  */
1194 function file_token_info() {
1195   $types['file'] = [
1196     'name' => t("Files"),
1197     'description' => t("Tokens related to uploaded files."),
1198     'needs-data' => 'file',
1199   ];
1200
1201   // File related tokens.
1202   $file['fid'] = [
1203     'name' => t("File ID"),
1204     'description' => t("The unique ID of the uploaded file."),
1205   ];
1206   $file['name'] = [
1207     'name' => t("File name"),
1208     'description' => t("The name of the file on disk."),
1209   ];
1210   $file['path'] = [
1211     'name' => t("Path"),
1212     'description' => t("The location of the file relative to Drupal root."),
1213   ];
1214   $file['mime'] = [
1215     'name' => t("MIME type"),
1216     'description' => t("The MIME type of the file."),
1217   ];
1218   $file['size'] = [
1219     'name' => t("File size"),
1220     'description' => t("The size of the file."),
1221   ];
1222   $file['url'] = [
1223     'name' => t("URL"),
1224     'description' => t("The web-accessible URL for the file."),
1225   ];
1226   $file['created'] = [
1227     'name' => t("Created"),
1228     'description' => t("The date the file created."),
1229     'type' => 'date',
1230   ];
1231   $file['changed'] = [
1232     'name' => t("Changed"),
1233     'description' => t("The date the file was most recently changed."),
1234     'type' => 'date',
1235   ];
1236   $file['owner'] = [
1237     'name' => t("Owner"),
1238     'description' => t("The user who originally uploaded the file."),
1239     'type' => 'user',
1240   ];
1241
1242   return [
1243     'types' => $types,
1244     'tokens' => [
1245       'file' => $file,
1246     ],
1247   ];
1248 }
1249
1250 /**
1251  * Form submission handler for upload / remove buttons of managed_file elements.
1252  *
1253  * @see \Drupal\file\Element\ManagedFile::processManagedFile()
1254  */
1255 function file_managed_file_submit($form, FormStateInterface $form_state) {
1256   // Determine whether it was the upload or the remove button that was clicked,
1257   // and set $element to the managed_file element that contains that button.
1258   $parents = $form_state->getTriggeringElement()['#array_parents'];
1259   $button_key = array_pop($parents);
1260   $element = NestedArray::getValue($form, $parents);
1261
1262   // No action is needed here for the upload button, because all file uploads on
1263   // the form are processed by \Drupal\file\Element\ManagedFile::valueCallback()
1264   // regardless of which button was clicked. Action is needed here for the
1265   // remove button, because we only remove a file in response to its remove
1266   // button being clicked.
1267   if ($button_key == 'remove_button') {
1268     $fids = array_keys($element['#files']);
1269     // Get files that will be removed.
1270     if ($element['#multiple']) {
1271       $remove_fids = [];
1272       foreach (Element::children($element) as $name) {
1273         if (strpos($name, 'file_') === 0 && $element[$name]['selected']['#value']) {
1274           $remove_fids[] = (int) substr($name, 5);
1275         }
1276       }
1277       $fids = array_diff($fids, $remove_fids);
1278     }
1279     else {
1280       // If we deal with single upload element remove the file and set
1281       // element's value to empty array (file could not be removed from
1282       // element if we don't do that).
1283       $remove_fids = $fids;
1284       $fids = [];
1285     }
1286
1287     foreach ($remove_fids as $fid) {
1288       // If it's a temporary file we can safely remove it immediately, otherwise
1289       // it's up to the implementing module to remove usages of files to have them
1290       // removed.
1291       if ($element['#files'][$fid] && $element['#files'][$fid]->isTemporary()) {
1292         $element['#files'][$fid]->delete();
1293       }
1294     }
1295     // Update both $form_state->getValues() and FormState::$input to reflect
1296     // that the file has been removed, so that the form is rebuilt correctly.
1297     // $form_state->getValues() must be updated in case additional submit
1298     // handlers run, and for form building functions that run during the
1299     // rebuild, such as when the managed_file element is part of a field widget.
1300     // FormState::$input must be updated so that
1301     // \Drupal\file\Element\ManagedFile::valueCallback() has correct information
1302     // during the rebuild.
1303     $form_state->setValueForElement($element['fids'], implode(' ', $fids));
1304     NestedArray::setValue($form_state->getUserInput(), $element['fids']['#parents'], implode(' ', $fids));
1305   }
1306
1307   // Set the form to rebuild so that $form is correctly updated in response to
1308   // processing the file removal. Since this function did not change $form_state
1309   // if the upload button was clicked, a rebuild isn't necessary in that
1310   // situation and calling $form_state->disableRedirect() would suffice.
1311   // However, we choose to always rebuild, to keep the form processing workflow
1312   // consistent between the two buttons.
1313   $form_state->setRebuild();
1314 }
1315
1316 /**
1317  * Saves any files that have been uploaded into a managed_file element.
1318  *
1319  * @param array $element
1320  *   The FAPI element whose values are being saved.
1321  * @param \Drupal\Core\Form\FormStateInterface $form_state
1322  *   The current state of the form.
1323  *
1324  * @return array|false
1325  *   An array of file entities for each file that was saved, keyed by its file
1326  *   ID. Each array element contains a file entity. Function returns FALSE if
1327  *   upload directory could not be created or no files were uploaded.
1328  */
1329 function file_managed_file_save_upload($element, FormStateInterface $form_state) {
1330   $upload_name = implode('_', $element['#parents']);
1331   $all_files = \Drupal::request()->files->get('files', []);
1332   if (empty($all_files[$upload_name])) {
1333     return FALSE;
1334   }
1335   $file_upload = $all_files[$upload_name];
1336
1337   $destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
1338   if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
1339     \Drupal::logger('file')->notice('The upload directory %directory for the file field %name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', ['%directory' => $destination, '%name' => $element['#field_name']]);
1340     $form_state->setError($element, t('The file could not be uploaded.'));
1341     return FALSE;
1342   }
1343
1344   // Save attached files to the database.
1345   $files_uploaded = $element['#multiple'] && count(array_filter($file_upload)) > 0;
1346   $files_uploaded |= !$element['#multiple'] && !empty($file_upload);
1347   if ($files_uploaded) {
1348     if (!$files = _file_save_upload_from_form($element, $form_state)) {
1349       \Drupal::logger('file')->notice('The file upload failed. %upload', ['%upload' => $upload_name]);
1350       return [];
1351     }
1352
1353     // Value callback expects FIDs to be keys.
1354     $files = array_filter($files);
1355     $fids = array_map(function ($file) {
1356       return $file->id();
1357     }, $files);
1358
1359     return empty($files) ? [] : array_combine($fids, $files);
1360   }
1361
1362   return [];
1363 }
1364
1365 /**
1366  * Prepares variables for file form widget templates.
1367  *
1368  * Default template: file-managed-file.html.twig.
1369  *
1370  * @param array $variables
1371  *   An associative array containing:
1372  *   - element: A render element representing the file.
1373  */
1374 function template_preprocess_file_managed_file(&$variables) {
1375   $element = $variables['element'];
1376
1377   $variables['attributes'] = [];
1378   if (isset($element['#id'])) {
1379     $variables['attributes']['id'] = $element['#id'];
1380   }
1381   if (!empty($element['#attributes']['class'])) {
1382     $variables['attributes']['class'] = (array) $element['#attributes']['class'];
1383   }
1384 }
1385
1386 /**
1387  * Prepares variables for file link templates.
1388  *
1389  * Default template: file-link.html.twig.
1390  *
1391  * @param array $variables
1392  *   An associative array containing:
1393  *   - file: A file object to which the link will be created.
1394  *   - icon_directory: (optional) A path to a directory of icons to be used for
1395  *     files. Defaults to the value of the "icon.directory" variable.
1396  *   - description: A description to be displayed instead of the filename.
1397  *   - attributes: An associative array of attributes to be placed in the a tag.
1398  */
1399 function template_preprocess_file_link(&$variables) {
1400   $file = $variables['file'];
1401   $options = [];
1402
1403   $file_entity = ($file instanceof File) ? $file : File::load($file->fid);
1404   // @todo Wrap in file_url_transform_relative(). This is currently
1405   // impossible. As a work-around, we currently add the 'url.site' cache context
1406   // to ensure different file URLs are generated for different sites in a
1407   // multisite setup, including HTTP and HTTPS versions of the same site.
1408   // Fix in https://www.drupal.org/node/2646744.
1409   $url = file_create_url($file_entity->getFileUri());
1410   $variables['#cache']['contexts'][] = 'url.site';
1411
1412   $mime_type = $file->getMimeType();
1413   // Set options as per anchor format described at
1414   // http://microformats.org/wiki/file-format-examples
1415   $options['attributes']['type'] = $mime_type . '; length=' . $file->getSize();
1416
1417   // Use the description as the link text if available.
1418   if (empty($variables['description'])) {
1419     $link_text = $file_entity->getFilename();
1420   }
1421   else {
1422     $link_text = $variables['description'];
1423     $options['attributes']['title'] = $file_entity->getFilename();
1424   }
1425
1426   // Classes to add to the file field for icons.
1427   $classes = [
1428     'file',
1429     // Add a specific class for each and every mime type.
1430     'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']),
1431     // Add a more general class for groups of well known MIME types.
1432     'file--' . file_icon_class($mime_type),
1433   ];
1434
1435   // Set file classes to the options array.
1436   $variables['attributes'] = new Attribute($variables['attributes']);
1437   $variables['attributes']->addClass($classes);
1438
1439   $variables['link'] = \Drupal::l($link_text, Url::fromUri($url, $options));
1440 }
1441
1442 /**
1443  * Gets a class for the icon for a MIME type.
1444  *
1445  * @param string $mime_type
1446  *   A MIME type.
1447  *
1448  * @return string
1449  *   A class associated with the file.
1450  */
1451 function file_icon_class($mime_type) {
1452   // Search for a group with the files MIME type.
1453   $generic_mime = (string) file_icon_map($mime_type);
1454   if (!empty($generic_mime)) {
1455     return $generic_mime;
1456   }
1457
1458   // Use generic icons for each category that provides such icons.
1459   foreach (['audio', 'image', 'text', 'video'] as $category) {
1460     if (strpos($mime_type, $category) === 0) {
1461       return $category;
1462     }
1463   }
1464
1465   // If there's no generic icon for the type the general class.
1466   return 'general';
1467 }
1468
1469 /**
1470  * Determines the generic icon MIME package based on a file's MIME type.
1471  *
1472  * @param string $mime_type
1473  *   A MIME type.
1474  *
1475  * @return string|false
1476  *   The generic icon MIME package expected for this file.
1477  */
1478 function file_icon_map($mime_type) {
1479   switch ($mime_type) {
1480     // Word document types.
1481     case 'application/msword':
1482     case 'application/vnd.ms-word.document.macroEnabled.12':
1483     case 'application/vnd.oasis.opendocument.text':
1484     case 'application/vnd.oasis.opendocument.text-template':
1485     case 'application/vnd.oasis.opendocument.text-master':
1486     case 'application/vnd.oasis.opendocument.text-web':
1487     case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1488     case 'application/vnd.stardivision.writer':
1489     case 'application/vnd.sun.xml.writer':
1490     case 'application/vnd.sun.xml.writer.template':
1491     case 'application/vnd.sun.xml.writer.global':
1492     case 'application/vnd.wordperfect':
1493     case 'application/x-abiword':
1494     case 'application/x-applix-word':
1495     case 'application/x-kword':
1496     case 'application/x-kword-crypt':
1497       return 'x-office-document';
1498
1499     // Spreadsheet document types.
1500     case 'application/vnd.ms-excel':
1501     case 'application/vnd.ms-excel.sheet.macroEnabled.12':
1502     case 'application/vnd.oasis.opendocument.spreadsheet':
1503     case 'application/vnd.oasis.opendocument.spreadsheet-template':
1504     case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1505     case 'application/vnd.stardivision.calc':
1506     case 'application/vnd.sun.xml.calc':
1507     case 'application/vnd.sun.xml.calc.template':
1508     case 'application/vnd.lotus-1-2-3':
1509     case 'application/x-applix-spreadsheet':
1510     case 'application/x-gnumeric':
1511     case 'application/x-kspread':
1512     case 'application/x-kspread-crypt':
1513       return 'x-office-spreadsheet';
1514
1515     // Presentation document types.
1516     case 'application/vnd.ms-powerpoint':
1517     case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
1518     case 'application/vnd.oasis.opendocument.presentation':
1519     case 'application/vnd.oasis.opendocument.presentation-template':
1520     case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
1521     case 'application/vnd.stardivision.impress':
1522     case 'application/vnd.sun.xml.impress':
1523     case 'application/vnd.sun.xml.impress.template':
1524     case 'application/x-kpresenter':
1525       return 'x-office-presentation';
1526
1527     // Compressed archive types.
1528     case 'application/zip':
1529     case 'application/x-zip':
1530     case 'application/stuffit':
1531     case 'application/x-stuffit':
1532     case 'application/x-7z-compressed':
1533     case 'application/x-ace':
1534     case 'application/x-arj':
1535     case 'application/x-bzip':
1536     case 'application/x-bzip-compressed-tar':
1537     case 'application/x-compress':
1538     case 'application/x-compressed-tar':
1539     case 'application/x-cpio-compressed':
1540     case 'application/x-deb':
1541     case 'application/x-gzip':
1542     case 'application/x-java-archive':
1543     case 'application/x-lha':
1544     case 'application/x-lhz':
1545     case 'application/x-lzop':
1546     case 'application/x-rar':
1547     case 'application/x-rpm':
1548     case 'application/x-tzo':
1549     case 'application/x-tar':
1550     case 'application/x-tarz':
1551     case 'application/x-tgz':
1552       return 'package-x-generic';
1553
1554     // Script file types.
1555     case 'application/ecmascript':
1556     case 'application/javascript':
1557     case 'application/mathematica':
1558     case 'application/vnd.mozilla.xul+xml':
1559     case 'application/x-asp':
1560     case 'application/x-awk':
1561     case 'application/x-cgi':
1562     case 'application/x-csh':
1563     case 'application/x-m4':
1564     case 'application/x-perl':
1565     case 'application/x-php':
1566     case 'application/x-ruby':
1567     case 'application/x-shellscript':
1568     case 'text/vnd.wap.wmlscript':
1569     case 'text/x-emacs-lisp':
1570     case 'text/x-haskell':
1571     case 'text/x-literate-haskell':
1572     case 'text/x-lua':
1573     case 'text/x-makefile':
1574     case 'text/x-matlab':
1575     case 'text/x-python':
1576     case 'text/x-sql':
1577     case 'text/x-tcl':
1578       return 'text-x-script';
1579
1580     // HTML aliases.
1581     case 'application/xhtml+xml':
1582       return 'text-html';
1583
1584     // Executable types.
1585     case 'application/x-macbinary':
1586     case 'application/x-ms-dos-executable':
1587     case 'application/x-pef-executable':
1588       return 'application-x-executable';
1589
1590     // Acrobat types
1591     case 'application/pdf':
1592     case 'application/x-pdf':
1593     case 'applications/vnd.pdf':
1594     case 'text/pdf':
1595     case 'text/x-pdf':
1596       return 'application-pdf';
1597
1598     default:
1599       return FALSE;
1600   }
1601 }
1602
1603 /**
1604  * Retrieves a list of references to a file.
1605  *
1606  * @param \Drupal\file\FileInterface $file
1607  *   A file entity.
1608  * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field
1609  *   (optional) A field definition to be used for this check. If given,
1610  *   limits the reference check to the given field. Defaults to NULL.
1611  * @param int $age
1612  *   (optional) A constant that specifies which references to count. Use
1613  *   EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all
1614  *   references within all revisions or
1615  *   EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in
1616  *   the current revisions of all entities that have references to this file.
1617  * @param string $field_type
1618  *   (optional) The name of a field type. If given, limits the reference check
1619  *   to fields of the given type. If both $field and $field_type are given but
1620  *   $field is not the same type as $field_type, an empty array will be
1621  *   returned. Defaults to 'file'.
1622  *
1623  * @return array
1624  *   A multidimensional array. The keys are field_name, entity_type,
1625  *   entity_id and the value is an entity referencing this file.
1626  *
1627  * @ingroup file
1628  */
1629 function file_get_file_references(FileInterface $file, FieldDefinitionInterface $field = NULL, $age = EntityStorageInterface::FIELD_LOAD_REVISION, $field_type = 'file') {
1630   $references = &drupal_static(__FUNCTION__, []);
1631   $field_columns = &drupal_static(__FUNCTION__ . ':field_columns', []);
1632
1633   // Fill the static cache, disregard $field and $field_type for now.
1634   if (!isset($references[$file->id()][$age])) {
1635     $references[$file->id()][$age] = [];
1636     $usage_list = \Drupal::service('file.usage')->listUsage($file);
1637     $file_usage_list = isset($usage_list['file']) ? $usage_list['file'] : [];
1638     foreach ($file_usage_list as $entity_type_id => $entity_ids) {
1639       $entities = \Drupal::entityTypeManager()
1640         ->getStorage($entity_type_id)->loadMultiple(array_keys($entity_ids));
1641       foreach ($entities as $entity) {
1642         $bundle = $entity->bundle();
1643         // We need to find file fields for this entity type and bundle.
1644         if (!isset($file_fields[$entity_type_id][$bundle])) {
1645           $file_fields[$entity_type_id][$bundle] = [];
1646           // This contains the possible field names.
1647           foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
1648             // If this is the first time this field type is seen, check
1649             // whether it references files.
1650             if (!isset($field_columns[$field_definition->getType()])) {
1651               $field_columns[$field_definition->getType()] = file_field_find_file_reference_column($field_definition);
1652             }
1653             // If the field type does reference files then record it.
1654             if ($field_columns[$field_definition->getType()]) {
1655               $file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_definition->getType()];
1656             }
1657           }
1658         }
1659         foreach ($file_fields[$entity_type_id][$bundle] as $field_name => $field_column) {
1660           // Iterate over the field items to find the referenced file and field
1661           // name. This will fail if the usage checked is in a non-current
1662           // revision because field items are from the current
1663           // revision.
1664           // We also iterate over all translations because a file can be linked
1665           // to a language other than the default.
1666           foreach ($entity->getTranslationLanguages() as $langcode => $language) {
1667             foreach ($entity->getTranslation($langcode)->get($field_name) as $item) {
1668               if ($file->id() == $item->{$field_column}) {
1669                 $references[$file->id()][$age][$field_name][$entity_type_id][$entity->id()] = $entity;
1670                 break;
1671               }
1672             }
1673           }
1674         }
1675       }
1676     }
1677   }
1678   $return = $references[$file->id()][$age];
1679   // Filter the static cache down to the requested entries. The usual static
1680   // cache is very small so this will be very fast.
1681   if ($field || $field_type) {
1682     foreach ($return as $field_name => $data) {
1683       foreach (array_keys($data) as $entity_type_id) {
1684         $field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($entity_type_id);
1685         $current_field = $field_storage_definitions[$field_name];
1686         if (($field_type && $current_field->getType() != $field_type) || ($field && $field->uuid() != $current_field->uuid())) {
1687           unset($return[$field_name][$entity_type_id]);
1688         }
1689       }
1690     }
1691   }
1692   return $return;
1693 }
1694
1695 /**
1696  * Formats human-readable version of file status.
1697  *
1698  * @param int|null $choice
1699  *   (optional) An integer status code. If not set, all statuses are returned.
1700  *   Defaults to NULL.
1701  *
1702  * @return \Drupal\Core\StringTranslation\TranslatableMarkup|\Drupal\Core\StringTranslation\TranslatableMarkup[]
1703  *   An array of file statuses or a specified status if $choice is set.
1704  */
1705 function _views_file_status($choice = NULL) {
1706   $status = [
1707     0 => t('Temporary'),
1708     FILE_STATUS_PERMANENT => t('Permanent'),
1709   ];
1710
1711   if (isset($choice)) {
1712     return isset($status[$choice]) ? $status[$choice] : t('Unknown');
1713   }
1714
1715   return $status;
1716 }