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