file.inc 42.7 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
Kjartan's avatar
Kjartan committed
2

Dries's avatar
   
Dries committed
3
4
5
6
7
/**
 * @file
 * API for handling file uploads and server file management.
 */

8
use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem;
9
use Drupal\Component\Utility\Unicode;
10
use Drupal\Component\Utility\UrlHelper;
11
use Drupal\Component\PhpStorage\FileStorage;
12
use Drupal\Component\Utility\Bytes;
13
use Drupal\Core\File\FileSystem;
14
use Drupal\Core\Site\Settings;
15
use Drupal\Core\StreamWrapper\PublicStream;
16
use Drupal\Core\StreamWrapper\PrivateStream;
17
18
19

/**
 * Default mode for new directories. See drupal_chmod().
20
21
22
 *
 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\File\FileSystem::CHMOD_DIRECTORY.
23
 */
24
const FILE_CHMOD_DIRECTORY = FileSystem::CHMOD_DIRECTORY;
25
26
27

/**
 * Default mode for new files. See drupal_chmod().
28
29
30
 *
 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\File\FileSystem::CHMOD_FILE.
31
 */
32
const FILE_CHMOD_FILE = FileSystem::CHMOD_FILE;
33

Kjartan's avatar
Kjartan committed
34
/**
Kjartan's avatar
Kjartan committed
35
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
36
 * @{
Dries's avatar
   
Dries committed
37
 * Common file handling functions.
Dries's avatar
   
Dries committed
38
39
 */

40
/**
41
 * Flag used by file_prepare_directory() -- create directory if not present.
42
 */
43
const FILE_CREATE_DIRECTORY = 1;
44
45

/**
46
 * Flag used by file_prepare_directory() -- file permissions may be changed.
47
 */
48
const FILE_MODIFY_PERMISSIONS = 2;
49
50

/**
51
 * Flag for dealing with existing files: Appends number until name is unique.
52
 */
53
const FILE_EXISTS_RENAME = 0;
54
55
56
57

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
58
const FILE_EXISTS_REPLACE = 1;
59
60
61
62

/**
 * Flag for dealing with existing files: Do nothing and return FALSE.
 */
63
const FILE_EXISTS_ERROR = 2;
Dries's avatar
   
Dries committed
64

65
/**
66
67
 * Indicates that the file is permanent and should not be deleted.
 *
68
69
70
71
 * Temporary files older than the system.file.temporary_maximum_age
 * configuration value will be, if clean-up not disabled, removed during cron
 * runs, but permanent files will not be removed during the file garbage
 * collection process.
72
 */
73
const FILE_STATUS_PERMANENT = 1;
74

75
76
77
/**
 * Returns the scheme of a URI (e.g. a stream).
 *
78
79
 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\File\FileSystem::uriScheme().
80
81
 */
function file_uri_scheme($uri) {
82
  return \Drupal::service('file_system')->uriScheme($uri);
83
84
85
}

/**
86
 * Checks that the scheme of a stream URI is valid.
87
 *
88
89
 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\File\FileSystem::validScheme().
90
91
 */
function file_stream_wrapper_valid_scheme($scheme) {
92
  return \Drupal::service('file_system')->validScheme($scheme);
93
94
}

95

96
/**
97
 * Returns the part of a URI after the schema.
98
 *
99
100
 * @param string $uri
 *   A stream, referenced as "scheme://target" or "data:target".
101
 *
102
 * @return string|bool
103
104
105
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
106
107
 *
 * @see file_uri_scheme()
108
109
 */
function file_uri_target($uri) {
110
111
112
  // Remove the scheme from the URI and remove erroneous leading or trailing,
  // forward-slashes and backslashes.
  $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
113

114
115
  // If nothing was replaced, the URI doesn't have a valid scheme.
  return $target !== $uri ? $target : FALSE;
116
117
}

118
/**
119
 * Gets the default file stream implementation.
120
 *
121
 * @return string
122
123
124
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
125
  return \Drupal::config('system.file')->get('default_scheme');
126
127
}

128
129
130
131
132
133
134
135
136
/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove trailing slashes from target
 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
 *
137
 * @param string $uri
138
 *   String reference containing the URI to normalize.
139
 *
140
 * @return string
141
 *   The normalized URI.
142
143
 */
