file.inc 56.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
9
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
10
use Drupal\Component\Utility\String;
11
use Drupal\Core\StreamWrapper\PublicStream;
12

13
/**
webchick's avatar
webchick committed
14
 * Stream wrapper bit flags that are the basis for composite types.
15
 *
webchick's avatar
webchick committed
16
17
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
 * 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;

45
46
47
48
49
50
51
52
53
54
55

/**
 * 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
56
57
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
/**
 * 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.
92
 */
webchick's avatar
webchick committed
93
define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
94

Kjartan's avatar
Kjartan committed
95
/**
Kjartan's avatar
Kjartan committed
96
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
97
 * @{
Dries's avatar
   
Dries committed
98
 * Common file handling functions.
99
 *
100
 * Fields on the file entity:
101
102
103
 * - 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
104
105
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
106
107
108
109
 * - 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
110
 *   bits are reserved for Drupal core. The least significant bit indicates
111
112
 *   temporary (0) or permanent (1). Temporary files older than
 *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
113
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
 
Dries committed
114
115
 */

116
/**
117
 * Flag used by file_prepare_directory() -- create directory if not present.
118
 */
119
const FILE_CREATE_DIRECTORY = 1;
120
121

/**
122
 * Flag used by file_prepare_directory() -- file permissions may be changed.
123
 */
124
const FILE_MODIFY_PERMISSIONS = 2;
125
126

/**
127
 * Flag for dealing with existing files: Appends number until name is unique.
128
 */
129
const FILE_EXISTS_RENAME = 0;
130
131
132
133

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
134
const FILE_EXISTS_REPLACE = 1;
135
136
137
138

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

141
/**
142
143
144
145
146
 * Indicates that the file is permanent and should not be deleted.
 *
 * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed
 * during cron runs, but permanent files will not be removed during the file
 * garbage collection process.
147
 */
148
const FILE_STATUS_PERMANENT = 1;
149

150
/**
151
 * Provides Drupal stream wrapper registry.
152
153
154
155
156
157
158
159
160
161
162
163
164
165
 *
 * 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".
 *
166
167
168
169
 * 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
170
 *   $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
171
172
173
174
175
176
177
178
179
 * @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
180
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
181
182
 * @endcode
 *
183
 * @param $filter
184
185
186
187
188
189
 *   (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.
190
 *
191
 * @return
192
193
194
195
196
 *   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.
197
 *
198
199
200
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
201
202
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__);
203

204
  if (!isset($wrappers_storage)) {
205
    $wrappers = \Drupal::moduleHandler()->invokeAll('stream_wrappers');
206
207
208
209
    foreach ($wrappers as $scheme => $info) {
      // Add defaults.
      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
    }
210
211
212
213
    drupal_alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
webchick's avatar
webchick committed
214
      if (in_array('Drupal\Core\StreamWrapper\StreamWrapperInterface', class_implements($info['class']), TRUE)) {
215
216
217
218
219
220
221
222
        // 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;
        }
223
224
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
225
226
        }
        else {
227
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
228
        }
229
      }
230
231
232
233
234
      // 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];
      }
235
236
    }
  }
237
238
239
240
241

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
242
      if (($info['type'] & $filter) == $filter) {
243
244
245
246
247
248
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
249
250
251
252
253
254
255
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
256
 *
257
258
259
260
261
262
263
264
265
266
267
268
269
 * @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".
270
 *
271
272
273
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
274
275
 *
 * @see file_uri_target()
276
277
 */
function file_uri_scheme($uri) {
278
279
  $position = strpos($uri, '://');
  return $position ? substr($uri, 0, $position) : FALSE;
280
281
282
}

/**
283
 * Checks that the scheme of a stream URI is valid.
284
285
286
287
288
289
290
 *
 * 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".
291
 *
292
293
294
295
296
 * @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) {
297
  return $scheme && class_exists(file_stream_wrapper_get_class($scheme));
298
299
}

300

301
/**
302
 * Returns the part of a URI after the schema.
303
304
305
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
306
 *
307
308
309
310
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
311
312
 *
 * @see file_uri_scheme()
313
314
 */
