file.inc 86.2 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 10
 * Manually include stream wrapper code.
 *
11 12 13 14
 * Stream wrapper code is included here because there are cases where
 * File API is needed before a bootstrap, or in an alternate order (e.g.
 * maintenance theme).
 */
15
require_once DRUPAL_ROOT . '/core/includes/stream_wrappers.inc';
16

Kjartan's avatar
Kjartan committed
17
/**
Kjartan's avatar
Kjartan committed
18
 * @defgroup file File interface
Kjartan's avatar
Kjartan committed
19
 * @{
Dries's avatar
 
Dries committed
20
 * Common file handling functions.
21 22
 *
 * Fields on the file object:
23 24 25
 * - 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
26 27
 *   the basename of the filepath if the file is renamed to avoid overwriting
 *   an existing file.
28 29 30 31
 * - 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
32
 *   bits are reserved for Drupal core. The least significant bit indicates
33 34
 *   temporary (0) or permanent (1). Temporary files older than
 *   DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
35
 * - timestamp: UNIX timestamp for the date the file was added to the database.
Dries's avatar
 
Dries committed
36 37
 */

38
/**
39
 * Flag used by file_prepare_directory() -- create directory if not present.
40
 */
41
const FILE_CREATE_DIRECTORY = 1;
42 43

/**
44
 * Flag used by file_prepare_directory() -- file permissions may be changed.
45
 */
46
const FILE_MODIFY_PERMISSIONS = 2;
47 48

/**
49
 * Flag for dealing with existing files: Appends number until name is unique.
50
 */
51
const FILE_EXISTS_RENAME = 0;
52 53 54 55

/**
 * Flag for dealing with existing files: Replace the existing file.
 */
56
const FILE_EXISTS_REPLACE = 1;
57 58 59 60

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

63
/**
64 65 66 67 68
 * 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.
69
 */
70
const FILE_STATUS_PERMANENT = 1;
71

72
/**
73
 * Provides Drupal stream wrapper registry.
74 75 76 77 78 79 80 81 82 83 84 85 86 87
 *
 * 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".
 *
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
 * 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
 *   $local_stream_wrappers = file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL);
 * @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
 *   $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STEAM_WRAPPERS_LOCAL));
 * @endcode
 *
105
 * @param $filter
106 107 108 109 110 111
 *   (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.
112
 *
113
 * @return
114 115 116 117 118
 *   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.
119
 *
120 121 122
 * @see hook_stream_wrappers()
 * @see hook_stream_wrappers_alter()
 */
123 124
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
  $wrappers_storage = &drupal_static(__FUNCTION__);
125

126
  if (!isset($wrappers_storage)) {
127
    $wrappers = module_invoke_all('stream_wrappers');
128 129 130 131
    foreach ($wrappers as $scheme => $info) {
      // Add defaults.
      $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
    }
132 133 134 135 136 137 138 139 140 141 142 143 144
    drupal_alter('stream_wrappers', $wrappers);
    $existing = stream_get_wrappers();
    foreach ($wrappers as $scheme => $info) {
      // We only register classes that implement our interface.
      if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
        // 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;
        }
145 146
        if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
          stream_wrapper_register($scheme, $info['class']);
147 148
        }
        else {
149
          stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
150
        }
151
      }
152 153 154 155 156
      // 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];
      }
157 158
    }
  }
159 160 161 162 163

  if (!isset($wrappers_storage[$filter])) {
    $wrappers_storage[$filter] = array();
    foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
      // Bit-wise filter.
164
      if (($info['type'] & $filter) == $filter) {
165 166 167 168 169 170
        $wrappers_storage[$filter][$scheme] = $info;
      }
    }
  }

  return $wrappers_storage[$filter];
171 172 173 174 175 176 177
}

/**
 * Returns the stream wrapper class name for a given scheme.
 *
 * @param $scheme
 *   Stream scheme.
178
 *
179 180 181 182 183 184 185 186 187 188 189 190 191
 * @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".
192
 *
193 194 195
 * @return
 *   A string containing the name of the scheme, or FALSE if none. For example,
 *   the URI "public://example.txt" would return "public".
196 197
 *
 * @see file_uri_target()
198 199
 */
function file_uri_scheme($uri) {
200 201
  $position = strpos($uri, '://');
  return $position ? substr($uri, 0, $position) : FALSE;
202 203 204
}