function file_stream_wrapper_uri_normalize($uri) {
144
  $scheme = \Drupal::service('file_system')->uriScheme($uri);
145

146
  if (file_stream_wrapper_valid_scheme($scheme)) {
147
148
    $target = file_uri_target($uri);

149
150
151
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
152
  }
153

154
155
156
  return $uri;
}

Dries's avatar
   
Dries committed
157
/**
158
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
   
Dries committed
159
 *
160
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
   
Dries committed
161
 *
162
 * There are two kinds of local files:
163
164
165
 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
 *   These are files that have either been uploaded by users or were generated
 *   automatically (for example through CSS aggregation).
166
167
168
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
169
 * @param string $uri
170
171
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
172
 *
173
 * @return string
174
 *   A string containing a URL that may be used to access the file.
175
176
177
 *   If the provided string already contains a preceding 'http', 'https', or
 *   '/', nothing is done and the same string is returned. If a stream wrapper
 *   could not be found to generate an external URL, then FALSE is returned.
178
 *
179
 * @see https://www.drupal.org/node/515192
180
 * @see file_url_transform_relative()
Dries's avatar
   
Dries committed
181
 */
182
function file_create_url($uri) {
183
184
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
185
  \Drupal::moduleHandler()->alter('file_url', $uri);
186

187
  $scheme = \Drupal::service('file_system')->uriScheme($uri);
188
189

  if (!$scheme) {
190
191
192
193
194
195
196
    // Allow for:
    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
    //   http://example.com/bar.jpg by the browser when viewing a page over
    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
    // Both types of relative URIs are characterized by a leading slash, hence
    // we can use a single check.
197
    if (Unicode::substr($uri, 0, 1) == '/') {
198
199
200
201
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
202
      // Therefore, return the urlencoded URI with the base URL prepended.
203
204
205
206
207
208
209
210
211
212
213
214
215
      $options = UrlHelper::parse($uri);
      $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
      // Append the query.
      if ($options['query']) {
        $path .= '?' . UrlHelper::buildQuery($options['query']);
      }

      // Append fragment.
      if ($options['fragment']) {
        $path .= '#' . $options['fragment'];
      }

      return $path;
216
    }
217
  }
218
219
220
  elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
    // Check for HTTP and data URI-encoded URLs so that we don't have to
    // implement getExternalUrl() for the HTTP and data schemes.
221
222
223
224
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
225
    if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
226
227
228
229
230
231
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries's avatar
   
Dries committed
232
233
}

234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
 * Transforms an absolute URL of a local file to a relative URL.
 *
 * May be useful to prevent problems on multisite set-ups and prevent mixed
 * content errors when using HTTPS + HTTP.
 *
 * @param string $file_url
 *   A file URL of a local file as generated by file_create_url().
 *
 * @return string
 *   If the file URL indeed pointed to a local file and was indeed absolute,
 *   then the transformed, relative URL to the local file. Otherwise: the
 *   original value of $file_url.
 *
 * @see file_create_url()
 */
function file_url_transform_relative($file_url) {
  // Unfortunately, we pretty much have to duplicate Symfony's
  // Request::getHttpHost() method because Request::getPort() may return NULL
  // instead of a port number.
  $request = \Drupal::request();
  $host = $request->getHost();
  $scheme = $request->getScheme();
  $port = $request->getPort() ?: 80;
  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
    $http_host = $host;
  }
  else {
    $http_host = $host . ':' . $port;
  }

  return preg_replace('|^https?://' . $http_host . '|', '', $file_url);
}

Dries's avatar
   
Dries committed
268
/**
269
 * Checks that the directory exists and is writable.
270
271
272
273
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
274
 * @param $directory
275
276
277
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
278
279
280
 *   A bitmask to indicate if the directory should be created if it does
 *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
 *   (FILE_MODIFY_PERMISSIONS).
281
 *
282
 * @return
283
284
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
   
Dries committed
285
 */
286
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
287
  if (!file_stream_wrapper_valid_scheme(\Drupal::service('file_system')->uriScheme($directory))) {
288
289
290
    // Only trim if we're not dealing with a stream.
    $directory = rtrim($directory, '/\\');
  }
