file.module 62.4 KB
Newer Older
1 2 3 4 5 6 7
<?php

/**
 * @file
 * Defines a "managed_file" Form API field and a "file" field for Field module.
 */

8
use Drupal\Core\Datetime\Entity\DateFormat;
9
use Drupal\Core\Field\FieldDefinitionInterface;
10
use Drupal\Core\Form\FormStateInterface;
11
use Drupal\Core\Render\BubbleableMetadata;
12
use Drupal\Core\Render\Element;
13
use Drupal\Core\Routing\RouteMatchInterface;
14
use Drupal\Core\Url;
15
use Drupal\file\Entity\File;
16
use Drupal\file\FileInterface;
17
use Drupal\Component\Utility\NestedArray;
18
use Drupal\Component\Utility\Unicode;
19
use Drupal\Core\Entity\EntityStorageInterface;
20
use Drupal\Core\Template\Attribute;
21

22
// Load all Field module hooks for File.
23
require_once __DIR__ . '/file.field.inc';
24

25 26 27
/**
 * Implements hook_help().
 */
28
function file_help($route_name, RouteMatchInterface $route_match) {
29 30
  switch ($route_name) {
    case 'help.page.file':
31 32
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
33
      $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>';
34 35
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
36
      $output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
37
      $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>';
38 39
      $output .= '<dt>' . t('Allowing file extensions') . '</dt>';
      $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>';
40
      $output .= '<dt>' . t('Storing files') . '</dt>';
41
      $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>';
42 43 44 45
      $output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
      $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>';
      $output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
      $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>';
46 47 48 49 50
      $output .= '</dl>';
      return $output;
  }
}

51 52 53
/**
 * Loads file entities from the database.
 *
54 55 56
 * @param array|null $fids
 *   (optional) An array of entity IDs. If omitted or NULL, all entities are
 *   loaded.
57
 * @param bool $reset
58 59
 *   (optional) Whether to reset the internal file_load_multiple() cache.
 *   Defaults to FALSE.
60 61 62 63
 *
 * @return array
 *   An array of file entities, indexed by fid.
 *
64 65 66
 * @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
 *   Use \Drupal\file\Entity\File::loadMultiple().
 *
67
 * @see hook_ENTITY_TYPE_load()
68 69
 * @see file_load()
 * @see entity_load()
70
 * @see \Drupal\Core\Entity\Query\EntityQueryInterface
71
 */
72
function file_load_multiple(array $fids = NULL, $reset = FALSE) {
73 74 75 76
  if ($reset) {
    \Drupal::entityManager()->getStorage('file')->resetCache($fids);
  }
  return File::loadMultiple($fids);
77 78 79 80 81
}

/**
 * Loads a single file entity from the database.
 *
82
 * @param int $fid
83
 *   A file ID.
84
 * @param bool $reset
85 86
 *   (optional) Whether to reset the internal file_load_multiple() cache.
 *   Defaults to FALSE.
87
 *
88
 * @return \Drupal\file\FileInterface|null
89
 *   A file entity or NULL if the file was not found.
90
 *
91 92 93
 * @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
 *   Use \Drupal\file\Entity\File::load().
 *
94
 * @see hook_ENTITY_TYPE_load()
95 96
 * @see file_load_multiple()
 */
97
function file_load($fid, $reset = FALSE) {
98
  if ($reset) {
99
    \Drupal::entityManager()->getStorage('file')->resetCache([$fid]);
100 101
  }
  return File::load($fid);
102 103 104 105 106 107 108 109 110 111 112
}