/**
205
 * Checks that the scheme of a stream URI is valid.
206 207 208 209 210 211 212
 *
 * 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".
213
 *
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
 * @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) {
  // Does the scheme have a registered handler that is callable?
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

229

230
/**
231
 * Returns the part of a URI after the schema.
232 233 234
 *
 * @param $uri
 *   A stream, referenced as "scheme://target".
235
 *
236 237 238 239
 * @return
 *   A string containing the target (path), or FALSE if none.
 *   For example, the URI "public://sample/test.txt" would return
 *   "sample/test.txt".
240 241
 *
 * @see file_uri_scheme()
242 243
 */
function file_uri_target($uri) {
244 245 246 247
  $data = explode('://', $uri, 2);

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

250
/**
251
 * Gets the default file stream implementation.
252 253 254 255 256 257 258 259
 *
 * @return
 *   'public', 'private' or any other file scheme defined as the default.
 */
function file_default_scheme() {
  return variable_get('file_default_scheme', 'public');
}

260 261 262 263 264 265 266 267 268 269 270
/**
 * 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.
271
 *
272 273
 * @return
 *   The normalized URI.
274 275 276 277 278 279 280
 */
function file_stream_wrapper_uri_normalize($uri) {
  $scheme = file_uri_scheme($uri);

  if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
    $target = file_uri_target($uri);

281 282 283
    if ($target !== FALSE) {
      $uri = $scheme . '://' . $target;
    }
284
  }
285 286 287 288
  else {
    // The default scheme is file://
    $url = 'file://' . $uri;
  }
289 290 291 292
  return $uri;
}

/**
293
 * Returns a reference to the stream wrapper class responsible for a given URI.
294 295 296 297 298 299
 *
 * 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".
300
 *
301 302 303 304 305 306 307 308 309 310
 * @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
 *   (DrupalPrivateStreamWrapper).
 */
function file_stream_wrapper_get_instance_by_uri($uri) {
  $scheme = file_uri_scheme($uri);
  $class = file_stream_wrapper_get_class($scheme);
  if (class_exists($class)) {
311
    $instance = new $class();
312 313 314 315 316 317 318 319 320
    $instance->setUri($uri);
    return $instance;
  }
  else {
    return FALSE;
  }
}

/**
321
 * Returns a reference to the stream wrapper class responsible for a scheme.
322 323 324 325 326 327 328 329 330 331 332
 *
 * 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.
333
 *
334 335 336 337 338 339 340 341 342
 * @return
 *   Returns a new stream wrapper object appropriate for the given $scheme.
 *   For example, for the public scheme a stream wrapper object
 *   (DrupalPublicStreamWrapper).
 *   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)) {
343
    $instance = new $class();
344 345 346 347 348 349 350 351
    $instance->setUri($scheme . '://');
    return $instance;
  }
  else {
    return FALSE;
  }
}

Dries's avatar
 
Dries committed
352
/**
353
 * Creates a web-accessible URL for a stream to an external or local file.
Dries's avatar
 
Dries committed
354
 *
355
 * Compatibility: normal paths and stream wrappers.
Dries's avatar
 
Dries committed
356
 *
357
 * There are two kinds of local files:
358 359 360
 * - "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).
361 362 363
 * - "shipped files", i.e. those outside of the files directory, which ship as
 *   part of Drupal core or contributed modules or themes.
 *
364
 * @param $uri
365 366
 *   The URI to a file for which we need an external URL, or the path to a
 *   shipped file.
367
 *
368
 * @return
369
 *   A string containing a URL that may be used to access the file.
370 371 372
 *   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.
373 374
 *
 * @see http://drupal.org/node/515192
Dries's avatar
 
Dries committed
375
 */
376
function file_create_url($uri) {
377 378 379
  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
  // file server.
  drupal_alter('file_url', $uri);
380

381 382 383
  $scheme = file_uri_scheme($uri);

  if (!$scheme) {
384 385 386 387 388 389 390 391 392 393 394 395
    // 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.
396 397
      // Therefore, return the urlencoded URI with the base URL prepended.
      return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
398
    }
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
  }
  elseif ($scheme == 'http' || $scheme == 'https') {
    // Check for http so that we don't have to implement getExternalUrl() for
    // the http wrapper.
    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
414 415 416
}