Dries's avatar
   
Dries committed
291
292
293

  // Check if directory exists.
  if (!is_dir($directory)) {
294
295
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
296
    if ($options & FILE_CREATE_DIRECTORY) {
297
      return @drupal_mkdir($directory, NULL, TRUE);
Dries's avatar
   
Dries committed
298
    }
299
    return FALSE;
Dries's avatar
   
Dries committed
300
  }
301
302
303
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
304
    return drupal_chmod($directory);
Dries's avatar
   
Dries committed
305
306
  }

307
  return $writable;
Dries's avatar
   
Dries committed
308
309
310
}

/**
311
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
   
Dries committed
312
 */
313
function file_ensure_htaccess() {
314
  file_save_htaccess('public://', FALSE);
315
  $private_path = PrivateStream::basePath();
316
  if (!empty($private_path)) {
317
    file_save_htaccess('private://', TRUE);
318
  }
319
  file_save_htaccess('temporary://', TRUE);
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335

  // If a staging directory exists then it should contain a .htaccess file.
  // @todo https://www.drupal.org/node/2696103 catch a more specific exception
  //   and simplify this code.
  try {
    $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
  }
  catch (\Exception $e) {
    $staging = FALSE;
  }
  if ($staging) {
    // Note that we log an error here if we can't write the .htaccess file. This
    // can occur if the staging directory is read-only. If it is then it is the
    // user's responsibility to create the .htaccess file.
    file_save_htaccess($staging, TRUE);
  }
Dries's avatar
   
Dries committed
336
337
338
}

/**
339
 * Creates a .htaccess file in the given directory.
Dries's avatar
   
Dries committed
340
 *
341
 * @param string $directory
342
 *   The directory.
343
344
345
346
347
348
349
350
 * @param bool $private
 *   (Optional) FALSE indicates that $directory should be a web-accessible
 *   directory. Defaults to TRUE which indicates a private directory.
 * @param bool $force_overwrite
 *   (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
 *   if one is already present. Defaults to FALSE.
 */
function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
351
  if (\Drupal::service('file_system')->uriScheme($directory)) {
352
    $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
353
354
  }
  else {
355
    $directory = rtrim($directory, '/\\');
356
    $htaccess_path = $directory . '/.htaccess';
357
  }
358

359
  if (file_exists($htaccess_path) && !$force_overwrite) {
360
    // Short circuit if the .htaccess file already exists.
361
    return TRUE;
362
  }
363
  $htaccess_lines = FileStorage::htaccessLines($private);
364
365

  // Write the .htaccess file.
366
367
  if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
    return drupal_chmod($htaccess_path, 0444);
368
369
  }
  else {
370
371
    $variables = array('%directory' => $directory, '@htaccess' => $htaccess_lines);
    \Drupal::logger('security')->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <pre><code>@htaccess</code></pre>", $variables);
372
    return FALSE;
Dries's avatar
   
Dries committed
373
374
375
  }
}

376
377
378
379
380
381
382
383
384
385
386
/**
 * Returns the standard .htaccess lines that Drupal writes to file directories.
 *
 * @param bool $private
 *   (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
 *   public directory. The default is TRUE, which returns the .htaccess lines
 *   for a private directory that should not be web-accessible.
 *
 * @return string
 *   The desired contents of the .htaccess file.
 *
387
388
 * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
 *   Use \Drupal\Component\PhpStorage\FileStorage::htaccessLines().
389
390
391
392
393
 */
function file_htaccess_lines($private = TRUE) {
  return FileStorage::htaccessLines($private);
}

394
/**
395
 * Determines whether the URI has a valid scheme for file API operations.
396
397
398
399
400
401
402
403
404
405
406
407
 *
 * There must be a scheme and it must be a Drupal-provided scheme like
 * 'public', 'private', 'temporary', or an extension provided with
 * hook_stream_wrappers().
 *
 * @param $uri
 *   The URI to be tested.
 *
 * @return
 *   TRUE if the URI is allowed.
 */
function file_valid_uri($uri) {
408
  // Assert that the URI has an allowed scheme. Bare paths are not allowed.
409
  $uri_scheme = \Drupal::service('file_system')->uriScheme($uri);
410
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
411
412
413
414
415
    return FALSE;
  }
  return TRUE;
}

