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

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

8
use Drupal\Component\Utility\UrlHelper;
9
use Drupal\Component\PhpStorage\FileStorage;
10
use Drupal\Component\Utility\Bytes;
11
use Drupal\Component\Utility\String;
12
use Drupal\Core\Site\Settings;
13
use Drupal\Core\StreamWrapper\PublicStream;
14

15
/**
webchick's avatar
webchick committed
16
 * Stream wrapper bit flags that are the basis for composite types.
17
 *
webchick's avatar
webchick committed
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 * Note that 0x0002 is skipped, because it was the value of a constant that has
 * since been removed.
 */

/**
 * Stream wrapper bit flag -- a filter that matches all wrappers.
 */
const STREAM_WRAPPERS_ALL = 0x0000;

/**
 * Stream wrapper bit flag -- refers to a local file system location.
 */
const STREAM_WRAPPERS_LOCAL = 0x0001;

/**
 * Stream wrapper bit flag -- wrapper is readable (almost always true).
 */
const STREAM_WRAPPERS_READ = 0x0004;

/**
 * Stream wrapper bit flag -- wrapper is writeable.
 */
const STREAM_WRAPPERS_WRITE = 0x0008;

/**
 * Stream wrapper bit flag -- exposed in the UI and potentially web accessible.
 */
const STREAM_WRAPPERS_VISIBLE = 0x0010;

47
48
49
50
51
52
53
54
55
56
57

/**
 * Default mode for new directories. See drupal_chmod().
 */
const FILE_CHMOD_DIRECTORY = 0775;

/**
 * Default mode for new files. See drupal_chmod().
 */
const FILE_CHMOD_FILE = 0664;

webchick's avatar
webchick committed
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * Composite stream wrapper bit flags that are usually used as the types.
 */

/**
 * Stream wrapper type flag -- not visible in the UI or accessible via web,
 * but readable and writable. E.g. the temporary directory for uploads.
 */
define('STREAM_WRAPPERS_HIDDEN', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE);

/**
 * Stream wrapper type flag -- hidden, readable and writeable using local files.
 */
define('STREAM_WRAPPERS_LOCAL_HIDDEN', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_HIDDEN);

/**
 * Stream wrapper type flag -- visible, readable and writeable.
 */