/**
 * Copies a file to a new location and adds a file record to the database.
 *
 * This function should be used when manipulating files that have records
 * stored in the database. This is a powerful function that in many ways
 * performs like an advanced version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
113
 * - If the $source and $destination are equal, the behavior depends on the
114
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
115
 *   will rename the file until the $destination is unique.
116 117 118 119
 * - Adds the new file to the files database. If the source file is a
 *   temporary file, the resulting file will also be a temporary file. See
 *   file_save_upload() for details on temporary files.
 *
120
 * @param \Drupal\file\FileInterface $source
121
 *   A file entity.
122
 * @param string $destination
123 124
 *   A string containing the destination that $source should be
 *   copied to. This must be a stream wrapper URI.
125
 * @param int $replace
126 127 128 129 130 131 132 133
 *   (optional) Replace behavior when the destination file already exists.
 *   Possible values include:
 *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
 *     the destination name exists, then its database entry will be updated. If
 *     no database entry is found, then a new one will be created.
 *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
 *     filename is unique.
 *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
134
 *
135 136
 * @return \Drupal\file\FileInterface|false
 *   File entity if the copy is successful, or FALSE in the event of an error.
137 138 139 140
 *
 * @see file_unmanaged_copy()
 * @see hook_file_copy()
 */
141
function file_copy(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
142
  if (!file_valid_uri($destination)) {
143
    if (($realpath = drupal_realpath($source->getFileUri())) !== FALSE) {
144
      \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]);
145 146
    }
    else {
147
      \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]);
148
    }
149
    drupal_set_message(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()]), 'error');
150 151 152
    return FALSE;
  }

153 154 155 156
  if ($uri = file_unmanaged_copy($source->getFileUri(), $destination, $replace)) {
    $file = $source->createDuplicate();
    $file->setFileUri($uri);
    $file->setFilename(drupal_basename($uri));
157
    // If we are replacing an existing file re-use its database record.
158 159
    // @todo Do not create a new entity in order to update it. See
    //   https://www.drupal.org/node/2241865.
160
    if ($replace == FILE_EXISTS_REPLACE) {
161
      $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
162 163
      if (count($existing_files)) {
        $existing = reset($existing_files);
164
        $file->fid = $existing->id();
165
        $file->setOriginalId($existing->id());
166
        $file->setFilename($existing->getFilename());
167 168 169 170 171
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
172
      $file->setFilename(drupal_basename($destination));
173 174 175 176 177
    }

    $file->save();

    // Inform modules that the file has been copied.
178
    \Drupal::moduleHandler()->invokeAll('file_copy', [$file, $source]);
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193

    return $file;
  }
  return FALSE;
}

/**
 * Moves a file to a new location and update the file's database entry.
 *
 * - Checks if $source and $destination are valid and readable/writable.
 * - Performs a file move if $source is not equal to $destination.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database.
 *
194
 * @param \Drupal\file\FileInterface $source
195
 *   A file entity.
196
 * @param string $destination
197 198
 *   A string containing the destination that $source should be moved
 *   to. This must be a stream wrapper URI.
199
 * @param int $replace
200 201 202 203 204 205 206 207 208
 *   (optional) The replace behavior when the destination file already exists.
 *   Possible values include:
 *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
 *     the destination name exists then its database entry will be updated and
 *     $source->delete() called after invoking hook_file_move(). If no database
 *     entry is found, then the source files record will be updated.
 *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
 *     filename is unique.
 *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
209
 *
210
 * @return \Drupal\file\FileInterface|false
211 212 213 214 215
 *   Resulting file entity for success, or FALSE in the event of an error.
 *
 * @see file_unmanaged_move()
 * @see hook_file_move()
 */
216
function file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
217
  if (!file_valid_uri($destination)) {
218
    if (($realpath = drupal_realpath($source->getFileUri())) !== FALSE) {
219
      \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]);
220 221
    }
    else {
222
      \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]);
223
    }
224
    drupal_set_message(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()]), 'error');
225 226 227
    return FALSE;
  }

228
  if ($uri = file_unmanaged_move($source->getFileUri(), $destination, $replace)) {
229 230 231
    $delete_source = FALSE;

    $file = clone $source;
232
    $file->setFileUri($uri);
233 234
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
235
      $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
236 237 238
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $delete_source = TRUE;
239
        $file->fid = $existing->id();
240
        $file->uuid = $existing->uuid();
241 242 243 244 245
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
246
      $file->setFilename(drupal_basename($destination));
247 248 249 250 251
    }

    $file->save();

    // Inform modules that the file has been moved.
252
    \Drupal::moduleHandler()->invokeAll('file_move', [$file, $source]);
253 254

    // Delete the original if it's not in use elsewhere.