/**
417
 * Checks that the directory exists and is writable.
418 419 420 421
 *
 * Directories need to have execute permissions to be considered a directory by
 * FTP servers, etc.
 *
422
 * @param $directory
423 424 425
 *   A string reference containing the name of a directory path or URI. A
 *   trailing slash will be trimmed from a path.
 * @param $options
426 427 428
 *   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).
429
 *
430
 * @return
431 432
 *   TRUE if the directory exists (or was created) and is writable. FALSE
 *   otherwise.
Dries's avatar
 
Dries committed
433
 */
434 435 436 437 438
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
  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
439 440 441

  // Check if directory exists.
  if (!is_dir($directory)) {
442 443
    // Let mkdir() recursively create directories and use the default directory
    // permissions.
444 445
    if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
      return drupal_chmod($directory);
Dries's avatar
 
Dries committed
446
    }
447
    return FALSE;
Dries's avatar
 
Dries committed
448
  }
449 450 451 452
  // The directory exists, so check to see if it is writable.
  $writable = is_writable($directory);
  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
    return drupal_chmod($directory);
Dries's avatar
 
Dries committed
453 454
  }

455
  return $writable;
Dries's avatar
 
Dries committed
456 457 458
}

/**
459
 * Creates a .htaccess file in each Drupal files directory if it is missing.
Dries's avatar
 
Dries committed
460
 */
461
function file_ensure_htaccess() {
462
  file_save_htaccess('public://', FALSE);
463
  if (variable_get('file_private_path', FALSE)) {
464
    file_save_htaccess('private://', TRUE);
465
  }
466
  file_save_htaccess('temporary://', TRUE);
Dries's avatar
 
Dries committed
467 468 469
}

/**
470
 * Creates a .htaccess file in the given directory.
Dries's avatar
 
Dries committed
471
 *
472
 * @param $directory
473 474 475 476
 *   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
477
 */
478
function file_save_htaccess($directory, $private = TRUE) {
479 480
  if (file_uri_scheme($directory)) {
    $directory = file_stream_wrapper_uri_normalize($directory);
481 482
  }
  else {
483
    $directory = rtrim($directory, '/\\');
484
  }
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
  $htaccess_path =  $directory . '/.htaccess';

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

  if ($private) {
    // Private .htaccess file.
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks";
  }
  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 {
    $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
507
    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
508 509 510 511
  }
}

/**
512
 * Loads file objects from the database.
513
 *
514 515 516
 * @param $fids
 *   An array of file IDs.
 * @param $conditions
517 518 519 520 521
 *   (deprecated) An associative array of conditions on the {file_managed}
 *   table, where the keys are the database fields and the values are the
 *   values those fields must have. Instead, it is preferable to use
 *   EntityFieldQuery to retrieve a list of entity IDs loadable by
 *   this function.
522
 *
523
 * @return
524
 *   An array of file objects, indexed by fid.
525
 *
526 527
 * @todo Remove $conditions in Drupal 8.
 *
528
 * @see hook_file_load()
529
 * @see file_load()
530 531
 * @see entity_load()
 * @see EntityFieldQuery
532
 */
533
function file_load_multiple($fids = array(), $conditions = array()) {
534
  return entity_load('file', $fids, $conditions);
535
}
536

537
/**
538
 * Loads a single file object from the database.
539 540
 *
 * @param $fid
541
 *   A file ID.
542
 *
543 544 545 546 547 548 549 550 551
 * @return
 *   A file object.
 *
 * @see hook_file_load()
 * @see file_load_multiple()
 */
function file_load($fid) {
  $files = file_load_multiple(array($fid), array());
  return reset($files);
552 553 554
}

/**
555
 * Saves a file object to the database.
556
 *
557
 * If the $file->fid is not set a new record will be added.
558 559 560
 *
 * @param $file
 *   A file object returned by file_load().
561
 *
562 563
 * @return
 *   The updated file object.
564
 *
565 566 567
 * @see hook_file_insert()
 * @see hook_file_update()
 */
568
function file_save(stdClass $file) {
569
  $file->timestamp = REQUEST_TIME;
570
  $file->filesize = filesize($file->uri);
571

572 573 574 575 576
  // Load the stored entity, if any.
  if (!empty($file->fid) && !isset($file->original)) {
    $file->original = entity_load_unchanged('file', $file->fid);
  }

577 578 579
  module_invoke_all('file_presave', $file);
  module_invoke_all('entity_presave', $file, 'file');

580
  if (empty($file->fid)) {
581
    drupal_write_record('file_managed', $file);
582 583
    // Inform modules about the newly added file.
    module_invoke_all('file_insert', $file);
584
    module_invoke_all('entity_insert', $file, 'file');
585 586
  }
  else {
587
    drupal_write_record('file_managed', $file, 'fid');
588 589
    // Inform modules that the file has been updated.
    module_invoke_all('file_update', $file);
590
    module_invoke_all('entity_update', $file, 'file');
591 592
  }

593
  unset($file->original);
594 595 596 597
  return $file;
}