define('STREAM_WRAPPERS_WRITE_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE | STREAM_WRAPPERS_VISIBLE);

/**
 * Stream wrapper type flag -- visible and read-only.
 */
define('STREAM_WRAPPERS_READ_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_VISIBLE);

/**
 * Stream wrapper type flag -- the default when 'type' is omitted from
 * hook_stream_wrappers(). This does not include STREAM_WRAPPERS_LOCAL,
 * because PHP grants a greater trust level to local files (for example, they
 * can be used in an "include" statement, regardless of the "allow_url_include"
 * setting), so stream wrappers need to explicitly opt-in to this.
 */
define('STREAM_WRAPPERS_NORMAL', STREAM_WRAPPERS_WRITE_VISIBLE);

/**
 * Stream wrapper type flag -- visible, readable and writeable using local files.
94
 */
webchick's avatar
webchick committed
95
define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
96

Kjartan's avatar
Kjartan committed
97
/**
Kjartan's avatar
Kjartan committed
98
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
99
 * @{
Dries's avatar
   
Dries committed
100
 * Common file handling functions.
101
 *
102
 * Fields on the file entity:
103
104
105
 * - fid: File ID
 * - uid: The {users}.uid of the user who is associated with the file.
 * - filename: Name of the file with no path components. This may differ from
106
107
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
108
109
110
111
 * - uri: URI of the file.
 * - filemime: The file's MIME type.
 * - filesize: The size of the file in bytes.
 * - status: A bitmapped field indicating the status of the file. The first 8
112
 *   bits are reserved for Drupal core. The least significant bit indicates
113
114
115
116
 *   temporary (0) or permanent (1). Temporary files will be removed during
 *   cron runs if they are older than the configuration value
 *   "system.file.temporary_maximum_age", and if clean-up is enabled. Permanent
 *   files will not be removed.
117
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
   
Dries committed
118
119
 */

120
/**
121
 * Flag used by file_prepare_directory() -- create directory if not present.
122
 */
123
const FILE_CREATE_DIRECTORY = 1;
124
125

/**
126
 * Flag used by file_prepare_directory() -- file permissions may be changed.
127
 */
128
const FILE_MODIFY_PERMISSIONS = 2;
129
130

/**
131
 * Flag for dealing with existing files: Appends number until name is unique.
132
 */
133
const FILE_EXISTS_RENAME = 0;
134
135
136
137

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
138
const FILE_EXISTS_REPLACE = 1;
139
140
141
142

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

145
/**
146
147
 * Indicates that the file is permanent and should not be deleted.
 *
148
149
150
151
 * Temporary files older than the system.file.temporary_maximum_age
 * configuration value will be, if clean-up not disabled, removed during cron
 * runs, but permanent files will not be removed during the file garbage
 * collection process.
152
 */
153
const FILE_STATUS_PERMANENT = 1;
154

155
/**
156
 * Provides Drupal stream wrapper registry.
157
158
159
160
161
162
163
164
165
166
167
168
169
170
 *
 * A stream wrapper is an abstraction of a file system that allows Drupal to
 * use the same set of methods to access both local files and remote resources.
 *
 * Provide a facility for managing and querying user-defined stream wrappers
 * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
 * registered to handle a stream, which we need to be able to find the handler
 * for class instantiation.
 *
 * If a module registers a scheme that is already registered with PHP, the
 * existing scheme will be unregistered and replaced with the specified class.
 *
 * A stream is referenced as "scheme://target".
 *
171
172
173
174
 * The optional $filter parameter can be used to retrieve only the stream
 * wrappers that are appropriate for particular usage. For example, this returns
 * only stream wrappers that use local file storage:
 * @code
175
 *   $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
176
177
178
179
180
181
182
183
184
 * @endcode
 *
 * The $filter parameter can only filter to types containing a particular flag.
 * In some cases, you may want to filter to types that do not contain a
 * particular flag. For example, you may want to retrieve all stream wrappers
 * that are not writable, or all stream wrappers that are not local. PHP's
 * array_diff_key() function can be used to help with this. For example, this
 * returns only stream wrappers that do not use local file storage:
 * @code
185
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
186
187
 * @endcode
 *
188
 * @param $filter
189
190
191
192
193
194
 *   (Optional) Filters out all types except those with an on bit for each on
 *   bit in $filter. For example, if $filter is STREAM_WRAPPERS_WRITE_VISIBLE,
 *   which is equal to (STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE |
 *   STREAM_WRAPPERS_VISIBLE), then only stream wrappers with all three of these
 *   bits set are returned. Defaults to STREAM_WRAPPERS_ALL, which returns all
 *   registered stream wrappers.
195
 *
196
 * @return
197
198
199
200
201
 *   An array keyed by scheme, with values containing an array of information
 *   about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
 *   is omitted or set to STREAM_WRAPPERS_ALL, the entire Drupal stream wrapper
 *   registry is returned. Otherwise only the stream wrappers whose 'type'
 *   bitmask has an on bit for each bit specified in $filter are returned.
202
 *
203
204
205
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
206
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
207
  $wrappers_storage = &drupal_static(__FUNCTION__, array());
208

209
210
211
212
  if (empty($wrappers_storage)) {
    // Initialize $wrappers_storage, so that we are not calling this method
    // repeatedly if no stream wrappers exist.
    $wrappers_storage[STREAM_WRAPPERS_ALL] = array();
213
    $wrappers = array();
214
    if (\Drupal::hasService('module_handler')) {
215
216
217
218
219
220
      $wrappers = \Drupal::moduleHandler()->invokeAll('stream_wrappers');
      foreach ($wrappers as $scheme => $info) {
        // Add defaults.
        $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
      }
      \Drupal::moduleHandler()->alter('stream_wrappers', $wrappers);
221
    }
222
223
224
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
webchick's avatar
webchick committed
225
      if (in_array('Drupal\Core\StreamWrapper\StreamWrapperInterface', class_implements($info['class']), TRUE)) {
226
227
228
229
230
231
232
233
        // Record whether we are overriding an existing scheme.
        if (in_array($scheme, $existing, TRUE)) {
          $wrappers[$scheme]['override'] = TRUE;
          stream_wrapper_unregister($scheme);
        }
        else {
          $wrappers[$scheme]['override'] = FALSE;
        }
234
235
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
236
237
        }
        else {
238
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
239
        }
240
      }
241
242
243
244
245
      // Pre-populate the static cache with the filters most typically used.
      $wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme];
      if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
        $wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme];
      }
246
247
    }
  }
248
249
250
251
252

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
253
      if (($info['type'] & $filter) == $filter) {
254
255
256
257
258
259
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
260
261
262
263
264
265
266
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
267
 *
268
269
270
271
272
273
274
275
276
277
278
279
280
 * @return
 *   Return string if a scheme has a registered handler, or FALSE.
 */
function file_stream_wrapper_get_class($scheme) {
  $wrappers = file_get_stream_wrappers();
  return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
}

/**
 * Returns the scheme of a URI (e.g. a stream).
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
281
 *
282
283
284
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
285
286
 *
 * @see file_uri_target()
287
288
 */
function file_uri_scheme($uri) {
289
290
  $position = strpos($uri, '://');
  return $position ? substr($uri, 0, $position) : FALSE;
291
292
293
}

/**
294
 * Checks that the scheme of a stream URI is valid.
295
296
297
298
299
300
301
 *
 * Confirms that there is a registered stream handler for the provided scheme
 * and that it is callable. This is useful if you want to confirm a valid
 * scheme without creating a new instance of the registered handler.
 *
 * @param $scheme
 *   A URI scheme, a stream is referenced as "scheme://target".
302
 *
303
304
305
306
307
 * @return
 *   Returns TRUE if the string is the name of a validated stream,
 *   or FALSE if the scheme does not have a registered handler.
 */
function file_stream_wrapper_valid_scheme($scheme) {
308
  return $scheme && class_exists(file_stream_wrapper_get_class($scheme));
309
310
}

311

312
/**
313
 * Returns the part of a URI after the schema.
314
315
316
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
317
 *
318
319
320
321
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
322
323
 *
 * @see file_uri_scheme()
324
325
 */
function file_uri_target($uri) {
326
327
328
329
  $data = explode('://', $uri, 2);

  // Remove erroneous leading or trailing, forward-slashes and backslashes.
  return count($data) == 2 ? trim($data[1], '\/') : FALSE;
330
331
}

332
/**
333
 * Gets the default file stream implementation.
334
335
336
337
338
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
339
  return \Drupal::config('system.file')->get('default_scheme');
340
341
}

342
343
344
345
346
347
348
349
350
351
352
/**
 * Normalizes a URI by making it syntactically correct.
 *
 * A stream is referenced as "scheme://target".
 *
 * The following actions are taken:
 * - Remove trailing slashes from target
 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
 *
 * @param $uri
 *   String reference containing the URI to normalize.
353
 *
354
355
 * @return
 *   The normalized URI.
356
357
358
359
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

360
  if (file_stream_wrapper_valid_scheme($scheme)) {
361
362
    $target = file_uri_target($uri);

363
364
365
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
366
  }
367

368
369
370
371
  return $uri;
}

/**
372
 * Returns a reference to the stream wrapper class responsible for a given URI.
373
374
375
376
377
378
 *
 * The scheme determines the stream wrapper class that should be
 * used by consulting the stream wrapper registry.
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
379
 *
380
381
382
383
 * @return
 *   Returns a new stream wrapper object appropriate for the given URI or FALSE
 *   if no registered handler could be found. For example, a URI of
 *   "private://example.txt" would return a new private stream wrapper object
webchick's avatar
webchick committed
384
 *   (Drupal\Core\StreamWrapper\PrivateStream).
385
386
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
387
388
389
390
391
392
393
  if ($scheme = file_uri_scheme($uri)) {
    $class = file_stream_wrapper_get_class($scheme);
    if (class_exists($class)) {
      $instance = new $class();
      $instance->setUri($uri);
      return $instance;
    }
394
  }
395
  return FALSE;
396
397
398
}

/**
399
 * Returns a reference to the stream wrapper class responsible for a scheme.
400
401
402
403
404
405
406
407
408
409
410
 *
 * This helper method returns a stream instance using a scheme. That is, the
 * passed string does not contain a "://". For example, "public" is a scheme
 * but "public://" is a URI (stream). This is because the later contains both
 * a scheme and target despite target being empty.
 *
 * Note: the instance URI will be initialized to "scheme://" so that you can
 * make the customary method calls as if you had retrieved an instance by URI.
 *
 * @param $scheme
 *   If the stream was "public://target", "public" would be the scheme.
411
 *
412
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface
413
414
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
webchick's avatar
webchick committed
415
 *   (Drupal\Core\StreamWrapper\PublicStream).
416
417
418
419
420
 *   FALSE is returned if no registered handler could be found.
 */
function file_stream_wrapper_get_instance_by_scheme($scheme) {
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
421
    $instance = new $class();
422
423
424
425
426
427
428
429
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries's avatar
   
Dries committed
430
/**
431
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
   
Dries committed
432
 *
433
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
   
Dries committed
434
 *
435
 * There are two kinds of local files:
436
437
438
 * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
 *   These are files that have either been uploaded by users or were generated
 *   automatically (for example through CSS aggregation).
439
440
441
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
442
 * @param $uri
443
444
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
445
 *
446
 * @return
447
 *   A string containing a URL that may be used to access the file.
448
449
450
 *   If the provided string already contains a preceding 'http', 'https', or
 *   '/', nothing is done and the same string is returned. If a stream wrapper
 *   could not be found to generate an external URL, then FALSE is returned.
451
452
 *
 * @see http://drupal.org/node/515192
453
 * @see file_url_transform_relative()
Dries's avatar
   
Dries committed
454
 */
455
function file_create_url($uri) {
456
457
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
458
  \Drupal::moduleHandler()->alter('file_url', $uri);
459

460
461
462
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
463
464
465
466
467
468
469
470
471
472
473
474
    // Allow for:
    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
    //   http://example.com/bar.jpg by the browser when viewing a page over
    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
    // Both types of relative URIs are characterized by a leading slash, hence
    // we can use a single check.
    if (drupal_substr($uri, 0, 1) == '/') {
      return $uri;
    }
    else {
      // If this is not a properly formatted stream, then it is a shipped file.
475
      // Therefore, return the urlencoded URI with the base URL prepended.
476
      return $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($uri);
477
    }
478
479
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
480
481
    // Check for HTTP so that we don't have to implement getExternalUrl() for
    // the HTTP wrapper.
482
483
484
485
486
487
488
489
490
491
492
    return $uri;
  }
  else {
    // Attempt to return an external URL using the appropriate wrapper.
    if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
      return $wrapper->getExternalUrl();
    }
    else {
      return FALSE;
    }
  }
Dries's avatar
   
Dries committed
493
494
}

495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
/**
 * Transforms an absolute URL of a local file to a relative URL.
 *
 * May be useful to prevent problems on multisite set-ups and prevent mixed
 * content errors when using HTTPS + HTTP.
 *
 * @param string $file_url
 *   A file URL of a local file as generated by file_create_url().
 *
 * @return string
 *   If the file URL indeed pointed to a local file and was indeed absolute,
 *   then the transformed, relative URL to the local file. Otherwise: the
 *   original value of $file_url.
 *
 * @see file_create_url()
 */
function file_url_transform_relative($file_url) {
  // Unfortunately, we pretty much have to duplicate Symfony's
  // Request::getHttpHost() method because Request::getPort() may return NULL
  // instead of a port number.
  $http_host = '';
  $request = \Drupal::request();
  $host = $request->getHost();
  $scheme = $request->getScheme();
  $port = $request->getPort() ?: 80;
  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
    $http_host = $host;
  }
  else {
    $http_host = $host . ':' . $port;
  }

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

Dries's avatar
   
Dries committed
530
/**
531
 * Checks that the directory exists and is writable.
532
533
534
535
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
536
 * @param $directory
537
538
539
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
540
541
542
 *   A bitmask to indicate if the directory should be created if it does
 *   not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
 *   (FILE_MODIFY_PERMISSIONS).
543
 *
544
 * @return
545
546
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
   
Dries committed
547
 */
548
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
549
550
551
552
  if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
    // Only trim if we're not dealing with a stream.
    $directory = rtrim($directory, '/\\');
  }
