Security update for Core, with self-updated composer
[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   $real_source = drupal_realpath($source) ?: $source;
466   $real_destination = drupal_realpath($destination) ?: $destination;
467   // Perform the copy operation.
468   if (!@copy($real_source, $real_destination)) {
469     \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', ['%file' => $source, '%destination' => $destination]);
470     return FALSE;
471   }
472   // Set the permissions on the new file.
473   drupal_chmod($destination);
474   return $destination;
475 }
476
477 /**
478  * Internal function that prepares the destination for a file_unmanaged_copy or
479  * file_unmanaged_move operation.
480  *
481  * - Checks if $source and $destination are valid and readable/writable.
482  * - Checks that $source is not equal to $destination; if they are an error
483  *   is reported.
484  * - If file already exists in $destination either the call will error out,
485  *   replace the file or rename the file based on the $replace parameter.
486  *
487  * @param $source
488  *   A string specifying the filepath or URI of the source file.
489  * @param $destination
490  *   A URI containing the destination that $source should be moved/copied to.
491  *   The URI may be a bare filepath (without a scheme) and in that case the
492  *   default scheme (file://) will be used. If this value is omitted, Drupal's
493  *   default files scheme will be used, usually "public://".
494  * @param $replace
495  *   Replace behavior when the destination file already exists:
496  *   - FILE_EXISTS_REPLACE - Replace the existing file.
497  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
498  *       unique.
499  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
500  *
501  * @return
502  *   TRUE, or FALSE in the event of an error.
503  *
504  * @see file_unmanaged_copy()
505  * @see file_unmanaged_move()
506  */
507 function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
508   $original_source = $source;
509   $logger = \Drupal::logger('file');
510
511   // Assert that the source file actually exists.
512   if (!file_exists($source)) {
513     // @todo Replace drupal_set_message() calls with exceptions instead.
514     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');
515     if (($realpath = drupal_realpath($original_source)) !== FALSE) {
516       $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', ['%file' => $original_source, '%realpath' => $realpath]);
517     }
518     else {
519       $logger->notice('File %file could not be moved/copied because it does not exist.', ['%file' => $original_source]);
520     }
521     return FALSE;
522   }
523
524   // Build a destination URI if necessary.
525   if (!isset($destination)) {
526     $destination = file_build_uri(drupal_basename($source));
527   }
528
529   // Prepare the destination directory.
530   if (file_prepare_directory($destination)) {
531     // The destination is already a directory, so append the source basename.
532     $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
533   }
534   else {
535     // Perhaps $destination is a dir/file?
536     $dirname = drupal_dirname($destination);
537     if (!file_prepare_directory($dirname)) {
538       // The destination is not valid.
539       $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', ['%file' => $original_source, '%destination' => $dirname]);
540       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');
541       return FALSE;
542     }
543   }
544
545   // Determine whether we can perform this operation based on overwrite rules.
546   $destination = file_destination($destination, $replace);
547   if ($destination === FALSE) {
548     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');
549     $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]);
550     return FALSE;
551   }
552
553   // Assert that the source and destination filenames are not the same.
554   $real_source = drupal_realpath($source);
555   $real_destination = drupal_realpath($destination);
556   if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
557     drupal_set_message(t('The specified file %file was not moved/copied because it would overwrite itself.', ['%file' => $source]), 'error');
558     $logger->notice('File %file could not be moved/copied because it would overwrite itself.', ['%file' => $source]);
559     return FALSE;
560   }
561   // Make sure the .htaccess files are present.
562   file_ensure_htaccess();
563   return TRUE;
564 }
565
566 /**
567  * Constructs a URI to Drupal's default files location given a relative path.
568  */
569 function file_build_uri($path) {
570   $uri = file_default_scheme() . '://' . $path;
571   return file_stream_wrapper_uri_normalize($uri);
572 }
573
574 /**
575  * Determines the destination path for a file.
576  *
577  * @param $destination
578  *   A string specifying the desired final URI or filepath.
579  * @param $replace
580  *   Replace behavior when the destination file already exists.
581  *   - FILE_EXISTS_REPLACE - Replace the existing file.
582  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
583  *       unique.
584  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
585  *
586  * @return
587  *   The destination filepath, or FALSE if the file already exists
588  *   and FILE_EXISTS_ERROR is specified.
589  */
590 function file_destination($destination, $replace) {
591   if (file_exists($destination)) {
592     switch ($replace) {
593       case FILE_EXISTS_REPLACE:
594         // Do nothing here, we want to overwrite the existing file.
595         break;
596
597       case FILE_EXISTS_RENAME:
598         $basename = drupal_basename($destination);
599         $directory = drupal_dirname($destination);
600         $destination = file_create_filename($basename, $directory);
601         break;
602
603       case FILE_EXISTS_ERROR:
604         // Error reporting handled by calling function.
605         return FALSE;
606     }
607   }
608   return $destination;
609 }
610
611 /**
612  * Moves a file to a new location without database changes or hook invocation.
613  *
614  * This is a powerful function that in many ways performs like an advanced
615  * version of rename().
616  * - Checks if $source and $destination are valid and readable/writable.
617  * - Checks that $source is not equal to $destination; if they are an error
618  *   is reported.
619  * - If file already exists in $destination either the call will error out,
620  *   replace the file or rename the file based on the $replace parameter.
621  * - Works around a PHP bug where rename() does not properly support streams if
622  *   safe_mode or open_basedir are enabled.
623  *   @see https://bugs.php.net/bug.php?id=60456
624  *
625  * @param $source
626  *   A string specifying the filepath or URI of the source file.
627  * @param $destination
628  *   A URI containing the destination that $source should be moved to. The
629  *   URI may be a bare filepath (without a scheme) and in that case the default
630  *   scheme (file://) will be used. If this value is omitted, Drupal's default
631  *   files scheme will be used, usually "public://".
632  * @param $replace
633  *   Replace behavior when the destination file already exists:
634  *   - FILE_EXISTS_REPLACE - Replace the existing file.
635  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
636  *       unique.
637  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
638  *
639  * @return
640  *   The path to the new file, or FALSE in the event of an error.
641  *
642  * @see file_move()
643  */
644 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
645   if (!file_unmanaged_prepare($source, $destination, $replace)) {
646     return FALSE;
647   }
648   // Ensure compatibility with Windows.
649   // @see drupal_unlink()
650   if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
651     chmod($source, 0600);
652   }
653   // Attempt to resolve the URIs. This is necessary in certain configurations
654   // (see above) and can also permit fast moves across local schemes.
655   $real_source = drupal_realpath($source) ?: $source;
656   $real_destination = drupal_realpath($destination) ?: $destination;
657   // Perform the move operation.
658   if (!@rename($real_source, $real_destination)) {
659     // Fall back to slow copy and unlink procedure. This is necessary for
660     // renames across schemes that are not local, or where rename() has not been
661     // implemented. It's not necessary to use drupal_unlink() as the Windows
662     // issue has already been resolved above.
663     if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
664       \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', ['%file' => $source, '%destination' => $destination]);
665       return FALSE;
666     }
667   }
668   // Set the permissions on the new file.
669   drupal_chmod($destination);
670   return $destination;
671 }
672
673 /**
674  * Modifies a filename as needed for security purposes.
675  *
676  * Munging a file name prevents unknown file extensions from masking exploit
677  * files. When web servers such as Apache decide how to process a URL request,
678  * they use the file extension. If the extension is not recognized, Apache
679  * skips that extension and uses the previous file extension. For example, if
680  * the file being requested is exploit.php.pps, and Apache does not recognize
681  * the '.pps' extension, it treats the file as PHP and executes it. To make
682  * this file name safe for Apache and prevent it from executing as PHP, the
683  * .php extension is "munged" into .php_, making the safe file name
684  * exploit.php_.pps.
685  *
686  * Specifically, this function adds an underscore to all extensions that are
687  * between 2 and 5 characters in length, internal to the file name, and not
688  * included in $extensions.
689  *
690  * Function behavior is also controlled by the configuration
691  * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
692  * will be made, if it evaluates to FALSE, the filename is 'munged'. *
693  * @param $filename
694  *   File name to modify.
695  * @param $extensions
696  *   A space-separated list of extensions that should not be altered.
697  * @param $alerts
698  *   If TRUE, drupal_set_message() will be called to display a message if the
699  *   file name was changed.
700  *
701  * @return string
702  *   The potentially modified $filename.
703  */
704 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
705   $original = $filename;
706
707   // Allow potentially insecure uploads for very savvy users and admin
708   if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
709     // Remove any null bytes. See
710     // http://php.net/manual/security.filesystem.nullbytes.php
711     $filename = str_replace(chr(0), '', $filename);
712
713     $whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
714
715     // Split the filename up by periods. The first part becomes the basename
716     // the last part the final extension.
717     $filename_parts = explode('.', $filename);
718     // Remove file basename.
719     $new_filename = array_shift($filename_parts);
720     // Remove final extension.
721     $final_extension = array_pop($filename_parts);
722
723     // Loop through the middle parts of the name and add an underscore to the
724     // end of each section that could be a file extension but isn't in the list
725     // of allowed extensions.
726     foreach ($filename_parts as $filename_part) {
727       $new_filename .= '.' . $filename_part;
728       if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
729         $new_filename .= '_';
730       }
731     }
732     $filename = $new_filename . '.' . $final_extension;
733
734     if ($alerts && $original != $filename) {
735       drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $filename]));
736     }
737   }
738
739   return $filename;
740 }
741
742 /**
743  * Undoes the effect of file_munge_filename().
744  *
745  * @param $filename
746  *   String with the filename to be unmunged.
747  *
748  * @return
749  *   An unmunged filename string.
750  */
751 function file_unmunge_filename($filename) {
752   return str_replace('_.', '.', $filename);
753 }
754
755 /**
756  * Creates a full file path from a directory and filename.
757  *
758  * If a file with the specified name already exists, an alternative will be
759  * used.
760  *
761  * @param $basename
762  *   String filename
763  * @param $directory
764  *   String containing the directory or parent URI.
765  *
766  * @return
767  *   File path consisting of $directory and a unique filename based off
768  *   of $basename.
769  */
770 function file_create_filename($basename, $directory) {
771   // Strip control characters (ASCII value < 32). Though these are allowed in
772   // some filesystems, not many applications handle them well.
773   $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
774   if (substr(PHP_OS, 0, 3) == 'WIN') {
775     // These characters are not allowed in Windows filenames
776     $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
777   }
778
779   // A URI or path may already have a trailing slash or look like "public://".
780   if (substr($directory, -1) == '/') {
781     $separator = '';
782   }
783   else {
784     $separator = '/';
785   }
786
787   $destination = $directory . $separator . $basename;
788
789   if (file_exists($destination)) {
790     // Destination file already exists, generate an alternative.
791     $pos = strrpos($basename, '.');
792     if ($pos !== FALSE) {
793       $name = substr($basename, 0, $pos);
794       $ext = substr($basename, $pos);
795     }
796     else {
797       $name = $basename;
798       $ext = '';
799     }
800
801     $counter = 0;
802     do {
803       $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
804     } while (file_exists($destination));
805   }
806
807   return $destination;
808 }
809
810 /**
811  * Deletes a file and its database record.
812  *
813  * Instead of directly deleting a file, it is strongly recommended to delete
814  * file usages instead. That will automatically mark the file as temporary and
815  * remove it during cleanup.
816  *
817  * @param $fid
818  *   The file id.
819  *
820  * @see file_unmanaged_delete()
821  * @see \Drupal\file\FileUsage\FileUsageBase::delete()
822  */
823 function file_delete($fid) {
824   return file_delete_multiple([$fid]);
825 }
826
827 /**
828  * Deletes files.
829  *
830  * Instead of directly deleting a file, it is strongly recommended to delete
831  * file usages instead. That will automatically mark the file as temporary and
832  * remove it during cleanup.
833  *
834  * @param $fid
835  *   The file id.
836  *
837  * @see file_unmanaged_delete()
838  * @see \Drupal\file\FileUsage\FileUsageBase::delete()
839  */
840 function file_delete_multiple(array $fids) {
841   entity_delete_multiple('file', $fids);
842 }
843
844 /**
845  * Deletes a file without database changes or hook invocations.
846  *
847  * This function should be used when the file to be deleted does not have an
848  * entry recorded in the files table.
849  *
850  * @param $path
851  *   A string containing a file path or (streamwrapper) URI.
852  *
853  * @return
854  *   TRUE for success or path does not exist, or FALSE in the event of an
855  *   error.
856  *
857  * @see file_delete()
858  * @see file_unmanaged_delete_recursive()
859  */
860 function file_unmanaged_delete($path) {
861   if (is_file($path)) {
862     return drupal_unlink($path);
863   }
864   $logger = \Drupal::logger('file');
865   if (is_dir($path)) {
866     $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', ['%path' => $path]);
867     return FALSE;
868   }
869   // Return TRUE for non-existent file, but log that nothing was actually
870   // deleted, as the current state is the intended result.
871   if (!file_exists($path)) {
872     $logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
873     return TRUE;
874   }
875   // We cannot handle anything other than files and directories. Log an error
876   // for everything else (sockets, symbolic links, etc).
877   $logger->error('The file %path is not of a recognized type so it was not deleted.', ['%path' => $path]);
878   return FALSE;
879 }
880
881 /**
882  * Deletes all files and directories in the specified filepath recursively.
883  *
884  * If the specified path is a directory then the function will call itself
885  * recursively to process the contents. Once the contents have been removed the
886  * directory will also be removed.
887  *
888  * If the specified path is a file then it will be passed to
889  * file_unmanaged_delete().
890  *
891  * Note that this only deletes visible files with write permission.
892  *
893  * @param $path
894  *   A string containing either an URI or a file or directory path.
895  * @param $callback
896  *   (optional) Callback function to run on each file prior to deleting it and
897  *   on each directory prior to traversing it. For example, can be used to
898  *   modify permissions.
899  *
900  * @return
901  *   TRUE for success or if path does not exist, FALSE in the event of an
902  *   error.
903  *
904  * @see file_unmanaged_delete()
905  */
906 function file_unmanaged_delete_recursive($path, $callback = NULL) {
907   if (isset($callback)) {
908     call_user_func($callback, $path);
909   }
910   if (is_dir($path)) {
911     $dir = dir($path);
912     while (($entry = $dir->read()) !== FALSE) {
913       if ($entry == '.' || $entry == '..') {
914         continue;
915       }
916       $entry_path = $path . '/' . $entry;
917       file_unmanaged_delete_recursive($entry_path, $callback);
918     }
919     $dir->close();
920
921     return drupal_rmdir($path);
922   }
923   return file_unmanaged_delete($path);
924 }
925
926
927 /**
928  * Moves an uploaded file to a new location.
929  *
930  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
931  *   Use \Drupal\Core\File\FileSystem::moveUploadedFile().
932  *
933  * @see https://www.drupal.org/node/2418133
934  */
935 function drupal_move_uploaded_file($filename, $uri) {
936   return \Drupal::service('file_system')->moveUploadedFile($filename, $uri);
937 }
938
939 /**
940  * Saves a file to the specified destination without invoking file API.
941  *
942  * This function is identical to file_save_data() except the file will not be
943  * saved to the {file_managed} table and none of the file_* hooks will be
944  * called.
945  *
946  * @param $data
947  *   A string containing the contents of the file.
948  * @param $destination
949  *   A string containing the destination location. This must be a stream wrapper
950  *   URI. If no value is provided, a randomized name will be generated and the
951  *   file will be saved using Drupal's default files scheme, usually
952  *   "public://".
953  * @param $replace
954  *   Replace behavior when the destination file already exists:
955  *   - FILE_EXISTS_REPLACE - Replace the existing file.
956  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
957  *                          unique.
958  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
959  *
960  * @return
961  *   A string with the path of the resulting file, or FALSE on error.
962  *
963  * @see file_save_data()
964  */
965 function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
966   // Write the data to a temporary file.
967   $temp_name = drupal_tempnam('temporary://', 'file');
968   if (file_put_contents($temp_name, $data) === FALSE) {
969     drupal_set_message(t('The file could not be created.'), 'error');
970     return FALSE;
971   }
972
973   // Move the file to its final destination.
974   return file_unmanaged_move($temp_name, $destination, $replace);
975 }
976
977 /**
978  * Finds all files that match a given mask in a given directory.
979  *
980  * Directories and files beginning with a dot are excluded; this prevents
981  * hidden files and directories (such as SVN working directories) from being
982  * scanned. Use the umask option to skip configuration directories to
983  * eliminate the possibility of accidentally exposing configuration
984  * information. Also, you can use the base directory, recurse, and min_depth
985  * options to improve performance by limiting how much of the filesystem has
986  * to be traversed.
987  *
988  * @param $dir
989  *   The base directory or URI to scan, without trailing slash.
990  * @param $mask
991  *   The preg_match() regular expression for files to be included.
992  * @param $options
993  *   An associative array of additional options, with the following elements:
994  *   - 'nomask': The preg_match() regular expression for files to be excluded.
995  *     Defaults to the 'file_scan_ignore_directories' setting.
996  *   - 'callback': The callback function to call for each match. There is no
997  *     default callback.
998  *   - 'recurse': When TRUE, the directory scan will recurse the entire tree
999  *     starting at the provided directory. Defaults to TRUE.
1000  *   - 'key': The key to be used for the returned associative array of files.
1001  *     Possible values are 'uri', for the file's URI; 'filename', for the
1002  *     basename of the file; and 'name' for the name of the file without the
1003  *     extension. Defaults to 'uri'.
1004  *   - 'min_depth': Minimum depth of directories to return files from. Defaults
1005  *     to 0.
1006  * @param $depth
1007  *   The current depth of recursion. This parameter is only used internally and
1008  *   should not be passed in.
1009  *
1010  * @return
1011  *   An associative array (keyed on the chosen key) of objects with 'uri',
1012  *   'filename', and 'name' properties corresponding to the matched files.
1013  */
1014 function file_scan_directory($dir, $mask, $options = [], $depth = 0) {
1015   // Merge in defaults.
1016   $options += [
1017     'callback' => 0,
1018     'recurse' => TRUE,
1019     'key' => 'uri',
1020     'min_depth' => 0,
1021   ];
1022   // Normalize $dir only once.
1023   if ($depth == 0) {
1024     $dir = file_stream_wrapper_uri_normalize($dir);
1025     $dir_has_slash = (substr($dir, -1) === '/');
1026   }
1027
1028   // Allow directories specified in settings.php to be ignored. You can use this
1029   // to not check for files in common special-purpose directories. For example,
1030   // node_modules and bower_components. Ignoring irrelevant directories is a
1031   // performance boost.
1032   if (!isset($options['nomask'])) {
1033     $ignore_directories = Settings::get('file_scan_ignore_directories', []);
1034     array_walk($ignore_directories, function (&$value) {
1035       $value = preg_quote($value, '/');
1036     });
1037     $default_nomask = '/^' . implode('|', $ignore_directories) . '$/';
1038   }
1039
1040   $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
1041   $files = [];
1042   // Avoid warnings when opendir does not have the permissions to open a
1043   // directory.
1044   if (is_dir($dir)) {
1045     if ($handle = @opendir($dir)) {
1046       while (FALSE !== ($filename = readdir($handle))) {
1047         // Skip this file if it matches the nomask or starts with a dot.
1048         if ($filename[0] != '.'
1049           && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
1050           && !(!empty($default_nomask) && preg_match($default_nomask, $filename))
1051           ) {
1052           if ($depth == 0 && $dir_has_slash) {
1053             $uri = "$dir$filename";
1054           }
1055           else {
1056             $uri = "$dir/$filename";
1057           }
1058           if ($options['recurse'] && is_dir($uri)) {
1059             // Give priority to files in this folder by merging them in after
1060             // any subdirectory files.
1061             $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
1062           }
1063           elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
1064             // Always use this match over anything already set in $files with
1065             // the same $options['key'].
1066             $file = new stdClass();
1067             $file->uri = $uri;
1068             $file->filename = $filename;
1069             $file->name = pathinfo($filename, PATHINFO_FILENAME);
1070             $key = $options['key'];
1071             $files[$file->$key] = $file;
1072             if ($options['callback']) {
1073               $options['callback']($uri);
1074             }
1075           }
1076         }
1077       }
1078
1079       closedir($handle);
1080     }
1081     else {
1082       \Drupal::logger('file')->error('@dir can not be opened', ['@dir' => $dir]);
1083     }
1084   }
1085
1086   return $files;
1087 }
1088
1089 /**
1090  * Determines the maximum file upload size by querying the PHP settings.
1091  *
1092  * @return
1093  *   A file size limit in bytes based on the PHP upload_max_filesize and
1094  *   post_max_size
1095  */
1096 function file_upload_max_size() {
1097   static $max_size = -1;
1098
1099   if ($max_size < 0) {
1100     // Start with post_max_size.
1101     $max_size = Bytes::toInt(ini_get('post_max_size'));
1102
1103     // If upload_max_size is less, then reduce. Except if upload_max_size is
1104     // zero, which indicates no limit.
1105     $upload_max = Bytes::toInt(ini_get('upload_max_filesize'));
1106     if ($upload_max > 0 && $upload_max < $max_size) {
1107       $max_size = $upload_max;
1108     }
1109   }
1110   return $max_size;
1111 }
1112
1113 /**
1114  * Sets the permissions on a file or directory.
1115  *
1116  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1117  *   Use \Drupal\Core\File\FileSystem::chmod().
1118  *
1119  * @see https://www.drupal.org/node/2418133
1120  */
1121 function drupal_chmod($uri, $mode = NULL) {
1122   return \Drupal::service('file_system')->chmod($uri, $mode);
1123 }
1124
1125 /**
1126  * Deletes a file.
1127  *
1128  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1129  *   Use \Drupal\Core\File\FileSystem::unlink().
1130  *
1131  * @see https://www.drupal.org/node/2418133
1132  */
1133 function drupal_unlink($uri, $context = NULL) {
1134   return \Drupal::service('file_system')->unlink($uri, $context);
1135 }
1136
1137 /**
1138  * Resolves the absolute filepath of a local URI or filepath.
1139  *
1140  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1141  *   Use \Drupal\Core\File\FileSystem::realpath().
1142  *
1143  * @see https://www.drupal.org/node/2418133
1144  */
1145 function drupal_realpath($uri) {
1146   return \Drupal::service('file_system')->realpath($uri);
1147 }
1148
1149 /**
1150  * Gets the name of the directory from a given path.
1151  *
1152  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1153  *   Use \Drupal\Core\File\FileSystem::dirname().
1154  *
1155  * @see https://www.drupal.org/node/2418133
1156  */
1157 function drupal_dirname($uri) {
1158   return \Drupal::service('file_system')->dirname($uri);
1159 }
1160
1161 /**
1162  * Gets the filename from a given path.
1163  *
1164  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1165  *   Use \Drupal\Core\File\FileSystem::basename().
1166  *
1167  * @see https://www.drupal.org/node/2418133
1168  */
1169 function drupal_basename($uri, $suffix = NULL) {
1170   return \Drupal::service('file_system')->basename($uri, $suffix);
1171 }
1172
1173 /**
1174  * Creates a directory, optionally creating missing components in the path to
1175  * the directory.
1176  *
1177  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1178  *   Use \Drupal\Core\File\FileSystem::mkdir().
1179  *
1180  * @see https://www.drupal.org/node/2418133
1181  */
1182 function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
1183   return \Drupal::service('file_system')->mkdir($uri, $mode, $recursive, $context);
1184 }
1185
1186 /**
1187  * Removes a directory.
1188  *
1189  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1190  *   Use \Drupal\Core\File\FileSystem::rmdir().
1191  *
1192  * @see https://www.drupal.org/node/2418133
1193  */
1194 function drupal_rmdir($uri, $context = NULL) {
1195   return \Drupal::service('file_system')->rmdir($uri, $context);
1196 }
1197
1198 /**
1199  * Creates a file with a unique filename in the specified directory.
1200  *
1201  * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
1202  *   Use \Drupal\Core\File\FileSystem::tempnam().
1203  *
1204  * @see https://www.drupal.org/node/2418133
1205  */
1206 function drupal_tempnam($directory, $prefix) {
1207   return \Drupal::service('file_system')->tempnam($directory, $prefix);
1208 }
1209
1210 /**
1211  * Gets and sets the path of the configured temporary directory.
1212  *
1213  * @return mixed|null
1214  *   A string containing the path to the temporary directory.
1215  */
1216 function file_directory_temp() {
1217   $temporary_directory = \Drupal::config('system.file')->get('path.temporary');
1218   if (empty($temporary_directory)) {
1219     // Needs set up.
1220     $config = \Drupal::configFactory()->getEditable('system.file');
1221     $temporary_directory = ComponentFileSystem::getOsTemporaryDirectory();
1222
1223     if (empty($temporary_directory)) {
1224       // If no directory has been found default to 'files/tmp'.
1225       $temporary_directory = PublicStream::basePath() . '/tmp';
1226
1227       // Windows accepts paths with either slash (/) or backslash (\), but will
1228       // not accept a path which contains both a slash and a backslash. Since
1229       // the 'file_public_path' variable may have either format, we sanitize
1230       // everything to use slash which is supported on all platforms.
1231       $temporary_directory = str_replace('\\', '/', $temporary_directory);
1232     }
1233     // Save the path of the discovered directory. Do not check config schema on
1234     // save.
1235     $config->set('path.temporary', (string) $temporary_directory)->save(TRUE);
1236   }
1237
1238   return $temporary_directory;
1239 }
1240
1241 /**
1242  * Discovers a writable system-appropriate temporary directory.
1243  *
1244  * @return mixed
1245  *   A string containing the path to the temporary directory.
1246  *
1247  * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
1248  *   Use \Drupal\Component\FileSystem\FileSystem::getOsTemporaryDirectory().
1249  *
1250  * @see https://www.drupal.org/node/2418133
1251  */
1252 function file_directory_os_temp() {
1253   return ComponentFileSystem::getOsTemporaryDirectory();
1254 }
1255
1256 /**
1257  * @} End of "defgroup file".
1258  */