/**
598 599 600 601 602 603 604
 * Determines where a file is used.
 *
 * @param $file
 *   A file object.
 *
 * @return
 *   A nested array with usage data. The first level is keyed by module name,
605 606
 *   the second by object type and the third by the object id. The value
 *   of the third level contains the usage count.
607 608 609 610 611 612 613 614 615 616 617 618
 *
 * @see file_usage_add()
 * @see file_usage_delete()
 */
function file_usage_list(stdClass $file) {
  $result = db_select('file_usage', 'f')
    ->fields('f', array('module', 'type', 'id', 'count'))
    ->condition('fid', $file->fid)
    ->condition('count', 0, '>')
    ->execute();
  $references = array();
  foreach ($result as $usage) {
619
    $references[$usage->module][$usage->type][$usage->id] = $usage->count;
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
  }
  return $references;
}

/**
 * Records that a module is using a file.
 *
 * This usage information will be queried during file_delete() to ensure that
 * a file is not in use before it is physically removed from disk.
 *
 * Examples:
 * - A module that associates files with nodes, so $type would be
 *   'node' and $id would be the node's nid. Files for all revisions are stored
 *   within a single nid.
 * - The User module associates an image with a user, so $type would be 'user'
 *   and the $id would be the user's uid.
 *
 * @param $file
 *   A file object.
 * @param $module
 *   The name of the module using the file.
 * @param $type
642
 *   The type of the object that contains the referenced file.
643
 * @param $id
644
 *   The unique, numeric ID of the object containing the referenced file.
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
 * @param $count
 *   (optional) The number of references to add to the object. Defaults to 1.
 *
 * @see file_usage_list()
 * @see file_usage_delete()
 */
function file_usage_add(stdClass $file, $module, $type, $id, $count = 1) {
  db_merge('file_usage')
    ->key(array(
      'fid' => $file->fid,
      'module' => $module,
      'type' => $type,
      'id' => $id,
    ))
    ->fields(array('count' => $count))
    ->expression('count', 'count + :count', array(':count' => $count))
    ->execute();
}

/**
 * Removes a record to indicate that a module is no longer using a file.
 *
 * The file_delete() function is typically called after removing a file usage
 * to remove the record from the file_managed table and delete the file itself.
 *
 * @param $file
 *   A file object.
 * @param $module
 *   The name of the module using the file.
 * @param $type
 *   (optional) The type of the object that contains the referenced file. May
 *   be omitted if all module references to a file are being deleted.
 * @param $id
 *   (optional) The unique, numeric ID of the object containing the referenced
 *   file. May be omitted if all module references to a file are being deleted.
 * @param $count
 *   (optional) The number of references to delete from the object. Defaults to
 *   1. 0 may be specified to delete all references to the file within a
 *   specific object.
 *
 * @see file_usage_add()
 * @see file_usage_list()
 * @see file_delete()
 */
function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $count = 1) {
  // Delete rows that have a exact or less value to prevent empty rows.
  $query = db_delete('file_usage')
    ->condition('module', $module)
    ->condition('fid', $file->fid);
  if ($type && $id) {
    $query
      ->condition('type', $type)
      ->condition('id', $id);
  }
  if ($count) {
    $query->condition('count', $count, '<=');
  }
  $result = $query->execute();

  // If the row has more than the specified count decrement it by that number.
705
  if (!$result && $count > 0) {
706 707 708 709 710 711 712 713
    $query = db_update('file_usage')
      ->condition('module', $module)
      ->condition('fid', $file->fid);
    if ($type && $id) {
      $query
        ->condition('type', $type)
        ->condition('id', $id);
    }
714
    $query->expression('count', 'count - :count', array(':count' => $count));
715 716 717 718 719 720
    $query->execute();
  }
}