Dries's avatar
   
Dries committed
553
554
555

  // Check if directory exists.
  if (!is_dir($directory)) {
556
557
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
558
    if ($options & FILE_CREATE_DIRECTORY) {
559
      return @drupal_mkdir($directory, NULL, TRUE);
Dries's avatar
   
Dries committed
560
    }
561
    return FALSE;
Dries's avatar
   
Dries committed
562
  }
563
564
565
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
566
    return drupal_chmod($directory);
Dries's avatar
   
Dries committed
567
568
  }

569
  return $writable;
Dries's avatar
   
Dries committed
570
571
572
}

/**
573
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
   
Dries committed
574
 */
575
function file_ensure_htaccess() {
576
  file_save_htaccess('public://', FALSE);
577
  $private_path = \Drupal::config('system.file')->get('path.private');
578
  if (!empty($private_path)) {
579
    file_save_htaccess('private://', TRUE);
580
  }
581
  file_save_htaccess('temporary://', TRUE);
582
  file_save_htaccess(config_get_config_directory(), TRUE);
583
  file_save_htaccess(config_get_config_directory(CONFIG_STAGING_DIRECTORY), TRUE);
Dries's avatar
   
Dries committed
584
585
586
}

/**
587
 * Creates a .htaccess file in the given directory.
Dries's avatar
   
Dries committed
588
 *
589
 * @param string $directory
590
 *   The directory.
591
592
593
594
595
596
597
598
 * @param bool $private
 *   (Optional) FALSE indicates that $directory should be a web-accessible
 *   directory. Defaults to TRUE which indicates a private directory.
 * @param bool $force_overwrite
 *   (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
 *   if one is already present. Defaults to FALSE.
 */