255
    if ($delete_source && !\Drupal::service('file.usage')->listUsage($source)) {
256 257 258 259 260 261 262 263 264 265 266 267 268 269
      $source->delete();
    }

    return $file;
  }
  return FALSE;
}

/**
 * Checks that a file meets the criteria specified by the validators.
 *
 * After executing the validator callbacks specified hook_file_validate() will
 * also be called to allow other modules to report errors about the file.
 *
270
 * @param \Drupal\file\FileInterface $file
271
 *   A file entity.
272
 * @param array $validators
273 274 275 276 277 278 279
 *   (optional) An associative array of callback functions used to validate
 *   the file. The keys are function names and the values arrays of callback
 *   parameters which will be passed in after the file entity. The functions
 *   should return an array of error messages; an empty array indicates that
 *   the file passed validation. The callback functions will be called in the
 *   order specified in the array, then the hook hook_file_validate()
 *   will be invoked so other modules can validate the new file.
280
 *
281
 * @return array
282 283 284 285
 *   An array containing validation error messages.
 *
 * @see hook_file_validate()
 */
286
function file_validate(FileInterface $file, $validators = []) {
287
  // Call the validation functions specified by this function's caller.
288
  $errors = [];
289 290 291 292 293 294 295 296
  foreach ($validators as $function => $args) {
    if (function_exists($function)) {
      array_unshift($args, $file);
      $errors = array_merge($errors, call_user_func_array($function, $args));
    }
  }

  // Let other modules perform validation on the new file.
297
  return array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
298 299 300 301 302
}

/**
 * Checks for files with names longer than can be stored in the database.
 *
303
 * @param \Drupal\file\FileInterface $file
304 305
 *   A file entity.
 *
306
 * @return array
307 308
 *   An empty array if the file name length is smaller than the limit or an
 *   array containing an error message if it's not or is empty.
309
 */
310
function file_validate_name_length(FileInterface $file) {
311
  $errors = [];
312

313
  if (!$file->getFilename()) {
314 315
    $errors[] = t("The file's name is empty. Please give a name to the file.");
  }
316
  if (strlen($file->getFilename()) > 240) {
317 318 319 320 321 322 323 324
    $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
  }
  return $errors;
}

/**
 * Checks that the filename ends with an allowed extension.
 *
325
 * @param \Drupal\file\FileInterface $file
326
 *   A file entity.
327
 * @param string $extensions
328 329
 *   A string with a space separated list of allowed extensions.
 *
330
 * @return array
331 332
 *   An empty array if the file extension is allowed or an array containing an
 *   error message if it's not.
333 334 335
 *
 * @see hook_file_validate()
 */
336
function file_validate_extensions(FileInterface $file, $extensions) {
337
  $errors = [];
338 339

  $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
340
  if (!preg_match($regex, $file->getFilename())) {
341
    $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', ['%files-allowed' => $extensions]);
342 343 344 345 346 347 348
  }
  return $errors;
}

/**
 * Checks that the file's size is below certain limits.
 *
349
 * @param \Drupal\file\FileInterface $file
350
 *   A file entity.
351
 * @param int $file_limit
352 353
 *   (optional) The maximum file size in bytes. Zero (the default) indicates
 *   that no limit should be enforced.
354
 * @param int $user_limit
355 356
 *   (optional) The maximum number of bytes the user is allowed. Zero (the
 *   default) indicates that no limit should be enforced.
357
 *
358
 * @return array
359 360
 *   An empty array if the file size is below limits or an array containing an
 *   error message if it's not.
361 362 363
 *
 * @see hook_file_validate()
 */
364
function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit = 0) {
365
  $user = \Drupal::currentUser();
366
  $errors = [];
367

368
  if ($file_limit && $file->getSize() > $file_limit) {
369
    $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', ['%filesize' => format_size($file->getSize()), '%maxsize' => format_size($file_limit)]);
370
  }
371

372
  // Save a query by only calling spaceUsed() when a limit is provided.
373
  if ($user_limit && (\Drupal::entityManager()->getStorage('file')->spaceUsed($user->id()) + $file->getSize()) > $user_limit) {
374
    $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', ['%filesize' => format_size($file->getSize()), '%quota' => format_size($user_limit)]);
375
  }
376

377 378 379 380
  return $errors;
}