/**
 * Copies a file to a new location and adds a file record to the database.
721 722 723 724 725 726 727 728 729 730
 *
 * This function should be used when manipulating files that have records
 * stored in the database. 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.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database. If the source file is a
731 732
 *   temporary file, the resulting file will also be a temporary file. See
 *   file_save_upload() for details on temporary files.
733 734 735 736
 *
 * @param $source
 *   A file object.
 * @param $destination
737
 *   A string containing the destination that $source should be copied to.
738
 *   This must be a stream wrapper URI.
739 740
 * @param $replace
 *   Replace behavior when the destination file already exists:
741 742 743
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated. If
 *       no database entry is found then a new one will be created.
744
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
745
 *       unique.
746
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
747
 *
748 749
 * @return
 *   File object if the copy is successful, or FALSE in the event of an error.
750
 *
751 752 753
 * @see file_unmanaged_copy()
 * @see hook_file_copy()
 */
754
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
755
  if (!file_valid_uri($destination)) {
756
    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
757
      watchdog('file', 'File %file (%realpath) could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
758 759
    }
    else {
760
      watchdog('file', 'File %file could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
761
    }
762
    drupal_set_message(t('The specified file %file could not be copied because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
763 764 765
    return FALSE;
  }

766
  if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
767
    $file = clone $source;
768
    $file->fid = NULL;
769
    $file->uri = $uri;
770
    $file->filename = drupal_basename($uri);
771 772
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
773
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
774 775 776 777 778
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $file->fid = $existing->fid;
        $file->filename = $existing->filename;
      }
779
    }
780 781
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
782
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
783
      $file->filename = drupal_basename($destination);
784 785 786 787 788 789 790 791
    }

    $file = file_save($file);

    // Inform modules that the file has been copied.
    module_invoke_all('file_copy', $file, $source);

    return $file;
792 793 794 795
  }
  return FALSE;
}

796
/**
797
 * Determines whether the URI has a valid scheme for file API operations.
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
 *
 * 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);
  if (empty($uri_scheme) || !file_stream_wrapper_valid_scheme($uri_scheme)) {
    return FALSE;
  }
  return TRUE;
}

818
/**
819
 * Copies a file to a new location without invoking the file API.
Dries's avatar
 
Dries committed
820
 *
821 822 823 824 825 826 827
 * 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.
 * - Checks that $source is not equal to $destination; if they are an error
 *   is reported.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
828 829 830 831
 * - 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
832 833
 *
 * @param $source
834
 *   A string specifying the filepath or URI of the source file.
835
 * @param $destination
836 837 838 839
 *   A URI containing the destination that $source should be copied to. The
 *   URI may be a bare filepath (without a scheme) and in that case the default
 *   scheme (file://) will be used. If this value is omitted, Drupal's default
 *   files scheme will be used, usually "public://".
840 841 842 843
 * @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
844
 *       unique.
845
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
846
 *
847 848
 * @return
 *   The path to the new file, or FALSE in the event of an error.
849
 *
850
 * @see file_copy()
Dries's avatar
 
Dries committed
851
 */
852
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
853 854 855
  $original_source = $source;
  $original_destination = $destination;

856
  // Assert that the source file actually exists.
857
  if (!file_exists($source)) {
858
    // @todo Replace drupal_set_message() calls with exceptions instead.
859
    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');
860 861 862 863 864 865
    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));
    }
866 867
    return FALSE;
  }
Dries's avatar
 
Dries committed
868

869 870
  // Build a destination URI if necessary.
  if (!isset($destination)) {
871
    $destination = file_build_uri(drupal_basename($source));
872
  }
Dries's avatar
 
Dries committed
873 874


875 876 877
  // Prepare the destination directory.
  if (file_prepare_directory($destination)) {
    // The destination is already a directory, so append the source basename.
878
    $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
879 880 881 882 883 884
  }
  else {
    // Perhaps $destination is a dir/file?
    $dirname = drupal_dirname($destination);
    if (!file_prepare_directory($dirname)) {
      // The destination is not valid.
885 886
      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');
887 888 889
      return FALSE;
    }
  }
890

891 892
  // Determine whether we can perform this operation based on overwrite rules.
  $destination = file_destination($destination, $replace);
893
  if ($destination === FALSE) {
894
    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');
895
    watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%destination' => $destination));
896
    return FALSE;
Dries's avatar
 
Dries committed
897
  }
898 899

  // Assert that the source and destination filenames are not the same.
900 901 902
  $real_source = drupal_realpath($source);
  $real_destination = drupal_realpath($destination);
  if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