function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
599
  if (file_uri_scheme($directory)) {
600
    $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
601
602
  }
  else {
603
    $directory = rtrim($directory, '/\\');
604
    $htaccess_path = $directory . '/.htaccess';
605
  }
606

607
  if (file_exists($htaccess_path) && !$force_overwrite) {
608
    // Short circuit if the .htaccess file already exists.
609
    return TRUE;
610
  }
611
  $htaccess_lines = file_htaccess_lines($private);
612
613

  // Write the .htaccess file.
614
615
  if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
    return drupal_chmod($htaccess_path, 0444);
616
617
  }
  else {
618
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(String::checkPlain($htaccess_lines)));
619
    \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: <code>!htaccess</code>", $variables);
620
    return FALSE;
Dries's avatar
   
Dries committed
621
622
623
  }
}

624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
/**
 * Returns the standard .htaccess lines that Drupal writes to file directories.
 *
 * @param bool $private
 *   (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
 *   public directory. The default is TRUE, which returns the .htaccess lines
 *   for a private directory that should not be web-accessible.
 *
 * @return string
 *   The desired contents of the .htaccess file.
 *
 * @see file_create_htaccess()
 */
function file_htaccess_lines($private = TRUE) {
  return FileStorage::htaccessLines($private);
}

641
/**
642
 * Determines whether the URI has a valid scheme for file API operations.
643
644
645
646
647
648
649
650
651
652
653
654
655
656
 *
 * There must be a scheme and it must be a Drupal-provided scheme like
 * 'public', 'private', 'temporary', or an extension provided with
 * hook_stream_wrappers().
 *
 * @param $uri
 *   The URI to be tested.
 *
 * @return
 *   TRUE if the URI is allowed.
 */