/**
381
 * Checks that the file is recognized as a valid image.
382
 *
383
 * @param \Drupal\file\FileInterface $file
384 385
 *   A file entity.
 *
386
 * @return array
387 388
 *   An empty array if the file is a valid image or an array containing an error
 *   message if it's not.
389 390 391
 *
 * @see hook_file_validate()
 */
392
function file_validate_is_image(FileInterface $file) {
393
  $errors = [];
394

395 396 397 398
  $image_factory = \Drupal::service('image.factory');
  $image = $image_factory->get($file->getFileUri());
  if (!$image->isValid()) {
    $supported_extensions = $image_factory->getSupportedExtensions();
399
    $errors[] = t('Image type not supported. Allowed types: %types', ['%types' => implode(' ', $supported_extensions)]);
400 401 402 403 404 405 406 407
  }

  return $errors;
}

/**
 * Verifies that image dimensions are within the specified maximum and minimum.
 *
408
 * Non-image files will be ignored. If an image toolkit is available the image
409 410
 * will be scaled to fit within the desired maximum dimensions.
 *
411
 * @param \Drupal\file\FileInterface $file
412
 *   A file entity. This function may resize the file affecting its size.
413 414 415 416 417 418 419 420 421
 * @param string|int $maximum_dimensions
 *   (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
 *   '85x85'. If an image toolkit is installed, the image will be resized down
 *   to these dimensions. A value of zero (the default) indicates no restriction
 *   on size, so no resizing will be attempted.
 * @param string|int $minimum_dimensions
 *   (optional) A string in the form WIDTHxHEIGHT. This will check that the
 *   image meets a minimum size. A value of zero (the default) indicates that
 *   there is no restriction on size.
422
 *
423 424 425 426 427
 * @return array
 *   An empty array if the file meets the specified dimensions, was resized
 *   successfully to meet those requirements or is not an image. If the image
 *   does not meet the requirements or an attempt to resize it fails, an array
 *   containing the error message will be returned.
428 429 430
 *
 * @see hook_file_validate()
 */
431
function file_validate_image_resolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
432
  $errors = [];
433 434

  // Check first that the file is an image.
435
  $image_factory = \Drupal::service('image.factory');
436
  $image = $image_factory->get($file->getFileUri());
437
  if ($image->isValid()) {
438 439 440
    if ($maximum_dimensions) {
      // Check that it is smaller than the given dimensions.
      list($width, $height) = explode('x', $maximum_dimensions);
441
      if ($image->getWidth() > $width || $image->getHeight() > $height) {
442
        // Try to resize the image to fit the dimensions.
443
        if ($image->scale($width, $height)) {
444
          $image->save();
445
          if (!empty($width) && !empty($height)) {
446
            $message = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $maximum_dimensions]);
447 448
          }
          elseif (empty($width)) {
449
            $message = t('The image was resized to fit within the maximum allowed height of %height pixels.', ['%height' => $height]);
450 451
          }
          elseif (empty($height)) {
452
            $message = t('The image was resized to fit within the maximum allowed width of %width pixels.', ['%width' => $width]);
453 454
          }
          drupal_set_message($message);
455 456
        }
        else {
457
          $errors[] = t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
458 459 460 461 462 463 464
        }
      }
    }

    if ($minimum_dimensions) {
      // Check that it is larger than the given dimensions.
      list($width, $height) = explode('x', $minimum_dimensions);
465
      if ($image->getWidth() < $width || $image->getHeight() < $height) {
466
        $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', ['%dimensions' => $minimum_dimensions]);
467 468 469 470 471 472 473 474 475 476
      }
    }
  }

  return $errors;
}