903
    drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
904
    watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
905
    return FALSE;
Dries's avatar
 
Dries committed
906
  }
907 908 909
  // Make sure the .htaccess files are present.
  file_ensure_htaccess();
  // Perform the copy operation.
910
  if (!@copy($source, $destination)) {
911 912 913 914 915 916
    // 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
917
  }
Dries's avatar
 
Dries committed
918

919 920
  // Set the permissions on the new file.
  drupal_chmod($destination);
921 922

  return $destination;
Dries's avatar
 
Dries committed
923 924
}

925
/**
926
 * Constructs a URI to Drupal's default files location given a relative path.
927 928
 */
function file_build_uri($path) {
929
  $uri = file_default_scheme() . '://' . $path;
930 931 932
  return file_stream_wrapper_uri_normalize($uri);
}

933
/**
934
 * Determines the destination path for a file.
935
 *
936
 * @param $destination
937
 *   A string specifying the desired final URI or filepath.
938 939
 * @param $replace
 *   Replace behavior when the destination file already exists.
940
 *   - FILE_EXISTS_REPLACE - Replace the existing file.
941
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
942
 *       unique.
943
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
944
 *
945
 * @return
946 947
 *   The destination filepath, or FALSE if the file already exists
 *   and FILE_EXISTS_ERROR is specified.
948 949 950 951
 */
function file_destination($destination, $replace) {
  if (file_exists($destination)) {
    switch ($replace) {
952 953 954 955
      case FILE_EXISTS_REPLACE:
        // Do nothing here, we want to overwrite the existing file.
        break;

956
      case FILE_EXISTS_RENAME:
957
        $basename = drupal_basename($destination);
958
        $directory = drupal_dirname($destination);
959 960 961 962
        $destination = file_create_filename($basename, $directory);
        break;

      case FILE_EXISTS_ERROR:
963
        // Error reporting handled by calling function.
964 965 966 967 968 969
        return FALSE;
    }
  }
  return $destination;
}

Dries's avatar
 
Dries committed
970
/**
971
 * Moves a file to a new location and update the file's database entry.
972 973 974 975 976 977 978 979 980 981 982 983
 *
 * Moving a file is performed by copying the file to the new location and then
 * deleting the original.
 * - Checks if $source and $destination are valid and readable/writable.
 * - Performs a file move if $source is not equal to $destination.
 * - If file already exists in $destination either the call will error out,
 *   replace the file or rename the file based on the $replace parameter.
 * - Adds the new file to the files database.
 *
 * @param $source
 *   A file object.
 * @param $destination
984
 *   A string containing the destination that $source should be moved to.
985
 *   This must be a stream wrapper URI.
986 987
 * @param $replace
 *   Replace behavior when the destination file already exists:
988 989 990 991 992
 *   - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
 *       the destination name exists then its database entry will be updated and
 *       file_delete() called on the source file after hook_file_move is called.
 *       If no database entry is found then the source files record will be
 *       updated.
993
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
994
 *       unique.
995
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
996
 *
997 998
 * @return
 *   Resulting file object for success, or FALSE in the event of an error.
999
 *
1000 1001 1002
 * @see file_unmanaged_move()
 * @see hook_file_move()
 */
1003
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
1004
  if (!file_valid_uri($destination)) {
1005
    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
1006
      watchdog('file', 'File %file (%realpath) could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
1007 1008
    }
    else {
1009
      watchdog('file', 'File %file could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
1010
    }
1011
    drupal_set_message(t('The specified file %file could not be moved because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
1012 1013 1014
    return FALSE;
  }

1015
  if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
1016 1017
    $delete_source = FALSE;

1018
    $file = clone $source;
1019
    $file->uri = $uri;
1020 1021
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
1022
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
1023 1024 1025 1026 1027 1028 1029 1030
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $delete_source = TRUE;
        $file->fid = $existing->fid;
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
1031
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
1032
      $file->filename = drupal_basename($destination);
1033
    }
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045

    $file = file_save($file);

    // Inform modules that the file has been moved.
    module_invoke_all('file_move', $file, $source);

    if ($delete_source) {
      // Try a soft delete to remove original if it's not in use elsewhere.
      file_delete($source);
    }

    return $file;
1046 1047 1048 1049 1050
  }
  return FALSE;
}

/**
1051
 * Moves a file to a new location without database changes or hook invocation.
Dries's avatar
 
Dries committed