function file_valid_uri($uri) {
  // Assert that the URI has an allowed scheme. Barepaths are not allowed.
  $uri_scheme = file_uri_scheme($uri);
657
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
658
659
660
661
662
    return FALSE;
  }
  return TRUE;
}

663
/**
664
 * Copies a file to a new location without invoking the file API.
Dries's avatar
   
Dries committed
665
 *
666
667
668
669
670
 * This is a powerful function that in many ways performs like an advanced
 * version of copy().
 * - Checks if $source and $destination are valid and readable/writable.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
671
 * - If the $source and $destination are equal, the behavior depends on the
672
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
673
 *   will rename the file until the $destination is unique.
674
675
 * - Provides a fallback using realpaths if the move fails using stream
 *   wrappers. This can occur because PHP's copy() function does not properly
676
 *   support streams if open_basedir is enabled. See
677
 *   https://bugs.php.net/bug.php?id=60456
678
679
 *
 * @param $source
680
 *   A string specifying the filepath or URI of the source file.
681
 * @param $destination
682
 *   A URI containing the destination that $source should be copied to. The
683
684
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
685
686
687
688
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
689
 *       unique.
690
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
691
 *
692
693
 * @return
 *   The path to the new file, or FALSE in the event of an error.
694
 *
695
 * @see file_copy()
Dries's avatar
   
Dries committed
696
 */
697
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
698
  $original_source = $source;
699
  $logger = \Drupal::logger('file');
700

701
  // Assert that the source file actually exists.
