Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / includes / file.inc
1 <?php
2
3 /**
4  * @file
5  * API for handling file uploads and server file management.
6  */
7
8 use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem;
9 use Drupal\Component\Utility\UrlHelper;
10 use Drupal\Component\PhpStorage\FileStorage;
11 use Drupal\Component\Utility\Bytes;
12 use Drupal\Core\File\FileSystem;
13 use Drupal\Core\Site\Settings;
14 use Drupal\Core\StreamWrapper\PublicStream;
15 use Drupal\Core\StreamWrapper\PrivateStream;
16
17 /**
18  * Default mode for new directories. See drupal_chmod().
19  *
20  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
21  *   Use \Drupal\Core\File\FileSystem::CHMOD_DIRECTORY.
22  *
23  * @see https://www.drupal.org/node/2418133
24  */
25 const FILE_CHMOD_DIRECTORY = FileSystem::CHMOD_DIRECTORY;
26
27 /**
28  * Default mode for new files. See drupal_chmod().
29  *
30  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
31  *   Use \Drupal\Core\File\FileSystem::CHMOD_FILE.
32  *
33  * @see https://www.drupal.org/node/2418133
34  */
35 const FILE_CHMOD_FILE = FileSystem::CHMOD_FILE;
36
37 /**
38  * @defgroup file File interface
39  * @{
40  * Common file handling functions.
41  */
42
43 /**
44  * Flag used by file_prepare_directory() -- create directory if not present.
45  */
46 const FILE_CREATE_DIRECTORY = 1;
47
48 /**
49  * Flag used by file_prepare_directory() -- file permissions may be changed.
50  */
51 const FILE_MODIFY_PERMISSIONS = 2;
52
53 /**
54  * Flag for dealing with existing files: Appends number until name is unique.
55  */
56 const FILE_EXISTS_RENAME = 0;
57
58 /**
59  * Flag for dealing with existing files: Replace the existing file.
60  */
61 const FILE_EXISTS_REPLACE = 1;
62
63 /**
64  * Flag for dealing with existing files: Do nothing and return FALSE.
65  */
66 const FILE_EXISTS_ERROR = 2;
67
68 /**
69  * Indicates that the file is permanent and should not be deleted.
70  *
71  * Temporary files older than the system.file.temporary_maximum_age
72  * configuration value will be, if clean-up not disabled, removed during cron
73  * runs, but permanent files will not be removed during the file garbage
74  * collection process.
75  */
76 const FILE_STATUS_PERMANENT = 1;
77
78 /**
79  * Returns the scheme of a URI (e.g. a stream).
80  *
81  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
82  *   Use \Drupal\Core\File\FileSystem::uriScheme().
83  *
84  * @see https://www.drupal.org/node/2418133
85  */
86 function file_uri_scheme($uri) {
87   return \Drupal::service('file_system')->uriScheme($uri);
88 }
89
90 /**
91  * Checks that the scheme of a stream URI is valid.
92  *
93  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
94  *   Use \Drupal\Core\File\FileSystem::validScheme().
95  *
96  * @see https://www.drupal.org/node/2418133
97  */
98 function file_stream_wrapper_valid_scheme($scheme) {
99   return \Drupal::service('file_system')->validScheme($scheme);
100 }
101
102 /**
103  * Returns the part of a URI after the schema.
104  *
105  * @param string $uri
106  *   A stream, referenced as "scheme://target" or "data:target".
107  *
108  * @return string|bool
109  *   A string containing the target (path), or FALSE if none.
110  *   For example, the URI "public://sample/test.txt" would return
111  *   "sample/test.txt".
112  *
113  * @see file_uri_scheme()
114  */
115 function file_uri_target($uri) {
116   // Remove the scheme from the URI and remove erroneous leading or trailing,
117   // forward-slashes and backslashes.
118   $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/');
119
120   // If nothing was replaced, the URI doesn't have a valid scheme.
121   return $target !== $uri ? $target : FALSE;
122 }
123
124 /**
125  * Gets the default file stream implementation.
126  *
127  * @return string
128  *   'public', 'private' or any other file scheme defined as the default.
129  */
130 function file_default_scheme() {
131   return \Drupal::config('system.file')->get('default_scheme');
132 }
133
134 /**
135  * Normalizes a URI by making it syntactically correct.
136  *
137  * A stream is referenced as "scheme://target".
138  *
139  * The following actions are taken:
140  * - Remove trailing slashes from target
141  * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
142  *
143  * @param string $uri
144  *   String reference containing the URI to normalize.
145  *
146  * @return string
147  *   The normalized URI.
148  */
149 function file_stream_wrapper_uri_normalize($uri) {
150   $scheme = \Drupal::service('file_system')->uriScheme($uri);
151
152   if (file_stream_wrapper_valid_scheme($scheme)) {
153     $target = file_uri_target($uri);
154
155     if ($target !== FALSE) {
156       $uri = $scheme . '://' . $target;
157     }
158   }
159
160   return $uri;
161 }
162
163 /**
164  * Creates a web-accessible URL for a stream to an external or local file.
165  *
166  * Compatibility: normal paths and stream wrappers.
167  *
168  * There are two kinds of local files:
169  * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
170  *   These are files that have either been uploaded by users or were generated
171  *   automatically (for example through CSS aggregation).
172  * - "shipped files", i.e. those outside of the files directory, which ship as
173  *   part of Drupal core or contributed modules or themes.
174  *
175  * @param string $uri
176  *   The URI to a file for which we need an external URL, or the path to a
177  *   shipped file.
178  *
179  * @return string
180  *   A string containing a URL that may be used to access the file.
181  *   If the provided string already contains a preceding 'http', 'https', or
182  *   '/', nothing is done and the same string is returned. If a stream wrapper
183  *   could not be found to generate an external URL, then FALSE is returned.
184  *
185  * @see https://www.drupal.org/node/515192
186  * @see file_url_transform_relative()
187  */
188 function file_create_url($uri) {
189   // Allow the URI to be altered, e.g. to serve a file from a CDN or static
190   // file server.
191   \Drupal::moduleHandler()->alter('file_url', $uri);
192
193   $scheme = \Drupal::service('file_system')->uriScheme($uri);
194
195   if (!$scheme) {
196     // Allow for:
197     // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
198     // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
199     //   http://example.com/bar.jpg by the browser when viewing a page over
200     //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
201     // Both types of relative URIs are characterized by a leading slash, hence
202     // we can use a single check.
203     if (mb_substr($uri, 0, 1) == '/') {
204       return $uri;
205     }
206     else {
207       // If this is not a properly formatted stream, then it is a shipped file.
208       // Therefore, return the urlencoded URI with the base URL prepended.
209       $options = UrlHelper::parse($uri);
210       $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
211       // Append the query.
212       if ($options['query']) {
213         $path .= '?' . UrlHelper::buildQuery($options['query']);
214       }
215
216       // Append fragment.
217       if ($options['fragment']) {
218         $path .= '#' . $options['fragment'];
219       }
220
221       return $path;
222     }
223   }
224   elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
225     // Check for HTTP and data URI-encoded URLs so that we don't have to
226     // implement getExternalUrl() for the HTTP and data schemes.
227     return $uri;
228   }
229   else {
230     // Attempt to return an external URL using the appropriate wrapper.
231     if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
232       return $wrapper->getExternalUrl();
233     }
234     else {
235       return FALSE;
236     }
237   }
238 }
239
240 /**
241  * Transforms an absolute URL of a local file to a relative URL.
242  *
243  * May be useful to prevent problems on multisite set-ups and prevent mixed
244  * content errors when using HTTPS + HTTP.
245  *
246  * @param string $file_url
247  *   A file URL of a local file as generated by file_create_url().
248  *
249  * @return string
250  *   If the file URL indeed pointed to a local file and was indeed absolute,
251  *   then the transformed, relative URL to the local file. Otherwise: the
252  *   original value of $file_url.
253  *
254  * @see file_create_url()
255  */
256 function file_url_transform_relative($file_url) {
257   // Unfortunately, we pretty much have to duplicate Symfony's
258   // Request::getHttpHost() method because Request::getPort() may return NULL
259   // instead of a port number.
260   $request = \Drupal::request();
261   $host = $request->getHost();
262   $scheme = $request->getScheme();
263   $port = $request->getPort() ?: 80;
264   if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
265     $http_host = $host;
266   }
267   else {
268     $http_host = $host . ':' . $port;
269   }
270
271   return preg_replace('|^https?://' . preg_quote($http_host, '|') . '|', '', $file_url);
272 }
273
274 /**
275  * Checks that the directory exists and is writable.
276  *
277  * Directories need to have execute permissions to be considered a directory by
278  * FTP servers, etc.
279  *
280  * @param $directory
281  *   A string reference containing the name of a directory path or URI. A
282  *   trailing slash will be trimmed from a path.
283  * @param $options
284  *   A bitmask to indicate if the directory should be created if it does
285  *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
286  *   (FILE_MODIFY_PERMISSIONS).
287  *
288  * @return
289  *   TRUE if the directory exists (or was created) and is writable. FALSE
290  *   otherwise.
291  */
292 function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
293   if (!file_stream_wrapper_valid_scheme(\Drupal::service('file_system')->uriScheme($directory))) {
294     // Only trim if we're not dealing with a stream.
295     $directory = rtrim($directory, '/\\');
296   }
297
298   // Check if directory exists.
299   if (!is_dir($directory)) {
300     // Let mkdir() recursively create directories and use the default directory
301     // permissions.
302     if ($options & FILE_CREATE_DIRECTORY) {
303       return @drupal_mkdir($directory, NULL, TRUE);
304     }
305     return FALSE;
306   }
307   // The directory exists, so check to see if it is writable.
308   $writable = is_writable($directory);
309   if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
310     return drupal_chmod($directory);
311   }
312
313   return $writable;
314 }
315
316 /**
317  * Creates a .htaccess file in each Drupal files directory if it is missing.
318  */
319 function file_ensure_htaccess() {
320   file_save_htaccess('public://', FALSE);
321   $private_path = PrivateStream::basePath();
322   if (!empty($private_path)) {
323     file_save_htaccess('private://', TRUE);
324   }
325   file_save_htaccess('temporary://', TRUE);
326
327   // If a staging directory exists then it should contain a .htaccess file.
328   // @todo https://www.drupal.org/node/2696103 catch a more specific exception
329   //   and simplify this code.
330   try {
331     $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
332   }
333   catch (\Exception $e) {
334     $staging = FALSE;
335   }
336   if ($staging) {
337     // Note that we log an error here if we can't write the .htaccess file. This
338     // can occur if the staging directory is read-only. If it is then it is the
339     // user's responsibility to create the .htaccess file.
340     file_save_htaccess($staging, TRUE);
341   }
342 }
343
344 /**
345  * Creates a .htaccess file in the given directory.
346  *
347  * @param string $directory
348  *   The directory.
349  * @param bool $private
350  *   (Optional) FALSE indicates that $directory should be a web-accessible
351  *   directory. Defaults to TRUE which indicates a private directory.
352  * @param bool $force_overwrite
353  *   (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
354  *   if one is already present. Defaults to FALSE.
355  */
356 function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
357   if (\Drupal::service('file_system')->uriScheme($directory)) {
358     $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
359   }
360   else {
361     $directory = rtrim($directory, '/\\');
362     $htaccess_path = $directory . '/.htaccess';
363   }
364
365   if (file_exists($htaccess_path) && !$force_overwrite) {
366     // Short circuit if the .htaccess file already exists.
367     return TRUE;
368   }
369   $htaccess_lines = FileStorage::htaccessLines($private);
370
371   // Write the .htaccess file.
372   if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
373     return drupal_chmod($htaccess_path, 0444);
374   }
375   else {
376     $variables = ['%directory' => $directory, '@htaccess' => $htaccess_lines];
377     \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);
378     return FALSE;
379   }
380 }
381
382 /**
383  * Returns the standard .htaccess lines that Drupal writes to file directories.
384  *
385  * @param bool $private
386  *   (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
387  *   public directory. The default is TRUE, which returns the .htaccess lines
388  *   for a private directory that should not be web-accessible.
389  *
390  * @return string
391  *   The desired contents of the .htaccess file.
392  *
393  * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
394  *   Use \Drupal\Component\PhpStorage\FileStorage::htaccessLines().
395  *
396  * @see https://www.drupal.org/node/2418133
397  */
398 function file_htaccess_lines($private = TRUE) {
399   return FileStorage::htaccessLines($private);
400 }
401
402 /**
403  * Determines whether the URI has a valid scheme for file API operations.
404  *
405  * There must be a scheme and it must be a Drupal-provided scheme like
406  * 'public', 'private', 'temporary', or an extension provided with
407  * hook_stream_wrappers().
408  *
409  * @param $uri
410  *   The URI to be tested.
411  *
412  * @return
413  *   TRUE if the URI is allowed.
414  */
415 function file_valid_uri($uri) {
416   // Assert that the URI has an allowed scheme. Bare paths are not allowed.
417   $uri_scheme = \Drupal::service('file_system')->uriScheme($uri);
418   if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
419     return FALSE;
420   }
421   return TRUE;
422 }
423
424 /**
425  * Copies a file to a new location without database changes or hook invocation.
426  *
427  * This is a powerful function that in many ways performs like an advanced
428  * version of copy().
429  * - Checks if $source and $destination are valid and readable/writable.
430  * - If file already exists in $destination either the call will error out,
431  *   replace the file or rename the file based on the $replace parameter.
432  * - If the $source and $destination are equal, the behavior depends on the
433  *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
434  *   will rename the file until the $destination is unique.
435  * - Works around a PHP bug where copy() does not properly support streams if
436  *   safe_mode or open_basedir are enabled.
437  *   @see https://bugs.php.net/bug.php?id=60456
438  *
439  * @param $source
440  *   A string specifying the filepath or URI of the source file.
441  * @param $destination
442  *   A URI containing the destination that $source should be copied to. The
443  *   URI may be a bare filepath (without a scheme). If this value is omitted,
444  *   Drupal's default files scheme will be used, usually "public://".
445  * @param $replace
446  *   Replace behavior when the destination file already exists:
447  *   - FILE_EXISTS_REPLACE - Replace the existing file.
448  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
449  *       unique.
450  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
451  *
452  * @return
453  *   The path to the new file, or FALSE in the event of an error.
454  *
455  * @see file_copy()
456  */
457 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
458   if (!file_unmanaged_prepare($source, $destination, $replace)) {
459     return FALSE;
460   }
461   // Attempt to resolve the URIs. This is necessary in certain configurations
462   // (see above).
463   $file_system = \Drupal::service('file_system');
464   $real_source = $file_system->realpath($source) ?: $source;
465   $real_destination = $file_system->realpath($destination) ?: $destination;
466   // Perform the copy operation.
467   if (!@copy($real_source, $real_destination)) {
468     \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', ['%file' => $source, '%destination' => $destination]);
469     return FALSE;
470   }
471   // Set the permissions on the new file.
472   drupal_chmod($destination);
473   return $destination;
474 }
475
476 /**
477  * Internal function that prepares the destination for a file_unmanaged_copy or
478  * file_unmanaged_move operation.
479  *
480  * - Checks if $source and $destination are valid and readable/writable.
481  * - Checks that $source is not equal to $destination; if they are an error
482  *   is reported.
483  * - If file already exists in $destination either the call will error out,
484  *   replace the file or rename the file based on the $replace parameter.
485  *
486  * @param $source
487  *   A string specifying the filepath or URI of the source file.
488  * @param $destination
489  *   A URI containing the destination that $source should be moved/copied to.
490  *   The URI may be a bare filepath (without a scheme) and in that case the
491  *   default scheme (file://) will be used. If this value is omitted, Drupal's
492  *   default files scheme will be used, usually "public://".
493  * @param $replace
494  *   Replace behavior when the destination file already exists:
495  *   - FILE_EXISTS_REPLACE - Replace the existing file.
496  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
497  *       unique.
498  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
499  *
500  * @return
501  *   TRUE, or FALSE in the event of an error.
502  *
503  * @see file_unmanaged_copy()
504  * @see file_unmanaged_move()
505  */
506 function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
507   $original_source = $source;
508   $logger = \Drupal::logger('file');
509   $file_system = \Drupal::service('file_system');
510
511   // Assert that the source file actually exists.
512   if (!file_exists($source)) {
513     // @todo Replace \Drupal::messenger()->addError() calls with exceptions
514     // instead.
515     \Drupal::messenger()->addError(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.', ['%file' => $original_source]));
516     if (($realpath = $file_system->realpath($original_source)) !== FALSE) {
517       $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', ['%file' => $original_source, '%realpath' => $realpath]);
518     }
519     else {
520       $logger->notice('File %file could not be moved/copied because it does not exist.', ['%file' => $original_source]);
521     }
522     return FALSE;
523   }
524
525   // Build a destination URI if necessary.
526   if (!isset($destination)) {
527     $destination = file_build_uri(drupal_basename($source));
528   }
529
530   // Prepare the destination directory.
531   if (file_prepare_directory($destination)) {
532     // The destination is already a directory, so append the source basename.
533     $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
534   }
535   else {
536     // Perhaps $destination is a dir/file?
537     $dirname = drupal_dirname($destination);
538     if (!file_prepare_directory($dirname)) {
539       // The destination is not valid.
540       $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', ['%file' => $original_source, '%destination' => $dirname]);
541       \Drupal::messenger()->addError(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.', ['%file' => $original_source]));
542       return FALSE;
543     }
544   }
545
546   // Determine whether we can perform this operation based on overwrite rules.
547   $destination = file_destination($destination, $replace);
548   if ($destination === FALSE) {
549     \Drupal::messenger()->addError(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', ['%file' => $original_source]));
550     $logger->notice('File %file could not be moved/copied because a file by that name already exists in the destination directory (%destination)', ['%file' => $original_source, '%destination' => $destination]);
551     return FALSE;
552   }
553
554   // Assert that the source and destination filenames are not the same.
555   $real_source = $file_system->realpath($source);
556   $real_destination = $file_system->realpath($destination);
557   if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
558     \Drupal::messenger()->addError(t('The specified file %file was not moved/copied because it would overwrite itself.', ['%file' => $source]));
559     $logger->notice('File %file could not be moved/copied because it would overwrite itself.', ['%file' => $source]);
560     return FALSE;
561   }
562   // Make sure the .htaccess files are present.
563   file_ensure_htaccess();
564   return TRUE;
565 }
566
567 /**
568  * Constructs a URI to Drupal's default files location given a relative path.
569  */
570 function file_build_uri($path) {
571   $uri = file_default_scheme() . '://' . $path;
572   return file_stream_wrapper_uri_normalize($uri);
573 }
574
575 /**
576  * Determines the destination path for a file.
577  *
578  * @param $destination
579  *   A string specifying the desired final URI or filepath.
580  * @param $replace
581  *   Replace behavior when the destination file already exists.
582  *   - FILE_EXISTS_REPLACE - Replace the existing file.
583  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
584  *       unique.
585  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
586  *
587  * @return
588  *   The destination filepath, or FALSE if the file already exists
589  *   and FILE_EXISTS_ERROR is specified.
590  */
591 function file_destination($destination, $replace) {
592   if (file_exists($destination)) {
593     switch ($replace) {
594       case FILE_EXISTS_REPLACE:
595         // Do nothing here, we want to overwrite the existing file.
596         break;
597
598       case FILE_EXISTS_RENAME:
599         $basename = drupal_basename($destination);
600         $directory = drupal_dirname($destination);
601         $destination = file_create_filename($basename, $directory);
602         break;
603
604       case FILE_EXISTS_ERROR:
605         // Error reporting handled by calling function.
606         return FALSE;
607     }
608   }
609   return $destination;
610 }
611
612 /**
613  * Moves a file to a new location without database changes or hook invocation.
614  *
615  * This is a powerful function that in many ways performs like an advanced
616  * version of rename().
617  * - Checks if $source and $destination are valid and readable/writable.
618  * - Checks that $source is not equal to $destination; if they are an error
619  *   is reported.
620  * - If file already exists in $destination either the call will error out,
621  *   replace the file or rename the file based on the $replace parameter.
622  * - Works around a PHP bug where rename() does not properly support streams if
623  *   safe_mode or open_basedir are enabled.
624  *   @see https://bugs.php.net/bug.php?id=60456
625  *
626  * @param $source
627  *   A string specifying the filepath or URI of the source file.
628  * @param $destination
629  *   A URI containing the destination that $source should be moved to. The
630  *   URI may be a bare filepath (without a scheme) and in that case the default
631  *   scheme (file://) will be used. If this value is omitted, Drupal's default
632  *   files scheme will be used, usually "public://".
633  * @param $replace
634  *   Replace behavior when the destination file already exists:
635  *   - FILE_EXISTS_REPLACE - Replace the existing file.
636  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
637  *       unique.
638  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
639  *
640  * @return
641  *   The path to the new file, or FALSE in the event of an error.
642  *
643  * @see file_move()
644  */
645 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
646   if (!file_unmanaged_prepare($source, $destination, $replace)) {
647     return FALSE;
648   }
649   // Ensure compatibility with Windows.
650   // @see drupal_unlink()
651   if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
652     chmod($source, 0600);
653   }
654   // Attempt to resolve the URIs. This is necessary in certain configurations
655   // (see above) and can also permit fast moves across local schemes.
656   $file_system = \Drupal::service('file_system');
657   $real_source = $file_system->realpath($source) ?: $source;
658   $real_destination = $file_system->realpath($destination) ?: $destination;
659   // Perform the move operation.
660   if (!@rename($real_source, $real_destination)) {
661     // Fall back to slow copy and unlink procedure. This is necessary for
662     // renames across schemes that are not local, or where rename() has not been
663     // implemented. It's not necessary to use drupal_unlink() as the Windows
664     // issue has already been resolved above.
665     if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
666       \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', ['%file' => $source, '%destination' => $destination]);
667       return FALSE;
668     }
669   }
670   // Set the permissions on the new file.
671   drupal_chmod($destination);
672   return $destination;
673 }
674
675 /**
676  * Modifies a filename as needed for security purposes.
677  *
678  * Munging a file name prevents unknown file extensions from masking exploit
679  * files. When web servers such as Apache decide how to process a URL request,
680  * they use the file extension. If the extension is not recognized, Apache
681  * skips that extension and uses the previous file extension. For example, if
682  * the file being requested is exploit.php.pps, and Apache does not recognize
683  * the '.pps' extension, it treats the file as PHP and executes it. To make
684  * this file name safe for Apache and prevent it from executing as PHP, the
685  * .php extension is "munged" into .php_, making the safe file name
686  * exploit.php_.pps.
687  *
688  * Specifically, this function adds an underscore to all extensions that are
689  * between 2 and 5 characters in length, internal to the file name, and not
690  * included in $extensions.
691  *
692  * Function behavior is also controlled by the configuration
693  * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
694  * will be made, if it evaluates to FALSE, the filename is 'munged'. *
695  * @param $filename
696  *   File name to modify.
697  * @param $extensions
698  *   A space-separated list of extensions that should not be altered.
699  * @param $alerts
700  *   If TRUE, \Drupal::messenger()->addStatus() will be called to display
701  *   a message if the file name was changed.
702  *
703  * @return string
704  *   The potentially modified $filename.
705  */
706 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
707   $original = $filename;
708
709   // Allow potentially insecure uploads for very savvy users and admin
710   if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
711     // Remove any null bytes. See
712     // http://php.net/manual/security.filesystem.nullbytes.php
713     $filename = str_replace(chr(0), '', $filename);
714
715     $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
716
717     // Split the filename up by periods. The first part becomes the basename
718     // the last part the final extension.
719     $filename_parts = explode('.', $filename);
720     // Remove file basename.
721     $new_filename = array_shift($filename_parts);
722     // Remove final extension.
723     $final_extension = array_pop($filename_parts);
724
725     // Loop through the middle parts of the name and add an underscore to the
726     // end of each section that could be a file extension but isn't in the list
727     // of allowed extensions.
728     foreach ($filename_parts as $filename_part) {
729       $new_filename .= '.' . $filename_part;
730       if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
731         $new_filename .= '_';
732       }
733     }
734     $filename = $new_filename . '.' . $final_extension;
735
736     if ($alerts && $original != $filename) {
737       \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $filename]));
738     }
739   }
740
741   return $filename;
742 }
743
744 /**
745  * Undoes the effect of file_munge_filename().
746  *
747  * @param $filename
748  *   String with the filename to be unmunged.
749  *
750  * @return
751  *   An unmunged filename string.
752  */
753 function file_unmunge_filename($filename) {
754   return str_replace('_.', '.', $filename);
755 }
756
757 /**
758  * Creates a full file path from a directory and filename.
759  *
760  * If a file with the specified name already exists, an alternative will be
761  * used.
762  *
763  * @param $basename
764  *   String filename
765  * @param $directory
766  *   String containing the directory or parent URI.
767  *
768  * @return
769  *   File path consisting of $directory and a unique filename based off
770  *   of $basename.
771  */
772 function file_create_filename($basename, $directory) {
773   // Strip control characters (ASCII value < 32). Though these are allowed in
774   // some filesystems, not many applications handle them well.
775   $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
776   if (substr(PHP_OS, 0, 3) == 'WIN') {
777     // These characters are not allowed in Windows filenames
778     $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
779   }
780
781   // A URI or path may already have a trailing slash or look like "public://".
782   if (substr($directory, -1) == '/') {
783     $separator = '';
784   }
785   else {
786     $separator = '/';
787   }
788
789   $destination = $directory . $separator . $basename;
790
791   if (file_exists($destination)) {
792     // Destination file already exists, generate an alternative.
793     $pos = strrpos($basename, '.');
794     if ($pos !== FALSE) {
795       $name = substr($basename, 0, $pos);
796       $ext = substr($basename, $pos);
797     }
798     else {
799       $name = $basename;
800       $ext = '';
801     }
802
803     $counter = 0;
804     do {
805       $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
806     } while (file_exists($destination));
807   }
808
809   return $destination;
810 }
811
812 /**
813  * Deletes a file and its database record.
814  *
815  * Instead of directly deleting a file, it is strongly recommended to delete
816  * file usages instead. That will automatically mark the file as temporary and
817  * remove it during cleanup.
818  *
819  * @param $fid
820  *   The file id.
821  *
822  * @see file_unmanaged_delete()
823  * @see \Drupal\file\FileUsage\FileUsageBase::delete()
824  */
825 function file_delete($fid) {
826   return file_delete_multiple([$fid]);
827 }
828
829 /**
830  * Deletes files.
831  *
832  * Instead of directly deleting a file, it is strongly recommended to delete
833  * file usages instead. That will automatically mark the file as temporary and
834  * remove it during cleanup.
835  *
836  * @param $fid
837  *   The file id.
838  *
839  * @see file_unmanaged_delete()
840  * @see \Drupal\file\FileUsage\FileUsageBase::delete()
841  */
842 function file_delete_multiple(array $fids) {
843   entity_delete_multiple('file', $fids);
844 }
845
846 /**
847  * Deletes a file without database changes or hook invocations.
848  *
849  * This function should be used when the file to be deleted does not have an
850  * entry recorded in the files table.
851  *
852  * @param $path
853  *   A string containing a file path or (streamwrapper) URI.
854  *
855  * @return
856  *   TRUE for success or path does not exist, or FALSE in the event of an
857  *   error.
858  *
859  * @see file_delete()
860  * @see file_unmanaged_delete_recursive()
861  */
862 function file_unmanaged_delete($path) {
863   if (is_file($path)) {
864     return drupal_unlink($path);
865   }
866   $logger = \Drupal::logger('file');
867   if (is_dir($path)) {
868     $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', ['%path' => $path]);
869     return FALSE;
870   }
871   // Return TRUE for non-existent file, but log that nothing was actually
872   // deleted, as the current state is the intended result.
873   if (!file_exists($path)) {
874     $logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
875     return TRUE;
876   }
877   // We cannot handle anything other than files and directories. Log an error
878   // for everything else (sockets, symbolic links, etc).
879   $logger->error('The file %path is not of a recognized type so it was not deleted.', ['%path' => $path]);
880   return FALSE;
881 }
882
883 /**
884  * Deletes all files and directories in the specified filepath recursively.
885  *
886  * If the specified path is a directory then the function will call itself
887  * recursively to process the contents. Once the contents have been removed the
888  * directory will also be removed.
889  *
890  * If the specified path is a file then it will be passed to
891  * file_unmanaged_delete().
892  *
893  * Note that this only deletes visible files with write permission.
894  *
895  * @param $path
896  *   A string containing either an URI or a file or directory path.
897  * @param callable $callback
898  *   (optional) Callback function to run on each file prior to deleting it and
899  *   on each directory prior to traversing it. For example, can be used to
900  *   modify permissions.
901  *
902  * @return
903  *   TRUE for success or if path does not exist, FALSE in the event of an
904  *   error.
905  *
906  * @see file_unmanaged_delete()
907  */
908 function file_unmanaged_delete_recursive($path, $callback = NULL) {
909   if (isset($callback)) {
910     call_user_func($callback, $path);
911   }
912   if (is_dir($path)) {
913     $dir = dir($path);
914     while (($entry = $dir->read()) !== FALSE) {
915       if ($entry == '.' || $entry == '..') {
916         continue;
917       }
918       $entry_path = $path . '/' . $entry;
919       file_unmanaged_delete_recursive($entry_path, $callback);
920     }
921     $dir->close();
922
923     return drupal_rmdir($path);
924   }
925   return file_unmanaged_delete($path);
926 }
927
928 /**
929  * Moves an uploaded file to a new location.
930  *
931  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
932  *   Use \Drupal\Core\File\FileSystem::moveUploadedFile().
933  *
934  * @see https://www.drupal.org/node/2418133
935  */
936 function drupal_move_uploaded_file($filename, $uri) {
937   return \Drupal::service('file_system')->moveUploadedFile($filename, $uri);
938 }
939
940 /**
941  * Saves a file to the specified destination without invoking file API.
942  *
943  * This function is identical to file_save_data() except the file will not be
944  * saved to the {file_managed} table and none of the file_* hooks will be
945  * called.
946  *
947  * @param $data
948  *   A string containing the contents of the file.
949  * @param $destination
950  *   A string containing the destination location. This must be a stream wrapper
951  *   URI. If no value is provided, a randomized name will be generated and the
952  *   file will be saved using Drupal's default files scheme, usually
953  *   "public://".
954  * @param $replace
955  *   Replace behavior when the destination file already exists:
956  *   - FILE_EXISTS_REPLACE - Replace the existing file.
957  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
958  *                          unique.
959  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
960  *
961  * @return
962  *   A string with the path of the resulting file, or FALSE on error.
963  *
964  * @see file_save_data()
965  */
966 function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
967   // Write the data to a temporary file.
968   $temp_name = drupal_tempnam('temporary://', 'file');
969   if (file_put_contents($temp_name, $data) === FALSE) {
970     \Drupal::messenger()->addError(t('The file could not be created.'));
971     return FALSE;
972   }
973
974   // Move the file to its final destination.
975   return file_unmanaged_move($temp_name, $destination, $replace);
976 }
977
978 /**
979  * Finds all files that match a given mask in a given directory.
980  *
981  * Directories and files beginning with a dot are excluded; this prevents
982  * hidden files and directories (such as SVN working directories) from being
983  * scanned. Use the umask option to skip configuration directories to
984  * eliminate the possibility of accidentally exposing configuration
985  * information. Also, you can use the base directory, recurse, and min_depth
986  * options to improve performance by limiting how much of the filesystem has
987  * to be traversed.
988  *
989  * @param $dir
990  *   The base directory or URI to scan, without trailing slash.
991  * @param $mask
992  *   The preg_match() regular expression for files to be included.
993  * @param $options
994  *   An associative array of additional options, with the following elements:
995  *   - 'nomask': The preg_match() regular expression for files to be excluded.
996  *     Defaults to the 'file_scan_ignore_directories' setting.
997  *   - 'callback': The callback function to call for each match. There is no
998  *     default callback.
999  *   - 'recurse': When TRUE, the directory scan will recurse the entire tree
1000  *     starting at the provided directory. Defaults to TRUE.
1001  *   - 'key': The key to be used for the returned associative array of files.
1002  *     Possible values are 'uri', for the file's URI; 'filename', for the
1003  *     basename of the file; and 'name' for the name of the file without the
1004  *     extension. Defaults to 'uri'.
1005  *   - 'min_depth': Minimum depth of directories to return files from. Defaults
1006  *     to 0.
1007  * @param $depth
1008  *   The current depth of recursion. This parameter is only used internally and
1009  *   should not be passed in.
1010  *
1011  * @return
1012  *   An associative array (keyed on the chosen key) of objects with 'uri',
1013  *   'filename', and 'name' properties corresponding to the matched files.
1014  */
1015 function file_scan_directory($dir, $mask, $options = [], $depth = 0) {
1016   // Merge in defaults.
1017   $options += [
1018     'callback' => 0,
1019     'recurse' => TRUE,
1020     'key' => 'uri',
1021     'min_depth' => 0,
1022   ];
1023   // Normalize $dir only once.
1024   if ($depth == 0) {
1025     $dir = file_stream_wrapper_uri_normalize($dir);
1026     $dir_has_slash = (substr($dir, -1) === '/');
1027   }
1028
1029   // Allow directories specified in settings.php to be ignored. You can use this
1030   // to not check for files in common special-purpose directories. For example,
1031   // node_modules and bower_components. Ignoring irrelevant directories is a
1032   // performance boost.
1033   if (!isset($options['nomask'])) {
1034     $ignore_directories = Settings::get('file_scan_ignore_directories', []);
1035     array_walk($ignore_directories, function (&$value) {
1036       $value = preg_quote($value, '/');
1037     });
1038     $default_nomask = '/^' . implode('|', $ignore_directories) . '$/';
1039   }
1040
1041   $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
1042   $files = [];
1043   // Avoid warnings when opendir does not have the permissions to open a
1044   // directory.
1045   if (is_dir($dir)) {
1046     if ($handle = @opendir($dir)) {
1047       while (FALSE !== ($filename = readdir($handle))) {
1048         // Skip this file if it matches the nomask or starts with a dot.
1049         if ($filename[0] != '.'
1050           && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
1051           && !(!empty($default_nomask) && preg_match($default_nomask, $filename))
1052           ) {
1053           if ($depth == 0 && $dir_has_slash) {
1054             $uri = "$dir$filename";
1055           }
1056           else {
1057             $uri = "$dir/$filename";
1058           }
1059           if ($options['recurse'] && is_dir($uri)) {
1060             // Give priority to files in this folder by merging them in after
1061             // any subdirectory files.
1062             $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
1063           }
1064           elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
1065             // Always use this match over anything already set in $files with
1066             // the same $options['key'].
1067             $file = new stdClass();
1068             $file->uri = $uri;
1069             $file->filename = $filename;
1070             $file->name = pathinfo($filename, PATHINFO_FILENAME);
1071             $key = $options['key'];
1072             $files[$file->$key] = $file;
1073             if ($options['callback']) {
1074               $options['callback']($uri);
1075             }
1076           }
1077         }
1078       }
1079
1080       closedir($handle);
1081     }
1082     else {
1083       \Drupal::logger('file')->error('@dir can not be opened', ['@dir' => $dir]);
1084     }
1085   }
1086
1087   return $files;
1088 }
1089
1090 /**
1091  * Determines the maximum file upload size by querying the PHP settings.
1092  *
1093  * @return
1094  *   A file size limit in bytes based on the PHP upload_max_filesize and
1095  *   post_max_size
1096  */
1097 function file_upload_max_size() {
1098   static $max_size = -1;
1099
1100   if ($max_size < 0) {
1101     // Start with post_max_size.
1102     $max_size = Bytes::toInt(ini_get('post_max_size'));
1103
1104     // If upload_max_size is less, then reduce. Except if upload_max_size is
1105     // zero, which indicates no limit.
1106     $upload_max = Bytes::toInt(ini_get('upload_max_filesize'));
1107     if ($upload_max > 0 && $upload_max < $max_size) {
1108       $max_size = $upload_max;
1109     }
1110   }
1111   return $max_size;
1112 }
1113
1114 /**
1115  * Sets the permissions on a file or directory.
1116  *
1117  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1118  *   Use \Drupal\Core\File\FileSystem::chmod().
1119  *
1120  * @see https://www.drupal.org/node/2418133
1121  */
1122 function drupal_chmod($uri, $mode = NULL) {
1123   return \Drupal::service('file_system')->chmod($uri, $mode);
1124 }
1125
1126 /**
1127  * Deletes a file.
1128  *
1129  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1130  *   Use \Drupal\Core\File\FileSystem::unlink().
1131  *
1132  * @see https://www.drupal.org/node/2418133
1133  */
1134 function drupal_unlink($uri, $context = NULL) {
1135   return \Drupal::service('file_system')->unlink($uri, $context);
1136 }
1137
1138 /**
1139  * Resolves the absolute filepath of a local URI or filepath.
1140  *
1141  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1142  *   Use \Drupal\Core\File\FileSystem::realpath().
1143  *
1144  * @see https://www.drupal.org/node/2418133
1145  */
1146 function drupal_realpath($uri) {
1147   return \Drupal::service('file_system')->realpath($uri);
1148 }
1149
1150 /**
1151  * Gets the name of the directory from a given path.
1152  *
1153  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1154  *   Use \Drupal\Core\File\FileSystem::dirname().
1155  *
1156  * @see https://www.drupal.org/node/2418133
1157  */
1158 function drupal_dirname($uri) {
1159   return \Drupal::service('file_system')->dirname($uri);
1160 }
1161
1162 /**
1163  * Gets the filename from a given path.
1164  *
1165  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1166  *   Use \Drupal\Core\File\FileSystem::basename().
1167  *
1168  * @see https://www.drupal.org/node/2418133
1169  */
1170 function drupal_basename($uri, $suffix = NULL) {
1171   return \Drupal::service('file_system')->basename($uri, $suffix);
1172 }
1173
1174 /**
1175  * Creates a directory, optionally creating missing components in the path to
1176  * the directory.
1177  *
1178  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1179  *   Use \Drupal\Core\File\FileSystem::mkdir().
1180  *
1181  * @see https://www.drupal.org/node/2418133
1182  */
1183 function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
1184   return \Drupal::service('file_system')->mkdir($uri, $mode, $recursive, $context);
1185 }
1186
1187 /**
1188  * Removes a directory.
1189  *
1190  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1191  *   Use \Drupal\Core\File\FileSystem::rmdir().
1192  *
1193  * @see https://www.drupal.org/node/2418133
1194  */
1195 function drupal_rmdir($uri, $context = NULL) {
1196   return \Drupal::service('file_system')->rmdir($uri, $context);
1197 }
1198
1199 /**
1200  * Creates a file with a unique filename in the specified directory.
1201  *
1202  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1203  *   Use \Drupal\Core\File\FileSystem::tempnam().
1204  *
1205  * @see https://www.drupal.org/node/2418133
1206  */
1207 function drupal_tempnam($directory, $prefix) {
1208   return \Drupal::service('file_system')->tempnam($directory, $prefix);
1209 }
1210
1211 /**
1212  * Gets and sets the path of the configured temporary directory.
1213  *
1214  * @return mixed|null
1215  *   A string containing the path to the temporary directory.
1216  */
1217 function file_directory_temp() {
1218   $temporary_directory = \Drupal::config('system.file')->get('path.temporary');
1219   if (empty($temporary_directory)) {
1220     // Needs set up.
1221     $config = \Drupal::configFactory()->getEditable('system.file');
1222     $temporary_directory = ComponentFileSystem::getOsTemporaryDirectory();
1223
1224     if (empty($temporary_directory)) {
1225       // If no directory has been found default to 'files/tmp'.
1226       $temporary_directory = PublicStream::basePath() . '/tmp';
1227
1228       // Windows accepts paths with either slash (/) or backslash (\), but will
1229       // not accept a path which contains both a slash and a backslash. Since
1230       // the 'file_public_path' variable may have either format, we sanitize
1231       // everything to use slash which is supported on all platforms.
1232       $temporary_directory = str_replace('\\', '/', $temporary_directory);
1233     }
1234     // Save the path of the discovered directory. Do not check config schema on
1235     // save.
1236     $config->set('path.temporary', (string) $temporary_directory)->save(TRUE);
1237   }
1238
1239   return $temporary_directory;
1240 }
1241
1242 /**
1243  * Discovers a writable system-appropriate temporary directory.
1244  *
1245  * @return mixed
1246  *   A string containing the path to the temporary directory.
1247  *
1248  * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
1249  *   Use \Drupal\Component\FileSystem\FileSystem::getOsTemporaryDirectory().
1250  *
1251  * @see https://www.drupal.org/node/2418133
1252  */
1253 function file_directory_os_temp() {
1254   return ComponentFileSystem::getOsTemporaryDirectory();
1255 }
1256
1257 /**
1258  * @} End of "defgroup file".
1259  */