function file_uri_target($uri) {
315
316
317
318
  $data = explode('://', $uri, 2);

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

321
/**
322
 * Gets the default file stream implementation.
323
324
325
326
327
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
328
  return \Drupal::config('system.file')->get('default_scheme');
329
330
}

331
332
333
334
335
336
337
338
339
340
341
/**
 * 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.
342
 *
343
344
 * @return
 *   The normalized URI.
345
346
347
348
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

349
  if (file_stream_wrapper_valid_scheme($scheme)) {
350
351
    $target = file_uri_target($uri);

352
353
354
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
355
  }
356

357
358
359
360
  return $uri;
}

/**
361
 * Returns a reference to the stream wrapper class responsible for a given URI.
362
363
364
365
366
367
 *
 * 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".
368
 *
369
370
371
372
 * @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
373
 *   (Drupal\Core\StreamWrapper\PrivateStream).
374
375
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
376
377
378
379
380
381
382
  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;
    }
383
  }
384
  return FALSE;
385
386
387
}

/**
388
 * Returns a reference to the stream wrapper class responsible for a scheme.
389
390
391
392
393
394
395
396
397
398
399
 *
 * 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.
400
 *
401
 * @return \Drupal\Core\StreamWrapper\StreamWrapperInterface
402
403
 *   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
404
 *   (Drupal\Core\StreamWrapper\PublicStream).
405
406
407
408
409
 *   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)) {
410
    $instance = new $class();
411
412
413
414
415
416
417
418
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries's avatar
 
Dries committed
419
/**
420
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
   
Dries committed
421
 *
422
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
   
Dries committed
423
 *
424
 * There are two kinds of local files:
425
426
427
 * - "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).
428
429
430
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
431
 * @param $uri
432
433
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
434
 *
435
 * @return
436
 *   A string containing a URL that may be used to access the file.
437
438
439
 *   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.
440
441
 *
 * @see http://drupal.org/node/515192
442
 * @see file_url_transform_relative()
Dries's avatar
 
Dries committed
443
 */
444
function file_create_url($uri) {
445
446
447
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
  drupal_alter('file_url', $uri);
448

449
450
451
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
452
453
454
455
456
457
458
459
460
461
462
463
    // 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.
464
465
      // Therefore, return the urlencoded URI with the base URL prepended.
      return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
466
    }
467
468
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
469
470
    // Check for HTTP so that we don't have to implement getExternalUrl() for
    // the HTTP wrapper.
471
472
473
474
475
476
477
478
479
480
481
    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
482
483
}

484
485
486
487
488
489
490
491
492
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
/**
 * 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
519
/**
520
 * Checks that the directory exists and is writable.
521
522
523
524
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
525
 * @param $directory
526
527
528
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
529
530
531
 *   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).
532
 *
533
 * @return
534
535
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
 
Dries committed
536
 */
537
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
538
539
540
541
  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
542
543
544

  // Check if directory exists.
  if (!is_dir($directory)) {
545
546
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
547
    if ($options & FILE_CREATE_DIRECTORY) {
548
      return @drupal_mkdir($directory, NULL, TRUE);
Dries's avatar
   
Dries committed
549
    }
550
    return FALSE;
Dries's avatar
   
Dries committed
551
  }
552
553
554
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
555
    return drupal_chmod($directory);
Dries's avatar
   
Dries committed
556
557
  }

558
  return $writable;
Dries's avatar
 
Dries committed
559
560
561
}

/**
562
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
 
Dries committed
563
 */
564
function file_ensure_htaccess() {
565
  file_save_htaccess('public://', FALSE);
566
  $private_path = \Drupal::config('system.file')->get('path.private');
567
  if (!empty($private_path)) {
568
    file_save_htaccess('private://', TRUE);
569
  }
570
  file_save_htaccess('temporary://', TRUE);
571
  file_save_htaccess(config_get_config_directory(), TRUE);
572
  file_save_htaccess(config_get_config_directory(CONFIG_STAGING_DIRECTORY), TRUE);
Dries's avatar
 
Dries committed
573
574
575
}

/**
576
 * Creates a .htaccess file in the given directory.
Dries's avatar
 
Dries committed
577
 *
578
 * @param $directory
579
580
581
582
 *   The directory.
 * @param $private
 *   FALSE indicates that $directory should be an open and public directory.
 *   The default is TRUE which indicates a private and protected directory.
Dries's avatar
 
Dries committed
583
 */