702
  if (!file_exists($source)) {
703
    // @todo Replace drupal_set_message() calls with exceptions instead.
704
    drupal_set_message(t('The specified file %file could not be copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
705
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
706
      $logger->notice('File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
707
708
    }
    else {
709
      $logger->notice('File %file could not be copied because it does not exist.', array('%file' => $original_source));
710
    }
711
712
    return FALSE;
  }
Dries's avatar
   
Dries committed
713

714
715
  // Build a destination URI if necessary.
  if (!isset($destination)) {
716
    $destination = file_build_uri(drupal_basename($source));
717
  }
Dries's avatar
   
Dries committed
718
719


720
721
722
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
723
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
724
725
726
727
728
729
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
730
      $logger->notice('File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
731
      drupal_set_message(t('The specified file %file could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
732
733
734
      return FALSE;
    }
  }
735

736
737
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
738
  if ($destination === FALSE) {
739
    drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
740
    $logger->notice('File %file could not be copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
741
    return FALSE;
Dries's avatar
   
Dries committed
742
  }
743
744

  // Assert that the source and destination filenames are not the same.
745
746
747
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
748
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
749
    $logger->notice('File %file could not be copied because it would overwrite itself.', array('%file' => $source));
750
    return FALSE;
Dries's avatar
   
Dries committed
751
  }
752
753
754
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
755
  if (!@copy($source, $destination)) {
756
757
758
    // If the copy failed and realpaths exist, retry the operation using them
    // instead.
    if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
759
      $logger->error('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
760
761
      return FALSE;
    }
Dries's avatar
   
Dries committed
762
  }
Dries's avatar
   
Dries committed
763

764
765
  // Set the permissions on the new file.
  drupal_chmod($destination);
766
767

  return $destination;
Dries's avatar
   
Dries committed
768
769
}

770
/**
771
 * Constructs a URI to Drupal's default files location given a relative path.
772
773
 */
function file_build_uri($path) {
774
  $uri = file_default_scheme() . '://' . $path;
775
776
777
  return file_stream_wrapper_uri_normalize($uri);
}

778
/**
779
 * Determines the destination path for a file.
780
 *
781
 * @param $destination
782
 *   A string specifying the desired final URI or filepath.
783
784
 * @param $replace
 *   Replace behavior when the destination file already exists.
785
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
786
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
787
 *       unique.
788
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
789
 *
790
 * @return
791
792
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
793
794
795
796
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
797
798
799
800
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

801
      case FILE_EXISTS_RENAME:
802
        $basename = drupal_basename($destination);
803
        $directory = drupal_dirname($destination);
804
805
806
807
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
808
        // Error reporting handled by calling function.
809
810
811
812
813
814
        return FALSE;
    }
  }
  return $destination;
}

815
/**
816
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
817
 *
818
 * @param $source
819
 *   A string specifying the filepath or URI of the original file.
820
 * @param $destination
821
822
823
 *   A string containing the destination that $source should be moved to.
 *   This must be a stream wrapper URI. If this value is omitted, Drupal's
 *   default files scheme will be used, usually "public://".
824
825
826
827
 * @param $replace
 *   Replace behavior when the destination file already exists:
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
828
 *       unique.
829
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
830
 *
831
 * @return
832
 *   The URI of the moved file, or FALSE in the event of an error.
833
 *
834
 * @see file_move()
Dries's avatar
   
Dries committed
835
 */
836
837
838
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  $filepath = file_unmanaged_copy($source, $destination, $replace);
  if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
839
    return FALSE;
Dries's avatar
   
Dries committed
840
  }
841
  return $filepath;
Dries's avatar
   
Dries committed
842
843
}