/**
 * Saves a file to the specified destination and creates a database entry.
 *
477
 * @param string $data
478
 *   A string containing the contents of the file.
479 480 481 482 483
 * @param string|null $destination
 *   (optional) A string containing the destination URI. This must be a stream
 *   wrapper URI. If no value or NULL is provided, a randomized name will be
 *   generated and the file will be saved using Drupal's default files scheme,
 *   usually "public://".
484
 * @param int $replace
485 486 487 488 489 490 491 492
 *   (optional) The replace behavior when the destination file already exists.
 *   Possible values include:
 *   - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
 *     the destination name exists, then its database entry will be updated. If
 *     no database entry is found, then a new one will be created.
 *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
 *     filename is unique.
 *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
493
 *
494
 * @return \Drupal\file\FileInterface|false
495 496 497 498 499
 *   A file entity, or FALSE on error.
 *
 * @see file_unmanaged_save_data()
 */
function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
500
  $user = \Drupal::currentUser();
501 502 503 504 505

  if (empty($destination)) {
    $destination = file_default_scheme() . '://';
  }
  if (!file_valid_uri($destination)) {
506
    \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]);
507 508 509 510 511 512
    drupal_set_message(t('The data could not be saved because the destination is invalid. More information is available in the system log.'), 'error');
    return FALSE;
  }

  if ($uri = file_unmanaged_save_data($data, $destination, $replace)) {
    // Create a file entity.
513
    $file = File::create([
514
      'uri' => $uri,
515
      'uid' => $user->id(),
516
      'status' => FILE_STATUS_PERMANENT,
517
    ]);
518
    // If we are replacing an existing file re-use its database record.
519 520
    // @todo Do not create a new entity in order to update it. See
    //   https://www.drupal.org/node/2241865.
521
    if ($replace == FILE_EXISTS_REPLACE) {
522
      $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
523 524
      if (count($existing_files)) {
        $existing = reset($existing_files);
525
        $file->fid = $existing->id();
526
        $file->setOriginalId($existing->id());
527
        $file->setFilename($existing->getFilename());
528 529 530 531 532
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
533
      $file->setFilename(drupal_basename($destination));
534 535 536 537 538 539 540 541 542 543 544
    }

    $file->save();
    return $file;
  }
  return FALSE;
}

/**
 * Examines a file entity and returns appropriate content headers for download.
 *
545
 * @param \Drupal\file\FileInterface $file
546 547
 *   A file entity.
 *
548
 * @return array
549 550 551
 *   An associative array of headers, as expected by
 *   \Symfony\Component\HttpFoundation\StreamedResponse.
 */
552
function file_get_content_headers(FileInterface $file) {
553
  $type = Unicode::mimeHeaderEncode($file->getMimeType());
554

555
  return [
556
    'Content-Type' => $type,
557
    'Content-Length' => $file->getSize(),
558
    'Cache-Control' => 'private',
559
  ];
560 561
}

562
/**
563
 * Implements hook_theme().
564 565
 */
function file_theme() {
566
  return [
567
    // From file.module.
568 569 570 571
    'file_link' => [
      'variables' => ['file' => NULL, 'description' => NULL, 'attributes' => []],
    ],
    'file_managed_file' => [
572
      'render element' => 'element',
573
    ],
574

575
    // From file.field.inc.
576
    'file_widget_multiple' => [
577
      'render element' => 'element',
578
      'file' => 'file.field.inc',
579 580 581
    ],
    'file_upload_help' => [
      'variables' => ['description' => NULL, 'upload_validators' => NULL, 'cardinality' => NULL],
582
      'file' => 'file.field.inc',
583 584
    ],
  ];
585 586 587
}

/**
588
 * Implements hook_file_download().
589
 */
590
function file_file_download($uri) {
591
  // Get the file record based on the URI. If not in the database just return.
592
  /** @var \Drupal\file\FileInterface[] $files */
593
  $files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
594
  if (count($files)) {
595 596 597
    foreach ($files as $item) {
      // Since some database servers sometimes use a case-insensitive comparison
      // by default, double check that the filename is an exact match.
598
      if ($item->getFileUri() === $uri) {
599 600 601 602
        $file = $item;
        break;
      }
    }
603
  }
604
  if (!isset($file)) {
605 606 607
    return;
  }

608 609 610 611 612 613 614 615 616 617 618 619 620 621
  // Find out if a temporary file is still used in the system.
  if ($file->isTemporary()) {
    $usage = \Drupal::service('file.usage')->listUsage($file);
    if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
      // Deny access to temporary files without usage that are not owned by the
      // same user. This prevents the security issue that a private file that
      // was protected by field permissions becomes available after its usage
      // was removed and before it is actually deleted from the file system.
      // Modules that depend on this behavior should make the file permanent
      // instead.
      return -1;
    }
  }