416
/**
417
 * Copies a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
418
 *
419
420
421
422
423
 * 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.
424
 * - If the $source and $destination are equal, the behavior depends on the
425
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
426
 *   will rename the file until the $destination is unique.
427
428
429
 * - Works around a PHP bug where copy() does not properly support streams if
 *   safe_mode or open_basedir are enabled.
 *   @see https://bugs.php.net/bug.php?id=60456
430
431
 *
 * @param $source
432
 *   A string specifying the filepath or URI of the source file.
433
 * @param $destination
434
 *   A URI containing the destination that $source should be copied to. The
435
436
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
437
438
439
440
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
441
 *       unique.
442
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
443
 *
444
445
 * @return
 *   The path to the new file, or FALSE in the event of an error.
446
 *
447
 * @see file_copy()
Dries's avatar
   
Dries committed
448
 */
449
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
  if (!file_unmanaged_prepare($source, $destination, $replace)) {
    return FALSE;
  }
  // Attempt to resolve the URIs. This is necessary in certain configurations
  // (see above).
  $real_source = drupal_realpath($source) ?: $source;
  $real_destination = drupal_realpath($destination) ?: $destination;
  // Perform the copy operation.
  if (!@copy($real_source, $real_destination)) {
    \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
    return FALSE;
  }
  // Set the permissions on the new file.
  drupal_chmod($destination);
  return $destination;
}

/**
 * Internal function that prepares the destination for a file_unmanaged_copy or
 * file_unmanaged_move operation.
 *
 * - Checks if $source and $destination are valid and readable/writable.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 *
 * @param $source
 *   A string specifying the filepath or URI of the source file.
 * @param $destination
 *   A URI containing the destination that $source should be moved/copied to.
 *   The URI may be a bare filepath (without a scheme) and in that case the
 *   default scheme (file://) will be used. If this value is omitted, Drupal's
 *   default files scheme will be used, usually "public://".
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *       unique.
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 *
 * @return
 *   TRUE, or FALSE in the event of an error.
 *
 * @see file_unmanaged_copy()
 * @see file_unmanaged_move()
 */
function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
498
  $original_source = $source;
499
  $logger = \Drupal::logger('file');
500

501
  // Assert that the source file actually exists.