844
/**
845
 * Modifies a filename as needed for security purposes.
846
 *
847
848
849
850
851
852
853
854
855
856
857
 * Munging a file name prevents unknown file extensions from masking exploit
 * files. When web servers such as Apache decide how to process a URL request,
 * they use the file extension. If the extension is not recognized, Apache
 * skips that extension and uses the previous file extension. For example, if
 * the file being requested is exploit.php.pps, and Apache does not recognize
 * the '.pps' extension, it treats the file as PHP and executes it. To make
 * this file name safe for Apache and prevent it from executing as PHP, the
 * .php extension is "munged" into .php_, making the safe file name
 * exploit.php_.pps.
 *
 * Specifically, this function adds an underscore to all extensions that are
858
 * between 2 and 5 characters in length, internal to the file name, and not
859
860
 * included in $extensions.
 *
861
862
863
 * Function behavior is also controlled by the configuration
 * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
 * will be made, if it evaluates to FALSE, the filename is 'munged'. *
864
 * @param $filename
865
 *   File name to modify.
866
 * @param $extensions
867
 *   A space-separated list of extensions that should not be altered.
868
 * @param $alerts
869
870
871
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
872
 * @return string
873
 *   The potentially modified $filename.
874
875
876
877
878
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

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

883
884
885
886
887
888
889
890
891
892
893
894
    $whitelist = array_unique(explode(' ', trim($extensions)));

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

    // Loop through the middle parts of the name and add an underscore to the
    // end of each section that could be a file extension but isn't in the list
    // of allowed extensions.
    foreach ($filename_parts as $filename_part) {
895
      $new_filename .= '.' . $filename_part;
896
897
898
899
      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
        $new_filename .= '_';
      }
    }
900
    $filename = $new_filename . '.' . $final_extension;
901
902
903
904
905
906
907
908
909
910

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

  return $filename;
}

/**
911
 * Undoes the effect of file_munge_filename().
912
 *
913
914
 * @param $filename
 *   String with the filename to be unmunged.
915
 *
916
917
 * @return
 *   An unmunged filename string.
918
919
920
921
922
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

923
/**
924
 * Creates a full file path from a directory and filename.
925
926
927
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
928
 *
929
930
931
 * @param $basename
 *   String filename
 * @param $directory
932
 *   String containing the directory or parent URI.
933
 *
934
 * @return
935
936
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
937
 */
Dries's avatar
   
Dries committed
938
function file_create_filename($basename, $directory) {
939
940
941
  // Strip control characters (ASCII value < 32). Though these are allowed in
  // some filesystems, not many applications handle them well.
  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
942
943
944
945
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
946

947
948
949
950
951
952
953
954
955
  // A URI or path may already have a trailing slash or look like "public://".
  if (substr($directory, -1) == '/') {
    $separator = '';
  }
  else {
    $separator = '/';
  }

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

957
  if (file_exists($destination)) {
Dries's avatar
   
Dries committed
958
    // Destination file already exists, generate an alternative.
959
960
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries's avatar
   
Dries committed
961
962
963
964
965
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
966
      $ext = '';
Dries's avatar
   
Dries committed
967
968
969
970
    }

    $counter = 0;
    do {
971
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
972
    } while (file_exists($destination));
Dries's avatar
   
Dries committed
973
974
  }

975
  return $destination;
Dries's avatar
   
Dries committed
976
977
}

978
/**
979
 * Deletes a file and its database record.
980
 *
981
982
983
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
984
 *
985
986
 * @param $fid
 *   The file id.
987
 *
988
 * @see file_unmanaged_delete()
989
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
990
 */
991
992
993
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}
994

995
996
997
998
999
1000
1001
1002
1003
1004
1005
/**
 * Deletes files.
 *
 * Instead of directly deleting a file, it is strongly recommended to delete
 * file usages instead. That will automatically mark the file as temporary and
 * remove it during cleanup.
 *
 * @param $fid
 *   The file id.
 *
 * @see file_unmanaged_delete()
1006
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
1007
1008
1009
 */
function file_delete_multiple(array $fids) {
  entity_delete_multiple('file', $fids);
1010
1011
1012
}

/**
1013
 * Deletes a file without database changes or hook invocations.
1014
1015
1016
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
1017
 *
1018
 * @param $path
1019
 *   A string containing a file path or (streamwrapper) URI.
1020
 *
1021
1022
1023
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
1024
 *
1025
 * @see file_delete()
1026
 * @see file_unmanaged_delete_recursive()
1027
 */
1028
function file_unmanaged_delete($path) {
1029
  $logger = \Drupal::logger('file');
1030
  if (is_dir($path)) {
1031
    $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path));
1032
1033
    return FALSE;
  }
1034
  if (is_file($path)) {
1035
    return drupal_unlink($path);
1036
  }
1037
  // Return TRUE for non-existent file, but log that nothing was actually
1038
  // deleted, as the current state is the intended result.
1039
  if (!file_exists($path)) {
1040
    $logger->notice('The file %path was not deleted because it does not exist.', array('%path' => $path));
1041
1042
    return TRUE;
  }
1043
1044
  // We cannot handle anything other than files and directories. Log an error
  // for everything else (sockets, symbolic links, etc).
1045
  $logger->error('The file %path is not of a recognized type so it was not deleted.', array('%path' => $path));
1046
  return FALSE;