584
function file_save_htaccess($directory, $private = TRUE) {
585
586
  if (file_uri_scheme($directory)) {
    $directory = file_stream_wrapper_uri_normalize($directory);
587
588
  }
  else {
589
    $directory = rtrim($directory, '/\\');
590
  }
591
592
593
594
595
596
597
598
599
  $htaccess_path =  $directory . '/.htaccess';

  if (file_exists($htaccess_path)) {
    // Short circuit if the .htaccess file already exists.
    return;
  }

  if ($private) {
    // Private .htaccess file.
600
    $htaccess_lines = MTimeProtectedFastFileStorage::HTACCESS;
601
602
603
604
605
606
607
608
609
610
611
  }
  else {
    // Public .htaccess file.
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
  }

  // Write the .htaccess file.
  if (file_put_contents($htaccess_path, $htaccess_lines)) {
    drupal_chmod($htaccess_path, 0444);
  }
  else {
612
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(String::checkPlain($htaccess_lines)));
613
    watchdog('security', "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, WATCHDOG_ERROR);
Dries's avatar
 
Dries committed
614
615
616
  }
}

617
/**
618
 * Determines whether the URI has a valid scheme for file API operations.
619
620
621
622
623
624
625
626
627
628
629
630
631
632
 *
 * 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);
633
  if (!file_stream_wrapper_valid_scheme($uri_scheme)) {
634
635
636
637
638
    return FALSE;
  }
  return TRUE;
}

639
/**
640
 * Copies a file to a new location without invoking the file API.
Dries's avatar
 
Dries committed
641
 *
642
643
644
645
646
 * 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.
647
 * - If the $source and $destination are equal, the behavior depends on the
648
 *   $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
649
 *   will rename the file until the $destination is unique.
650
651
652
653
 * - Provides a fallback using realpaths if the move fails using stream
 *   wrappers. This can occur because PHP's copy() function does not properly
 *   support streams if safe_mode or open_basedir are enabled. See
 *   https://bugs.php.net/bug.php?id=60456
654
655
 *
 * @param $source
656
 *   A string specifying the filepath or URI of the source file.
657
 * @param $destination
658
 *   A URI containing the destination that $source should be copied to. The
659
660
 *   URI may be a bare filepath (without a scheme). If this value is omitted,
 *   Drupal's default files scheme will be used, usually "public://".
661
662
663
664
 * @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
665
 *       unique.
666
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
667
 *
668
669
 * @return
 *   The path to the new file, or FALSE in the event of an error.
670
 *
671
 * @see file_copy()
Dries's avatar
 
Dries committed
672
 */
673
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
674
675
  $original_source = $source;

676
  // Assert that the source file actually exists.
677
  if (!file_exists($source)) {
678
    // @todo Replace drupal_set_message() calls with exceptions instead.
679
    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');
680
681
682
683
684
685
    if (($realpath = drupal_realpath($original_source)) !== FALSE) {
      watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
    }
    else {
      watchdog('file', 'File %file could not be copied because it does not exist.', array('%file' => $original_source));
    }
686
687
    return FALSE;
  }
Dries's avatar
 
Dries committed
688

689
690
  // Build a destination URI if necessary.
  if (!isset($destination)) {
691
    $destination = file_build_uri(drupal_basename($source));
692
  }
Dries's avatar
 
Dries committed
693
694


695
696
697
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
698
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
699
700
701
702
703
704
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
705
706
      watchdog('file', 'File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
      drupal_set_message(t('The specified file %file could not be 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');
707
708
709
      return FALSE;
    }
  }
710

711
712
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
713
  if ($destination === FALSE) {
714
    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');
715
    watchdog('file', '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));
716
    return FALSE;
Dries's avatar
 
Dries committed
717
  }
718
719

  // Assert that the source and destination filenames are not the same.
720
721
722
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
723
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
724
    watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
725
    return FALSE;
Dries's avatar
 
Dries committed
726
  }
727
728
729
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
730
  if (!@copy($source, $destination)) {
731
732
733
734
735
736
    // 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)) {
      watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR);
      return FALSE;
    }
Dries's avatar
 
Dries committed
737
  }
Dries's avatar
   
Dries committed
738

739
740
  // Set the permissions on the new file.
  drupal_chmod($destination);
741
742

  return $destination;
Dries's avatar
 
Dries committed
743
744
}

745
/**
746
 * Constructs a URI to Drupal's default files location given a relative path.
747
748
 */
function file_build_uri($path) {
749
  $uri = file_default_scheme() . '://' . $path;
750
751
752
  return file_stream_wrapper_uri_normalize($uri);
}