502
  if (!file_exists($source)) {
503
    // @todo Replace drupal_set_message() calls with exceptions instead.
504
    drupal_set_message(t('The specified file %file could not be moved/copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
505
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
506
      $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
507
508
    }
    else {
509
      $logger->notice('File %file could not be moved/copied because it does not exist.', array('%file' => $original_source));
510
    }
511
512
    return FALSE;
  }
Dries's avatar
   
Dries committed
513

514
515
  // Build a destination URI if necessary.
  if (!isset($destination)) {
516
    $destination = file_build_uri(drupal_basename($source));
517
  }
Dries's avatar
   
Dries committed
518
519


520
521
522
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
523
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
524
525
526
527
528
529
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
530
531
      $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
      drupal_set_message(t('The specified file %file could not be moved/copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
532
533
534
      return FALSE;
    }
  }
535

536
537
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
538
  if ($destination === FALSE) {
539
540
    drupal_set_message(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
    $logger->notice('File %file could not be moved/copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
541
    return FALSE;
Dries's avatar
   
Dries committed
542
  }
543
544

  // Assert that the source and destination filenames are not the same.
545
546
547
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
548
549
    drupal_set_message(t('The specified file %file was not moved/copied because it would overwrite itself.', array('%file' => $source)), 'error');
    $logger->notice('File %file could not be moved/copied because it would overwrite itself.', array('%file' => $source));
550
    return FALSE;
Dries's avatar
   
Dries committed
551
  }
552
553
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
554
  return TRUE;
Dries's avatar
   
Dries committed
555
556
}

557
/**
558
 * Constructs a URI to Drupal's default files location given a relative path.
559
560
 */
function file_build_uri($path) {
561
  $uri = file_default_scheme() . '://' . $path;
562
563
564
  return file_stream_wrapper_uri_normalize($uri);
}

565
/**
566
 * Determines the destination path for a file.
567
 *
568
 * @param $destination
569
 *   A string specifying the desired final URI or filepath.
570
571
 * @param $replace
 *   Replace behavior when the destination file already exists.
572
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
573
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
574
 *       unique.
575
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
576
 *
577
 * @return
578
579
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
580
581
582
583
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
584
585
586
587
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

588
      case FILE_EXISTS_RENAME:
589
        $basename = drupal_basename($destination);
590
        $directory = drupal_dirname($destination);
591
592
593
594
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
595
        // Error reporting handled by calling function.
596
597
598
599
600
601
        return FALSE;
    }
  }
  return $destination;
}

602
/**
603
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
604
 *
605
606
607
608
609
610
611
612
613
614
615
 * This is a powerful function that in many ways performs like an advanced
 * version of rename().
 * - Checks if $source and $destination are valid and readable/writable.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Works around a PHP bug where rename() does not properly support streams if
 *   safe_mode or open_basedir are enabled.
 *   @see https://bugs.php.net/bug.php?id=60456
 *
616
 * @param $source
617
 *   A string specifying the filepath or URI of the source file.
618
 * @param $destination
619
620
621
622
 *   A URI containing the destination that $source should be moved to. The
 *   URI may be a bare filepath (without a scheme) and in that case the default
 *   scheme (file://) will be used. If this value is omitted, Drupal's default
 *   files scheme will be used, usually "public://".
623
624
625
626
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
627
 *       unique.
628
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
629
 *
630
 * @return
631
 *   The path to the new file, or FALSE in the event of an error.
632
 *
633
 * @see file_move()
Dries's avatar
   
Dries committed
634
 */
635
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
636
  if (!file_unmanaged_prepare($source, $destination, $replace)) {
637
    return FALSE;
Dries's avatar
   
Dries committed
638
  }
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
  // Ensure compatibility with Windows.
  // @see drupal_unlink()
  if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
    chmod($source, 0600);
  }
  // Attempt to resolve the URIs. This is necessary in certain configurations
  // (see above) and can also permit fast moves across local schemes.
  $real_source = drupal_realpath($source) ?: $source;
  $real_destination = drupal_realpath($destination) ?: $destination;
  // Perform the move operation.
  if (!@rename($real_source, $real_destination)) {
    // Fall back to slow copy and unlink procedure. This is necessary for
    // renames across schemes that are not local, or where rename() has not been
    // implemented. It's not necessary to use drupal_unlink() as the Windows
    // issue has already been resolved above.
    if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
      \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', array('%file' => $source, '%destination' => $destination));
      return FALSE;
    }
  }
  // Set the permissions on the new file.
  drupal_chmod($destination);
  return $destination;
Dries's avatar
   
Dries committed
662
663
}

664
/**
665
 * Modifies a filename as needed for security purposes.
666
 *
667
668
669
670
671
672
673
674
675
676
677
 * Munging a file name prevents unknown file extensions from masking exploit
 * files. When web servers such as Apache decide how to process a URL request,
 * they use the file extension. If the extension is not recognized, Apache
 * skips that extension and uses the previous file extension. For example, if
 * the file being requested is exploit.php.pps, and Apache does not recognize
 * the '.pps' extension, it treats the file as PHP and executes it. To make
 * this file name safe for Apache and prevent it from executing as PHP, the
 * .php extension is "munged" into .php_, making the safe file name
 * exploit.php_.pps.
 *
 * Specifically, this function adds an underscore to all extensions that are
678
 * between 2 and 5 characters in length, internal to the file name, and not
679
680
 * included in $extensions.
 *
681
682
683
 * Function behavior is also controlled by the configuration
 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
684
 * @param $filename
685
 *   File name to modify.
686
 * @param $extensions
687
 *   A space-separated list of extensions that should not be altered.
688
 * @param $alerts
689
690
691
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
692
 * @return string
693
 *   The potentially modified $filename.
694
695
696
697
698
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

  // Allow potentially insecure uploads for very savvy users and admin
699
  if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
700
701
    // Remove any null bytes. See
    // http://php.net/manual/security.filesystem.nullbytes.php
702
703
    $filename = str_replace(chr(0), '', $filename);

704
    $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
705
706
707
708
709
710
711
712
713
714
715

    // Split the filename up by periods. The first part becomes the basename
    // the last part the final extension.
    $filename_parts = explode('.', $filename);
    $new_filename = array_shift($filename_parts); // Remove file basename.
    $final_extension = array_pop($filename_parts); // Remove final extension.

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
716
      $new_filename .= '.' . $filename_part;
717
      if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
718
719
720
        $new_filename .= '_';
      }
    }
721
    $filename = $new_filename . '.' . $final_extension;
722
723
724
725
726
727
728
729
730
731

    if ($alerts && $original != $filename) {
      drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
    }
  }

  return $filename;
}

/**
732
 * Undoes the effect of file_munge_filename().
733
 *
734
735
 * @param $filename
 *   String with the filename to be unmunged.
736
 *
737
738
 * @return
 *   An unmunged filename string.
739
740
741
742
743
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

744
/**
745
 * Creates a full file path from a directory and filename.
746
747
748
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
749
 *
750
751
752
 * @param $basename
 *   String filename
 * @param $directory
753
 *   String containing the directory or parent URI.
754
 *
755
 * @return
756
757
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
758
 */