622
  // Find out which (if any) fields of this type contain the file.
623
  $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
624

625 626 627 628 629
  // Stop processing if there are no references in order to avoid returning
  // headers for files controlled by other modules. Make an exception for
  // temporary files where the host entity has not yet been saved (for example,
  // an image preview on a node/add form) in which case, allow download by the
  // file's owner.
630
  if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
631
    return;
632 633
  }

634
  if (!$file->access('download')) {
635 636 637 638
    return -1;
  }

  // Access is granted.
639 640
  $headers = file_get_content_headers($file);
  return $headers;
641 642
}

643
/**
644
 * Implements hook_cron().
645 646
 */
function file_cron() {
647
  $age = \Drupal::config('system.file')->get('temporary_maximum_age');
648
  $file_storage = \Drupal::entityManager()->getStorage('file');
649 650 651 652 653 654 655 656 657

  // Only delete temporary files if older than $age. Note that automatic cleanup
  // is disabled if $age set to 0.
  if ($age) {
    $fids = Drupal::entityQuery('file')
      ->condition('status', FILE_STATUS_PERMANENT, '<>')
      ->condition('changed', REQUEST_TIME - $age, '<')
      ->range(0, 100)
      ->execute();
658
    $files = $file_storage->loadMultiple($fids);
659
    foreach ($files as $file) {
660
      $references = \Drupal::service('file.usage')->listUsage($file);
661
      if (empty($references)) {
662
        if (file_exists($file->getFileUri())) {
663 664 665
          $file->delete();
        }
        else {
666
          \Drupal::logger('file system')->error('Could not delete temporary file "%path" during garbage collection', ['%path' => $file->getFileUri()]);
667 668 669
        }
      }
      else {
670
        \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))]);
671 672 673 674 675
      }
    }
  }
}

676 677 678 679
/**
 * Saves file uploads to a new location.
 *
 * The files will be added to the {file_managed} table as temporary files.
680 681
 * Temporary files are periodically cleaned. Use the 'file.usage' service to
 * register the usage of the file which will automatically mark it as permanent.
682
 *
683
 * @param string $form_field_name
684 685
 *   A string that is the associative array key of the upload form element in
 *   the form array.
686
 * @param array $validators
687
 *   (optional) An associative array of callback functions used to validate the
688
 *   file. See file_validate() for a full discussion of the array format.
689 690 691 692 693 694 695 696 697 698 699 700
 *   If the array is empty, it will be set up to call file_validate_extensions()
 *   with a safe list of extensions, as follows: "jpg jpeg gif png txt doc
 *   xls pdf ppt pps odt ods odp". To allow all extensions, you must explicitly
 *   set this array to ['file_validate_extensions' => '']. (Beware: this is not
 *   safe and should only be allowed for trusted users, if at all.)
 * @param string|false $destination
 *   (optional) A string containing the URI that the file should be copied to.
 *   This must be a stream wrapper URI. If this value is omitted or set to
 *   FALSE, Drupal's temporary files scheme will be used ("temporary://").
 * @param null|int $delta
 *   (optional) The delta of the file to return the file entity.
 *   Defaults to NULL.
701
 * @param int $replace
702 703
 *   (optional) The replace behavior when the destination file already exists.
 *   Possible values include:
704
 *   - FILE_EXISTS_REPLACE: Replace the existing file.
705 706
 *   - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
 *     filename is unique.
707 708
 *   - FILE_EXISTS_ERROR: Do nothing and return FALSE.
 *
709 710 711 712
 * @return array|\Drupal\file\FileInterface|null|false
 *   An array of file entities or a single file entity if $delta != NULL. Each
 *   array element contains the file entity if the upload succeeded or FALSE if
 *   there was an error. Function returns NULL if no file was uploaded.
713
 */