753
/**
754
 * Determines the destination path for a file.
755
 *
756
 * @param $destination
757
 *   A string specifying the desired final URI or filepath.
758
759
 * @param $replace
 *   Replace behavior when the destination file already exists.
760
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
761
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
762
 *       unique.
763
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
764
 *
765
 * @return
766
767
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
768
769
770
771
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
772
773
774
775
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

776
      case FILE_EXISTS_RENAME:
777
        $basename = drupal_basename($destination);
778
        $directory = drupal_dirname($destination);
779
780
781
782
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
783
        // Error reporting handled by calling function.
784
785
786
787
788
789
        return FALSE;
    }
  }
  return $destination;
}

790
/**
791
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
   
Dries committed
792
 *
793
 * @param $source
794
 *   A string specifying the filepath or URI of the original file.
795
 * @param $destination
796
797
798
 *   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://".
799
800
801
802
 * @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
803
 *       unique.
804
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
805
 *
806
 * @return
807
 *   The URI of the moved file, or FALSE in the event of an error.
808
 *
809
 * @see file_move()
Dries's avatar
   
Dries committed
810
 */
811
812
813
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) {
814
    return FALSE;
Dries's avatar
 
Dries committed
815
  }
816
  return $filepath;
Dries's avatar
 
Dries committed
817
818
}

819
/**
820
 * Modifies a filename as needed for security purposes.
821
 *
822
823
824
825
826
827
828
829
830
831
832
 * 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
833
 * between 2 and 5 characters in length, internal to the file name, and not
834
835
 * included in $extensions.
 *
836
837
838
 * 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'. *
839
 * @param $filename
840
 *   File name to modify.
841
 * @param $extensions
842
 *   A space-separated list of extensions that should not be altered.
843
 * @param $alerts
844
845
846
 *   If TRUE, drupal_set_message() will be called to display a message if the
 *   file name was changed.
 *
847
 * @return string
848
 *   The potentially modified $filename.
849
850
851
852
853
 */
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
  $original = $filename;

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

858
859
860
861
862
863
864
865
866
867
868
869
    $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) {
870
      $new_filename .= '.' . $filename_part;
871
872
873
874
      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
        $new_filename .= '_';
      }
    }
875
    $filename = $new_filename . '.' . $final_extension;
876
877
878
879
880
881
882
883
884
885

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

  return $filename;
}

/**
886
 * Undoes the effect of file_munge_filename().
887
 *
888
889
 * @param $filename
 *   String with the filename to be unmunged.
890
 *
891
892
 * @return
 *   An unmunged filename string.
893
894
895
896
897
 */
function file_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

898
/**
899
 * Creates a full file path from a directory and filename.
900
901
902
 *
 * If a file with the specified name already exists, an alternative will be
 * used.
903
 *
904
905
906
 * @param $basename
 *   String filename
 * @param $directory
907
 *   String containing the directory or parent URI.
908
 *
909
 * @return
910
911
 *   File path consisting of $directory and a unique filename based off
 *   of $basename.
912
 */
Dries's avatar
   
Dries committed
913
function file_create_filename($basename, $directory) {
914
915
916
  // 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);
917
918
919
920
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
  }
921

922
923
924
925
926
927
928
929
930
  // 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
931

932
  if (file_exists($destination)) {
Dries's avatar
   
Dries committed
933
    // Destination file already exists, generate an alternative.
934
935
    $pos = strrpos($basename, '.');
    if ($pos !== FALSE) {
Dries's avatar
   
Dries committed
936
937
938
939
940
      $name = substr($basename, 0, $pos);
      $ext = substr($basename, $pos);
    }
    else {
      $name = $basename;
941
      $ext = '';
Dries's avatar
   
Dries committed
942
943
944
945
    }

    $counter = 0;
    do {
946
      $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
947
    } while (file_exists($destination));
Dries's avatar
   
Dries committed
948
949
  }

950
  return $destination;
Dries's avatar
   
Dries committed
951
952
}

953
/**
954
 * Deletes a file and its database record.
955
 *
956
957
958
 * 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.
959
 *
960
961
 * @param $fid
 *   The file id.
962
 *
963
 * @see file_unmanaged_delete()
964
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
965
 */
966
967
968
function file_delete($fid) {
  return file_delete_multiple(array($fid));
}
969

970
971
972
973
974
975
976
977
978
979
980
/**
 * 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()
981
 * @see \Drupal\file\FileUsage\FileUsageBase::delete()
982
983
984
 */
function file_delete_multiple(array $fids) {
  entity_delete_multiple('file', $fids);
985
986
987
}

/**
988
 * Deletes a file without database changes or hook invocations.
989
990
991
 *
 * This function should be used when the file to be deleted does not have an
 * entry recorded in the files table.
992
 *
993
 * @param $path
994
 *   A string containing a file path or (streamwrapper) URI.
995
 *
996
997
998
 * @return
 *   TRUE for success or path does not exist, or FALSE in the event of an
 *   error.
999
 *
1000
 * @see file_delete()