Dries's avatar
   
Dries committed
759
function file_create_filename($basename, $directory) {
760
761
762
  // Strip control characters (ASCII value < 32). Though these are allowed in
  // some filesystems, not many applications handle them well.
  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
763
764
765
766
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
767

768
769
770
771
772
773
774
775
776
  // A URI or path may already have a trailing slash or look like "public://".
  if (substr($directory, -1) == '/') {
    $separator = '';
  }
  else {
    $separator = '/';
  }

  $destination = $directory . $separator . $basename;
Dries's avatar
   
Dries committed
777

778
  if (file_exists($destination)) {
Dries's avatar
   
Dries committed
779
    // Destination file already exists, generate an alternative.
780
781
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries's avatar
   
Dries committed
782
783
784
785
786
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
787
      $ext = '';
Dries's avatar
   
Dries committed
788
789
790
791
    }

    $counter = 0;
    do {
792
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
793
    } while (file_exists($destination));
Dries's avatar
   
Dries committed
794
795
  }

796
  return $destination;
Dries's avatar
   
Dries committed
797
798
}

799
/**
800
 * Deletes a file and its database record.
801
 *
802
803
804
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
805
 *
806
807
 * @param $fid
 *   The file id.
808
 *
809
 * @see file_unmanaged_delete()
810
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
811
 */
812
813
814
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}
815

816
817
818
819
820
821
822
823
824
825
826
/**
 * Deletes files.
 *
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
 *
 * @param $fid
 *   The file id.
 *
 * @see file_unmanaged_delete()
827
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
828
829
830
 */
function file_delete_multiple(array $fids) {
  entity_delete_multiple('file', $fids);
831
832
833
}

/**
834
 * Deletes a file without database changes or hook invocations.
835
836
837
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
838
 *
839
 * @param $path
840
 *   A string containing a file path or (streamwrapper) URI.
841
 *
842
843
844
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
845
 *
846
 * @see file_delete()
847
 * @see file_unmanaged_delete_recursive()
848
 */
849
function file_unmanaged_delete($path) {
850
851
852
  if (is_file($path)) {
    return drupal_unlink($path);
  }
853
  $logger = \Drupal::logger('file');
854
  if (is_dir($path)) {
855
    $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path));
856
857
    return FALSE;
  }
858
  // Return TRUE for non-existent file, but log that nothing was actually
859
  // deleted, as the current state is the intended result.
860
  if (!file_exists($path)) {
861
    $logger->notice('The file %path was not deleted because it does not exist.', array('%path' => $path));
862
863
    return TRUE;
  }
864
865
  // We cannot handle anything other than files and directories. Log an error
  // for everything else (sockets, symbolic links, etc).
866
  $logger->error('The file %path is not of a recognized type so it was not deleted.', array('%path' => $path));
867
  return FALSE;
Dries's avatar
   
Dries committed
868
869
}