714
function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
715
  $user = \Drupal::currentUser();
716 717
  static $upload_cache;

718
  $all_files = \Drupal::request()->files->get('files', []);
719
  // Make sure there's an upload to process.
720
  if (empty($all_files[$form_field_name])) {
721 722
    return NULL;
  }
723
  $file_upload = $all_files[$form_field_name];
724 725 726 727 728 729 730 731 732 733 734 735

  // Return cached objects without processing since the file will have
  // already been processed and the paths in $_FILES will be invalid.
  if (isset($upload_cache[$form_field_name])) {
    if (isset($delta)) {
      return $upload_cache[$form_field_name][$delta];
    }
    return $upload_cache[$form_field_name];
  }

  // Prepare uploaded files info. Representation is slightly different
  // for multiple uploads and we fix that here.
736 737
  $uploaded_files = $file_upload;
  if (!is_array($file_upload)) {
738
    $uploaded_files = [$file_upload];
739 740
  }

741
  $files = [];
742
  foreach ($uploaded_files as $i => $file_info) {
743 744 745
    // Check for file upload errors and return FALSE for this file if a lower
    // level system error occurred. For a complete list of errors:
    // See http://php.net/manual/features.file-upload.errors.php.
746
    switch ($file_info->getError()) {
747 748
      case UPLOAD_ERR_INI_SIZE:
      case UPLOAD_ERR_FORM_SIZE:
749
        drupal_set_message(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())]), 'error');
750 751 752 753 754
        $files[$i] = FALSE;
        continue;

      case UPLOAD_ERR_PARTIAL:
      case UPLOAD_ERR_NO_FILE:
755
        drupal_set_message(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]), 'error');
756 757 758 759 760 761
        $files[$i] = FALSE;
        continue;

      case UPLOAD_ERR_OK:
        // Final check that this is a valid upload, if it isn't, use the
        // default error handler.
762
        if (is_uploaded_file($file_info->getRealPath())) {
763 764 765 766 767
          break;
        }

        // Unknown error
      default:
768
        drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]), 'error');
769 770 771 772 773
        $files[$i] = FALSE;
        continue;

    }
    // Begin building file entity.
774
    $values = [
775 776
      'uid' => $user->id(),
      'status' => 0,
777 778 779
      'filename' => $file_info->getClientOriginalName(),
      'uri' => $file_info->getRealPath(),
      'filesize' => $file_info->getSize(),
780
    ];
781
    $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
782
    $file = File::create($values);
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800

    $extensions = '';
    if (isset($validators['file_validate_extensions'])) {
      if (isset($validators['file_validate_extensions'][0])) {
        // Build the list of non-munged extensions if the caller provided them.
        $extensions = $validators['file_validate_extensions'][0];
      }
      else {
        // If 'file_validate_extensions' is set and the list is empty then the
        // caller wants to allow any extension. In this case we have to remove the
        // validator or else it will reject all extensions.
        unset($validators['file_validate_extensions']);
      }
    }
    else {
      // No validator was provided, so add one using the default list.
      // Build a default non-munged safe list for file_munge_filename().
      $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
801
      $validators['file_validate_extensions'] = [];
802 803 804 805 806 807 808 809 810 811 812 813 814
      $validators['file_validate_extensions'][0] = $extensions;
    }

    if (!empty($extensions)) {
      // Munge the filename to protect against possible malicious extension
      // hiding within an unknown file type (ie: filename.html.foo).
      $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
    }

    // Rename potentially executable files, to help prevent exploits (i.e. will
    // rename filename.php.foo and filename.php to filename.php.foo.txt and
    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
    // evaluates to TRUE.
815
    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
816
      $file->setMimeType('text/plain');
817
      // The destination filename will also later be used to create the URI.
818 819 820 821 822
      $file->setFilename($file->getFilename() . '.txt');
      // The .txt extension may not be in the allowed list of extensions. We have
      // to add it here or else the file upload will fail.
      if (!empty($extensions)) {
        $validators['file_validate_extensions'][0] .= ' txt';
823
        drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
824 825 826 827 828 829 830 831