870
/**
871
 * Deletes all files and directories in the specified filepath recursively.
872
873
874
875
876
877
878
879
880
881
882
 *
 * If the specified path is a directory then the function will call itself
 * recursively to process the contents. Once the contents have been removed the
 * directory will also be removed.
 *
 * If the specified path is a file then it will be passed to
 * file_unmanaged_delete().
 *
 * Note that this only deletes visible files with write permission.
 *
 * @param $path
883
 *   A string containing either an URI or a file or directory path.
884
885
886
887
 * @param $callback
 *   (optional) Callback function to run on each file prior to deleting it and
 *   on each directory prior to traversing it. For example, can be used to
 *   modify permissions.
888
 *
889
 * @return
890
 *   TRUE for success or if path does not exist, FALSE in the event of an
891
892
893
894
 *   error.
 *
 * @see file_unmanaged_delete()
 */
895
896
897
898
function file_unmanaged_delete_recursive($path, $callback = NULL) {
  if (isset($callback)) {
    call_user_func($callback, $path);
  }
899
900
901
902
903
904
905
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
906
      file_unmanaged_delete_recursive($entry_path, $callback);
907
    }
908
    $dir->close();
909
910

    return drupal_rmdir($path);
911
912
913
914
  }
  return file_unmanaged_delete($path);
}

915

Dries's avatar
   
Dries committed
916

917
918
919
/**
 * Moves an uploaded file to a new location.
 *
920
921
 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\File\FileSystem::moveUploadedFile().
922
923
 */
function drupal_move_uploaded_file($filename, $uri) {
924
  return \Drupal::service('file_system')->moveUploadedFile($filename, $uri);
925
}
926

927
/**
928
 * Saves a file to the specified destination without invoking file API.
929
930
 *
 * This function is identical to file_save_data() except the file will not be
931
932
 * saved to the {file_managed} table and none of the file_* hooks will be
 * called.
933
934
935
936
 *
 * @param $data
 *   A string containing the contents of the file.
 * @param $destination
937
938
 *   A string containing the destination location. This must be a stream wrapper
 *   URI. If no value is provided, a randomized name will be generated and the
939
940
 *   file will be saved using Drupal's default files scheme, usually
 *   "public://".
941
942
943
944
945
946
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
 *                          unique.
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
947
 *
948
949
 * @return
 *   A string with the path of the resulting file, or FALSE on error.
950
 *
951
952
953
 * @see file_save_data()
 */
function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
954
  // Write the data to a temporary file.
955
  $temp_name = drupal_tempnam('temporary://', 'file');
956
  if (file_put_contents($temp_name, $data) === FALSE) {
957
    drupal_set_message(t('The file could not be created.'), 'error');
958
    return FALSE;
959
960
  }

961
  // Move the file to its final destination.
962
  return file_unmanaged_move($temp_name, $destination, $replace);
963
964
}

Dries's avatar
   
Dries committed
965
/**
966
 * Finds all files that match a given mask in a given directory.
967
 *
968
969
 * Directories and files beginning with a dot are excluded; this prevents
 * hidden files and directories (such as SVN working directories) from being
970
971
972
973
974
 * scanned. Use the umask option to skip configuration directories to
 * eliminate the possibility of accidentally exposing configuration
 * information. Also, you can use the base directory, recurse, and min_depth
 * options to improve performance by limiting how much of the filesystem has
 * to be traversed.
Dries's avatar
   
Dries committed
975
 *
Dries's avatar
Dries committed
976
 * @param $dir
977
 *   The base directory or URI to scan, without trailing slash.
Dries's avatar
Dries committed
978
 * @param $mask
979
 *   The preg_match() regular expression for files to be included.
980
 * @param $options
981
 *   An associative array of additional options, with the following elements:
982
 *   - 'nomask': The preg_match() regular expression for files to be excluded.
983
 *     Defaults to the 'file_scan_ignore_directories' setting.
984
985
986
987
988
989
990
991
992
993
 *   - 'callback': The callback function to call for each match. There is no
 *     default callback.
 *   - 'recurse': When TRUE, the directory scan will recurse the entire tree
 *     starting at the provided directory. Defaults to TRUE.
 *   - 'key': The key to be used for the returned associative array of files.
 *     Possible values are 'uri', for the file's URI; 'filename', for the
 *     basename of the file; and 'name' for the name of the file without the
 *     extension. Defaults to 'uri'.
 *   - 'min_depth': Minimum depth of directories to return files from. Defaults
 *     to 0.
994
 * @param $depth
99