Simpletest Coverage - includes/file.inc

1 <?php
2 // $Id: file.inc,v 1.180 2009/08/11 04:50:36 webchick Exp $
3
4 /**
5 * @file
6 * API for handling file uploads and server file management.
7 */
8
9 /**
10 * Stream wrapper code is included here because there are cases where
11 * File API is needed before a bootstrap, or in an alternate order (e.g.
12 * maintenance theme).
13 */
14 require_once DRUPAL_ROOT . '/includes/stream_wrappers.inc';
15
16 /**
17 * @defgroup file File interface
18 * @{
19 * Common file handling functions.
20 *
21 * Fields on the file object:
22 * - fid - File ID
23 * - uid - The {users}.uid of the user who is associated with the file.
24 * - filename - Name of the file with no path components. This may differ from
25 * the basename of the filepath if the file is renamed to avoid overwriting
26 * an existing file.
27 * - filepath - Path of the file relative to Drupal root.
28 * - filemime - The file's MIME type.
29 * - filesize - The size of the file in bytes.
30 * - status - A bitmapped field indicating the status of the file. The first 8
31 * bits are reserved for Drupal core. The least sigifigant bit indicates
32 * temporary (0) or permanent (1). Temporary files older than
33 * DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
34 * - timestamp - UNIX timestamp for the date the file was added to the database.
35 */
36
37 /**
38 * Flag to indicate that the 'public' file download method is enabled.
39 *
40 * When using this method, files are available from a regular HTTP request,
41 * which provides no additional access restrictions.
42 */
43 define('FILE_DOWNLOADS_PUBLIC', 1);
44
45 /**
46 * Flag to indicate that the 'private' file download method is enabled.
47 *
48 * When using this method, all file requests are served by Drupal, during which
49 * access-control checking can be performed.
50 */
51 define('FILE_DOWNLOADS_PRIVATE', 2);
52
53 /**
54 * Flag used by file_check_directory() -- create directory if not present.
55 */
56 define('FILE_CREATE_DIRECTORY', 1);
57
58 /**
59 * Flag used by file_check_directory() -- file permissions may be changed.
60 */
61 define('FILE_MODIFY_PERMISSIONS', 2);
62
63 /**
64 * Flag for dealing with existing files: Appends number until name is unique.
65 */
66 define('FILE_EXISTS_RENAME', 0);
67
68 /**
69 * Flag for dealing with existing files: Replace the existing file.
70 */
71 define('FILE_EXISTS_REPLACE', 1);
72
73 /**
74 * Flag for dealing with existing files: Do nothing and return FALSE.
75 */
76 define('FILE_EXISTS_ERROR', 2);
77
78 /**
79 * File status -- This bit in the status indicates that the file is permanent
80 * and should not be deleted during file garbage collection process. Temporary
81 * files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron
82 * runs.
83 */
84 define('FILE_STATUS_PERMANENT', 1);
85
86 /**
87 * Methods to manage a registry of stream wrappers.
88 */
89
90 /**
91 * Drupal stream wrapper registry.
92 *
93 * A stream wrapper is an abstraction of a file system that allows Drupal to
94 * use the same set of methods to access both local files and remote resources.
95 *
96 * Provide a facility for managing and querying user-defined stream wrappers
97 * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
98 * registered to handle a stream, which we need to be able to find the handler
99 * for class instantiation.
100 *
101 * If a module registers a scheme that is already registered with PHP, the
102 * existing scheme will be unregistered and replaced with the specified class.
103 *
104 * A stream is referenced as "scheme://target".
105 *
106 * @return
107 * Returns the entire Drupal stream wrapper registry.
108 * @see hook_stream_wrappers()
109 * @see hook_stream_wrappers_alter()
110 */
111 function file_get_stream_wrappers() {
112 $wrappers = &drupal_static(__FUNCTION__);
113
114 if (!isset($wrappers)) {
115 $wrappers = module_invoke_all('stream_wrappers');
116 drupal_alter('stream_wrappers', $wrappers);
117 $existing = stream_get_wrappers();
118 foreach ($wrappers as $scheme => $info) {
119 // We only register classes that implement our interface.
120 if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
121 // Record whether we are overriding an existing scheme.
122 if (in_array($scheme, $existing, TRUE)) {
123 $wrappers[$scheme]['override'] = TRUE;
124 stream_wrapper_unregister($scheme);
125 }
126 else {
127 $wrappers[$scheme]['override'] = FALSE;
128 }
129 stream_wrapper_register($scheme, $info['class']);
130 }
131 }
132 }
133 return $wrappers;
134 }
135
136 /**
137 * Returns the stream wrapper class name for a given scheme.
138 *
139 * @param $scheme
140 * Stream scheme.
141 * @return
142 * Return string if a scheme has a registered handler, or FALSE.
143 */
144 function file_stream_wrapper_get_class($scheme) {
145 $wrappers = file_get_stream_wrappers();
146 return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
147 }
148
149 /**
150 * Returns the scheme of a URI (e.g. a stream).
151 *
152 * @param $uri
153 * A stream, referenced as "scheme://target".
154 * @return
155 * A string containing the name of the scheme, or FALSE if none. For example,
156 * the URI "public://example.txt" would return "public".
157 */
158 function file_uri_scheme($uri) {
159 $data = explode('://', $uri, 2);
160
161 return count($data) == 2 ? $data[0] : FALSE;
162 }
163
164 /**
165 * Check that the scheme of a stream URI is valid.
166 *
167 * Confirms that there is a registered stream handler for the provided scheme
168 * and that it is callable. This is useful if you want to confirm a valid
169 * scheme without creating a new instance of the registered handler.
170 *
171 * @param $scheme
172 * A URI scheme, a stream is referenced as "scheme://target".
173 * @return
174 * Returns TRUE if the string is the name of a validated stream,
175 * or FALSE if the scheme does not have a registered handler.
176 */
177 function file_stream_wrapper_valid_scheme($scheme) {
178 // Does the scheme have a registered handler that is callable?
179 $class = file_stream_wrapper_get_class($scheme);
180 if (class_exists($class)) {
181 return TRUE;
182 }
183 else {
184 return FALSE;
185 }
186 }
187
188 /**
189 * Returns the target of a URI (e.g. a stream).
190 *
191 * @param $uri
192 * A stream, referenced as "scheme://target".
193 * @return
194 * A string containing the target (path), or FALSE if none.
195 * For example, the URI "public://sample/test.txt" would return
196 * "sample/test.txt".
197 */
198 function file_uri_target($uri) {
199 $data = explode('://', $uri, 2);
200
201 if (count($data) != 2) {
202 return FALSE;
203 }
204
205 // Remove erroneous beginning forward slash.
206 $data[1] = ltrim($data[1], '\/');
207
208 return $data[1];
209 }
210
211 /**
212 * Normalizes a URI by making it syntactically correct.
213 *
214 * A stream is referenced as "scheme://target".
215 *
216 * The following actions are taken:
217 * - Remove all occurrences of the wrapper's directory path
218 * - Remove trailing slashes from target
219 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
220 *
221 * @param $uri
222 * String reference containing the URI to normalize.
223 */
224 function file_stream_wrapper_uri_normalize($uri) {
225 $scheme = file_uri_scheme($uri);
226
227 if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
228 $target = file_uri_target($uri);
229
230 // Remove all occurrences of the wrapper's directory path.
231 $directory_path = file_stream_wrapper_get_instance_by_scheme($scheme)->getDirectoryPath();
232 $target = str_replace($directory_path, '', $target);
233
234 // Trim trailing slashes from target.
235 $target = rtrim($target, '/');
236
237 // Trim erroneous leading slashes from target.
238 $uri = $scheme . '://' . ltrim($target, '/');
239 }
240 return $uri;
241 }
242
243 /**
244 * Returns a reference to the stream wrapper class responsible for a given URI (stream).
245 *
246 * The scheme determines the stream wrapper class that should be
247 * used by consulting the stream wrapper registry.
248 *
249 * @param $uri
250 * A stream, referenced as "scheme://target".
251 * @return
252 * Returns a new stream wrapper object appropriate for the given URI or FALSE
253 * if no registered handler could be found. For example, a URI of
254 * "private://example.txt" would return a new private stream wrapper object
255 * (DrupalPrivateStreamWrapper).
256 */
257 function file_stream_wrapper_get_instance_by_uri($uri) {
258 $scheme = file_uri_scheme($uri);
259 $class = file_stream_wrapper_get_class($scheme);
260 if (class_exists($class)) {
261 $instance = new $class;
262 $instance->setUri($uri);
263 return $instance;
264 }
265 else {
266 return FALSE;
267 }
268 }
269
270 /**
271 * Returns a reference to the stream wrapper class responsible for a given scheme.
272 *
273 * This helper method returns a stream instance using a scheme. That is, the
274 * passed string does not contain a "://". For example, "public" is a scheme
275 * but "public://" is a URI (stream). This is because the later contains both
276 * a scheme and target despite target being empty.
277 *
278 * Note: the instance URI will be initialized to "scheme://" so that you can
279 * make the customary method calls as if you had retrieved an instance by URI.
280 *
281 * @param $scheme
282 * If the stream was "public://target", "public" would be the scheme.
283 * @return
284 * Returns a new stream wrapper object appropriate for the given $scheme.
285 * For example, for the public scheme a stream wrapper object
286 * (DrupalPublicStreamWrapper).
287 * FALSE is returned if no registered handler could be found.
288 */
289 function file_stream_wrapper_get_instance_by_scheme($scheme) {
290 $class = file_stream_wrapper_get_class($scheme);
291 if (class_exists($class)) {
292 $instance = new $class;
293 $instance->setUri($scheme . '://');
294 return $instance;
295 }
296 else {
297 return FALSE;
298 }
299 }
300
301 /**
302 * Create the download path to a file.
303 *
304 * @param $path A string containing the path of the file to generate URL for.
305 * @return A string containing a URL that can be used to download the file.
306 */
307 function file_create_url($path) {
308 // Strip file_directory_path from $path. We only include relative paths in
309 // URLs.
310 $path = file_directory_strip($path);
311 switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
312 case FILE_DOWNLOADS_PUBLIC:
313 return $GLOBALS['base_url'] . '/' . file_directory_path() . '/' . str_replace('\\', '/', $path);
314 case FILE_DOWNLOADS_PRIVATE:
315 return url('system/files/' . $path, array('absolute' => TRUE));
316 }
317 }
318
319 /**
320 * Make sure the destination is a complete path and resides in the file system
321 * directory, if it is not prepend the file system directory.
322 *
323 * @param $destination
324 * A string containing the path to verify. If this value is omitted, Drupal's
325 * 'files' directory will be used.
326 * @return
327 * A string containing the path to file, with file system directory appended
328 * if necessary, or FALSE if the path is invalid (i.e. outside the configured
329 * 'files' or temp directories).
330 */
331 function file_create_path($destination = NULL) {
332 $file_path = file_directory_path();
333 if (is_null($destination)) {
334 return $file_path;
335 }
336 // file_check_location() checks whether the destination is inside the Drupal
337 // files directory.
338 if (file_check_location($destination, $file_path)) {
339 return $destination;
340 }
341 // Check if the destination is instead inside the Drupal temporary files
342 // directory.
343 elseif (file_check_location($destination, file_directory_temp())) {
344 return $destination;
345 }
346 // Not found, try again with prefixed directory path.
347 elseif (file_check_location($file_path . '/' . $destination, $file_path)) {
348 return $file_path . '/' . $destination;
349 }
350 // File not found.
351 return FALSE;
352 }
353
354 /**
355 * Check that the directory exists and is writable.
356 *
357 * Directories need to have execute permissions to be considered a directory by
358 * FTP servers, etc.
359 *
360 * @param $directory
361 * A string containing the name of a directory path.
362 * @param $mode
363 * A bitmask to indicate if the directory should be created if it does
364 * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
365 * (FILE_MODIFY_PERMISSIONS).
366 * @param $form_item
367 * An optional string containing the name of a form item that any errors will
368 * be attached to. This is useful for settings forms that require the user to
369 * specify a writable directory. If it can't be made to work, a form error
370 * will be set preventing them from saving the settings.
371 * @return
372 * FALSE when directory not found, or TRUE when directory exists.
373 */
374 function file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
375 $directory = rtrim($directory, '/\\');
376
377 // Check if directory exists.
378 if (!is_dir($directory)) {
379 // Let mkdir() recursively create directories and use the default directory
380 // permissions.
381 if (($mode & FILE_CREATE_DIRECTORY) && @mkdir($directory, variable_get('file_chmod_directory', 0775), TRUE)) {
382 drupal_chmod($directory);
383 }
384 else {
385 if ($form_item) {
386 form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory)));
387 watchdog('file system', 'The directory %directory does not exist.', array('%directory' => $directory), WATCHDOG_ERROR);
388 }
389 return FALSE;
390 }
391 }
392
393 // Check to see if the directory is writable.
394 if (!is_writable($directory)) {
395 // If not able to modify permissions, or if able to, but chmod
396 // fails, return false.
397 if (!$mode || (($mode & FILE_MODIFY_PERMISSIONS) && !drupal_chmod($directory))) {
398 if ($form_item) {
399 form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory)));
400 watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR);
401 }
402 return FALSE;
403 }
404 }
405
406 if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) {
407 $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
408 if (file_put_contents("$directory/.htaccess", $htaccess_lines)) {
409 drupal_chmod("$directory/.htaccess");
410 }
411 else {
412 $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
413 form_set_error($form_item, t("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));
414 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);
415 }
416 }
417
418 return TRUE;
419 }
420
421 /**
422 * Checks path to see if it is a directory, or a directory/file.
423 *
424 * @param $path
425 * A string containing a file path. This will be set to the directory's path.
426 * @return
427 * If the directory is not in a Drupal writable directory, FALSE is returned.
428 * Otherwise, the base name of the path is returned.
429 */
430 function file_check_path(&$path) {
431 // Check if path is a directory.
432 if (file_check_directory($path)) {
433 return '';
434 }
435
436 // Check if path is a possible dir/file.
437 $filename = basename($path);
438 $path = dirname($path);
439 if (file_check_directory($path)) {
440 return $filename;
441 }
442
443 return FALSE;
444 }
445
446 /**
447 * Check if a file is really located inside $directory.
448 *
449 * This should be used to make sure a file specified is really located within
450 * the directory to prevent exploits. Note that the file or path being checked
451 * does not actually need to exist yet.
452 *
453 * @code
454 * // Returns FALSE:
455 * file_check_location('/www/example.com/files/../../../etc/passwd', '/www/example.com/files');
456 * @endcode
457 *
458 * @param $source
459 * A string set to the file to check.
460 * @param $directory
461 * A string where the file should be located.
462 * @return
463 * FALSE if the path does not exist in the directory; otherwise, the real
464 * path of the source.
465 */
466 function file_check_location($source, $directory = '') {
467 $check = realpath($source);
468 if ($check) {
469 $source = $check;
470 }
471 else {
472 // This file does not yet exist.
473 $source = realpath(dirname($source)) . '/' . basename($source);
474 }
475 $directory = realpath($directory);
476 if ($directory && strpos($source, $directory) !== 0) {
477 return FALSE;
478 }
479 return $source;
480 }
481
482 /**
483 * Load file objects from the database.
484 *
485 * @param $fids
486 * An array of file IDs.
487 * @param $conditions
488 * An array of conditions to match against the {files} table. These
489 * should be supplied in the form array('field_name' => 'field_value').
490 * @return
491 * An array of file objects, indexed by fid.
492 *
493 * @see hook_file_load()
494 * @see file_load()
495 */
496 function file_load_multiple($fids = array(), $conditions = array()) {
497 $query = db_select('files', 'f')->fields('f');
498
499 // If the $fids array is populated, add those to the query.
500 if ($fids) {
501 $query->condition('f.fid', $fids, 'IN');
502 }
503
504 // If the conditions array is populated, add those to the query.
505 if ($conditions) {
506 foreach ($conditions as $field => $value) {
507 $query->condition('f.' . $field, $value);
508 }
509 }
510 $files = $query->execute()->fetchAllAssoc('fid');
511
512 // Invoke hook_file_load() on the terms loaded from the database
513 // and add them to the static cache.
514 if (!empty($files)) {
515 foreach (module_implements('file_load') as $module) {
516 $function = $module . '_file_load';
517 $function($files);
518 }
519 }
520 return $files;
521 }
522
523 /**
524 * Load a file object from the database.
525 *
526 * @param $fid
527 * A file ID.
528 * @return
529 * A file object.
530 *
531 * @see hook_file_load()
532 * @see file_load_multiple()
533 */
534 function file_load($fid) {
535 $files = file_load_multiple(array($fid), array());
536 return reset($files);
537 }
538
539 /**
540 * Save a file object to the database.
541 *
542 * If the $file->fid is not set a new record will be added. Re-saving an
543 * existing file will not change its status.
544 *
545 * @param $file
546 * A file object returned by file_load().
547 * @return
548 * The updated file object.
549 *
550 * @see hook_file_insert()
551 * @see hook_file_update()
552 */
553 function file_save($file) {
554 $file = (object)$file;
555 $file->timestamp = REQUEST_TIME;
556 $file->filesize = filesize($file->filepath);
557
558 if (empty($file->fid)) {
559 drupal_write_record('files', $file);
560 // Inform modules about the newly added file.
561 module_invoke_all('file_insert', $file);
562 }
563 else {
564 drupal_write_record('files', $file, 'fid');
565 // Inform modules that the file has been updated.
566 module_invoke_all('file_update', $file);
567 }
568
569 return $file;
570 }
571
572 /**
573 * Copy a file to a new location and adds a file record to the database.
574 *
575 * This function should be used when manipulating files that have records
576 * stored in the database. This is a powerful function that in many ways
577 * performs like an advanced version of copy().
578 * - Checks if $source and $destination are valid and readable/writable.
579 * - Checks that $source is not equal to $destination; if they are an error
580 * is reported.
581 * - If file already exists in $destination either the call will error out,
582 * replace the file or rename the file based on the $replace parameter.
583 * - Adds the new file to the files database. If the source file is a
584 * temporary file, the resulting file will also be a temporary file.
585 * @see file_save_upload() for details on temporary files.
586 *
587 * @param $source
588 * A file object.
589 * @param $destination
590 * A string containing the destination that $source should be copied to. This
591 * can be a complete file path, a directory path or, if this value is omitted,
592 * Drupal's 'files' directory will be used.
593 * @param $replace
594 * Replace behavior when the destination file already exists:
595 * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
596 * the destination name exists then its database entry will be updated. If
597 * no database entry is found then a new one will be created.
598 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
599 * unique.
600 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
601 * @return
602 * File object if the copy is successful, or FALSE in the event of an error.
603 *
604 * @see file_unmanaged_copy()
605 * @see hook_file_copy()
606 */
607 function file_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
608 $source = (object)$source;
609
610 if ($filepath = file_unmanaged_copy($source->filepath, $destination, $replace)) {
611 $file = clone $source;
612 $file->fid = NULL;
613 $file->filepath = $filepath;
614 $file->filename = basename($filepath);
615 // If we are replacing an existing file re-use its database record.
616 if ($replace == FILE_EXISTS_REPLACE) {
617 $existing_files = file_load_multiple(array(), array('filepath' => $filepath));
618 if (count($existing_files)) {
619 $existing = reset($existing_files);
620 $file->fid = $existing->fid;
621 $file->filename = $existing->filename;
622 }
623 }
624 // If we are renaming around an existing file (rather than a directory),
625 // use its basename for the filename.
626 else if ($replace == FILE_EXISTS_RENAME && is_file(file_create_path($destination))) {
627 $file->filename = basename($destination);
628 }
629
630 $file = file_save($file);
631
632 // Inform modules that the file has been copied.
633 module_invoke_all('file_copy', $file, $source);
634
635 return $file;
636 }
637 return FALSE;
638 }
639
640 /**
641 * Copy a file to a new location without calling any hooks or making any
642 * changes to the database.
643 *
644 * This is a powerful function that in many ways performs like an advanced
645 * version of copy().
646 * - Checks if $source and $destination are valid and readable/writable.
647 * - Checks that $source is not equal to $destination; if they are an error
648 * is reported.
649 * - If file already exists in $destination either the call will error out,
650 * replace the file or rename the file based on the $replace parameter.
651 *
652 * @param $source
653 * A string specifying the file location of the original file.
654 * @param $destination
655 * A string containing the destination that $source should be copied to. This
656 * can be a complete file path, a directory path or, if this value is omitted,
657 * Drupal's 'files' directory will be used.
658 * @param $replace
659 * Replace behavior when the destination file already exists:
660 * - FILE_EXISTS_REPLACE - Replace the existing file.
661 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
662 * unique.
663 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
664 * @return
665 * The path to the new file, or FALSE in the event of an error.
666 *
667 * @see file_copy()
668 */
669 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
670 $original_source = $source;
671 $original_destination = $destination;
672
673 $source = realpath($source);
674 if (!file_exists($source)) {
675 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');
676 return FALSE;
677 }
678
679 $proposed_destination = file_create_path($destination);
680 $directory = $proposed_destination;
681 $basename = file_check_path($directory);
682
683 // Make sure we at least have a valid directory.
684 if ($basename === FALSE) {
685 drupal_set_message(t('The specified file %file could not be copied, because the destination %directory is not properly configured. This is often caused by a problem with file or directory permissions.', array('%file' => $original_source, '%directory' => empty($original_destination) ? $proposed_destination : $original_destination)), 'error');
686 return FALSE;
687 }
688
689 // If the destination file is not specified then use the filename of the
690 // source file.
691 $basename = $basename ? $basename : basename($source);
692 $destination = file_destination($directory . '/' . $basename, $replace);
693
694 if ($destination === FALSE) {
695 drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $source, '%directory' => $proposed_destination)), 'error');
696 return FALSE;
697 }
698 // Make sure source and destination filenames are not the same, makes no
699 // sense to copy it if they are. In fact copying the file will most likely
700 // result in a 0 byte file. Which is bad. Real bad.
701 if ($source == realpath($destination)) {
702 drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
703 return FALSE;
704 }
705 if (!@copy($source, $destination)) {
706 drupal_set_message(t('The specified file %file could not be copied.', array('%file' => $source)), 'error');
707 return FALSE;
708 }
709
710 // Set the permissions on the new file.
711 drupal_chmod($destination);
712
713 return $destination;
714 }
715
716 /**
717 * Determines the destination path for a file depending on how replacement of
718 * existing files should be handled.
719 *
720 * @param $destination
721 * A string specifying the desired path.
722 * @param $replace
723 * Replace behavior when the destination file already exists.
724 * - FILE_EXISTS_REPLACE - Replace the existing file.
725 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
726 * unique.
727 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
728 * @return
729 * The destination file path or FALSE if the file already exists and
730 * FILE_EXISTS_ERROR was specified.
731 */
732 function file_destination($destination, $replace) {
733 if (file_exists($destination)) {
734 switch ($replace) {
735 case FILE_EXISTS_REPLACE:
736 // Do nothing here, we want to overwrite the existing file.
737 break;
738
739 case FILE_EXISTS_RENAME:
740 $basename = basename($destination);
741 $directory = dirname($destination);
742 $destination = file_create_filename($basename, $directory);
743 break;
744
745 case FILE_EXISTS_ERROR:
746 // Error reporting handled by calling function.
747 return FALSE;
748 }
749 }
750 return $destination;
751 }
752
753 /**
754 * Move a file to a new location and update the file's database entry.
755 *
756 * Moving a file is performed by copying the file to the new location and then
757 * deleting the original.
758 * - Checks if $source and $destination are valid and readable/writable.
759 * - Performs a file move if $source is not equal to $destination.
760 * - If file already exists in $destination either the call will error out,
761 * replace the file or rename the file based on the $replace parameter.
762 * - Adds the new file to the files database.
763 *
764 * @param $source
765 * A file object.
766 * @param $destination
767 * A string containing the destination that $source should be moved to. This
768 * can be a complete file path, a directory path or, if this value is omitted,
769 * Drupal's 'files' directory will be used.
770 * @param $replace
771 * Replace behavior when the destination file already exists:
772 * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
773 * the destination name exists then its database entry will be updated and
774 * file_delete() called on the source file after hook_file_move is called.
775 * If no database entry is found then the source files record will be
776 * updated.
777 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
778 * unique.
779 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
780 * @return
781 * Resulting file object for success, or FALSE in the event of an error.
782 *
783 * @see file_unmanaged_move()
784 * @see hook_file_move()
785 */
786 function file_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
787 $source = (object)$source;
788
789 if ($filepath = file_unmanaged_move($source->filepath, $destination, $replace)) {
790 $delete_source = FALSE;
791
792 $file = clone $source;
793 $file->filepath = $filepath;
794 // If we are replacing an existing file re-use its database record.
795 if ($replace == FILE_EXISTS_REPLACE) {
796 $existing_files = file_load_multiple(array(), array('filepath' => $filepath));
797 if (count($existing_files)) {
798 $existing = reset($existing_files);
799 $delete_source = TRUE;
800 $file->fid = $existing->fid;
801 }
802 }
803 // If we are renaming around an existing file (rather than a directory),
804 // use its basename for the filename.
805 else if ($replace == FILE_EXISTS_RENAME && is_file(file_create_path($destination))) {
806 $file->filename = basename($destination);
807 }
808
809 $file = file_save($file);
810
811 // Inform modules that the file has been moved.
812 module_invoke_all('file_move', $file, $source);
813
814 if ($delete_source) {
815 // Try a soft delete to remove original if it's not in use elsewhere.
816 file_delete($source);
817 }
818
819 return $file;
820 }
821 return FALSE;
822 }
823
824 /**
825 * Move a file to a new location without calling any hooks or making any
826 * changes to the database.
827 *
828 * @param $source
829 * A string specifying the file location of the original file.
830 * @param $destination
831 * A string containing the destination that $source should be moved to. This
832 * can be a complete file path, a directory name or, if this value is omitted,
833 * Drupal's 'files' directory will be used.
834 * @param $replace
835 * Replace behavior when the destination file already exists:
836 * - FILE_EXISTS_REPLACE - Replace the existing file.
837 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
838 * unique.
839 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
840 * @return
841 * The filepath of the moved file, or FALSE in the event of an error.
842 *
843 * @see file_move()
844 */
845 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
846 $filepath = file_unmanaged_copy($source, $destination, $replace);
847 if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
848 return FALSE;
849 }
850 return $filepath;
851 }
852
853 /**
854 * Munge the filename as needed for security purposes.
855 *
856 * For instance the file name "exploit.php.pps" would become "exploit.php_.pps".
857 *
858 * @param $filename
859 * The name of a file to modify.
860 * @param $extensions
861 * A space separated list of extensions that should not be altered.
862 * @param $alerts
863 * Whether alerts (watchdog, drupal_set_message()) should be displayed.
864 * @return
865 * $filename The potentially modified $filename.
866 */
867 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
868 $original = $filename;
869
870 // Allow potentially insecure uploads for very savvy users and admin
871 if (!variable_get('allow_insecure_uploads', 0)) {
872 $whitelist = array_unique(explode(' ', trim($extensions)));
873
874 // Split the filename up by periods. The first part becomes the basename
875 // the last part the final extension.
876 $filename_parts = explode('.', $filename);
877 $new_filename = array_shift($filename_parts); // Remove file basename.
878 $final_extension = array_pop($filename_parts); // Remove final extension.
879
880 // Loop through the middle parts of the name and add an underscore to the
881 // end of each section that could be a file extension but isn't in the list
882 // of allowed extensions.
883 foreach ($filename_parts as $filename_part) {
884 $new_filename .= '.' . $filename_part;
885 if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
886 $new_filename .= '_';
887 }
888 }
889 $filename = $new_filename . '.' . $final_extension;
890
891 if ($alerts && $original != $filename) {
892 drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
893 }
894 }
895
896 return $filename;
897 }
898
899 /**
900 * Undo the effect of upload_munge_filename().
901 *
902 * @param $filename
903 * String with the filename to be unmunged.
904 * @return
905 * An unmunged filename string.
906 */
907 function file_unmunge_filename($filename) {
908 return str_replace('_.', '.', $filename);
909 }
910
911 /**
912 * Create a full file path from a directory and filename.
913 *
914 * If a file with the specified name already exists, an alternative will be
915 * used.
916 *
917 * @param $basename
918 * String filename
919 * @param $directory
920 * String directory
921 * @return
922 * File path consisting of $directory and a unique filename based off
923 * of $basename.
924 */
925 function file_create_filename($basename, $directory) {
926 $destination = $directory . '/' . $basename;
927
928 if (file_exists($destination)) {
929 // Destination file already exists, generate an alternative.
930 $pos = strrpos($basename, '.');
931 if ($pos !== FALSE) {
932 $name = substr($basename, 0, $pos);
933 $ext = substr($basename, $pos);
934 }
935 else {
936 $name = $basename;
937 $ext = '';
938 }
939
940 $counter = 0;
941 do {
942 $destination = $directory . '/' . $name . '_' . $counter++ . $ext;
943 } while (file_exists($destination));
944 }
945
946 return $destination;
947 }
948
949 /**
950 * Delete a file and its database record.
951 *
952 * If the $force parameter is not TRUE hook_file_references() will be called
953 * to determine if the file is being used by any modules. If the file is being
954 * used is the delete will be canceled.
955 *
956 * @param $file
957 * A file object.
958 * @param $force
959 * Boolean indicating that the file should be deleted even if
960 * hook_file_references() reports that the file is in use.
961 * @return mixed
962 * TRUE for success, FALSE in the event of an error, or an array if the file
963 * is being used by another module. The array keys are the module's name and
964 * the values are the number of references.
965 *
966 * @see file_unmanaged_delete()
967 * @see hook_file_references()
968 * @see hook_file_delete()
969 */
970 function file_delete($file, $force = FALSE) {
971 $file = (object)$file;
972
973 // If any module returns a value from the reference hook, the file will not
974 // be deleted from Drupal, but file_delete will return a populated array that
975 // tests as TRUE.
976 if (!$force && ($references = module_invoke_all('file_references', $file))) {
977 return $references;
978 }
979
980 // Let other modules clean up any references to the deleted file.
981 module_invoke_all('file_delete', $file);
982
983 // Make sure the file is deleted before removing its row from the
984 // database, so UIs can still find the file in the database.
985 if (file_unmanaged_delete($file->filepath)) {
986 db_delete('files')->condition('fid', $file->fid)->execute();
987 return TRUE;
988 }
989 return FALSE;
990 }
991
992 /**
993 * Delete a file without calling any hooks or making any changes to the
994 * database.
995 *
996 * This function should be used when the file to be deleted does not have an
997 * entry recorded in the files table.
998 *
999 * @param $path
1000 * A string containing a file path.
1001 * @return
1002 * TRUE for success or path does not exist, or FALSE in the event of an
1003 * error.
1004 *
1005 * @see file_delete()
1006 * @see file_unmanaged_delete_recursive()
1007 */
1008 function file_unmanaged_delete($path) {
1009 if (is_dir($path)) {
1010 watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
1011 return FALSE;
1012 }
1013 if (is_file($path)) {
1014 return unlink($path);
1015 }
1016 // Return TRUE for non-existent file, but log that nothing was actually
1017 // deleted, as the current state is the indended result.
1018 if (!file_exists($path)) {
1019 watchdog('file', 'The file %path was not deleted, because it does not exist.', array('%path' => $path), WATCHDOG_NOTICE);
1020 return TRUE;
1021 }
1022 // We cannot handle anything other than files and directories. Log an error
1023 // for everything else (sockets, symbolic links, etc).
1024 watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), WATCHDOG_ERROR);
1025 return FALSE;
1026 }
1027
1028 /**
1029 * Recursively delete all files and directories in the specified filepath.
1030 *
1031 * If the specified path is a directory then the function will call itself
1032 * recursively to process the contents. Once the contents have been removed the
1033 * directory will also be removed.
1034 *
1035 * If the specified path is a file then it will be passed to
1036 * file_unmanaged_delete().
1037 *
1038 * Note that this only deletes visible files with write permission.
1039 *
1040 * @param $path
1041 * A string containing a file or directory path.
1042 * @return
1043 * TRUE for success or path does not exist, or FALSE in the event of an
1044 * error.
1045 *
1046 * @see file_unmanaged_delete()
1047 */
1048 function file_unmanaged_delete_recursive($path) {
1049 if (is_dir($path)) {
1050 $dir = dir($path);
1051 while (($entry = $dir->read()) !== FALSE) {
1052 if ($entry == '.' || $entry == '..') {
1053 continue;
1054 }
1055 $entry_path = $path . '/' . $entry;
1056 file_unmanaged_delete_recursive($entry_path);
1057 }
1058 $dir->close();
1059 return rmdir($path);
1060 }
1061 return file_unmanaged_delete($path);
1062 }
1063
1064 /**
1065 * Determine total disk space used by a single user or the whole filesystem.
1066 *
1067 * @param $uid
1068 * Optional. A user id, specifying NULL returns the total space used by all
1069 * non-temporary files.
1070 * @param $status
1071 * Optional. File Status to return. Combine with a bitwise OR(|) to return
1072 * multiple statuses. The default status is FILE_STATUS_PERMANENT.
1073 * @return
1074 * An integer containing the number of bytes used.
1075 */
1076 function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
1077 $query = db_select('files', 'f');
1078 // Use separate placeholders for the status to avoid a bug in some versions
1079 // of PHP. @see http://drupal.org/node/352956
1080 $query->where('f.status & :status1 = :status2', array(':status1' => $status, ':status2' => $status));
1081 $query->addExpression('SUM(f.filesize)', 'filesize');
1082 if (!is_null($uid)) {
1083 $query->condition('f.uid', $uid);
1084 }
1085 return $query->execute()->fetchField();
1086 }
1087
1088 /**
1089 * Saves a file upload to a new location.
1090 *
1091 * The file will be added to the files table as a temporary file. Temporary
1092 * files are periodically cleaned. To make the file a permanent file call
1093 * assign the status and use file_save() to save it.
1094 *
1095 * @param $source
1096 * A string specifying the name of the upload field to save.
1097 * @param $validators
1098 * An optional, associative array of callback functions used to validate the
1099 * file. See file_validate() for a full discussion of the array format.
1100 * @param $destination
1101 * A string containing the directory $source should be copied to. If this is
1102 * not provided or is not writable, the temporary directory will be used.
1103 * @param $replace
1104 * Replace behavior when the destination file already exists:
1105 * - FILE_EXISTS_REPLACE: Replace the existing file.
1106 * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is
1107 * unique.
1108 * - FILE_EXISTS_ERROR: Do nothing and return FALSE.
1109 * @return
1110 * An object containing the file information if the upload succeeded, FALSE
1111 * in the event of an error, or NULL if no file was uploaded.
1112 */
1113 function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
1114 global $user;
1115 static $upload_cache;
1116
1117 // Return cached objects without processing since the file will have
1118 // already been processed and the paths in _FILES will be invalid.
1119 if (isset($upload_cache[$source])) {
1120 return $upload_cache[$source];
1121 }
1122
1123 // Make sure there's an upload to process.
1124 if (empty($_FILES['files']['name'][$source])) {
1125 return NULL;
1126 }
1127
1128 // Check for file upload errors and return FALSE if a lower level system
1129 // error occurred. For a complete list of errors:
1130 // @see http://php.net/manual/en/features.file-upload.errors.php
1131 switch ($_FILES['files']['error'][$source]) {
1132 case UPLOAD_ERR_INI_SIZE:
1133 case UPLOAD_ERR_FORM_SIZE:
1134 drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $_FILES['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()))), 'error');
1135 return FALSE;
1136
1137 case UPLOAD_ERR_PARTIAL:
1138 case UPLOAD_ERR_NO_FILE:
1139 drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source])), 'error');
1140 return FALSE;
1141
1142 case UPLOAD_ERR_OK:
1143 // Final check that this is a valid upload, if it isn't, use the
1144 // default error handler.
1145 if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
1146 break;
1147 }
1148
1149 // Unknown error
1150 default:
1151 drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error');
1152 return FALSE;
1153 }
1154
1155 // Build the list of non-munged extensions.
1156 // @todo: this should not be here. we need to figure out the right place.
1157 $extensions = '';
1158 foreach ($user->roles as $rid => $name) {
1159 $extensions .= ' ' . variable_get("upload_extensions_$rid",
1160 variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
1161 }
1162
1163 // Begin building file object.
1164 $file = new stdClass();
1165 $file->uid = $user->uid;
1166 $file->status = 0;
1167 $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions);
1168 $file->filepath = $_FILES['files']['tmp_name'][$source];
1169 $file->filemime = file_get_mimetype($file->filename);
1170 $file->filesize = $_FILES['files']['size'][$source];
1171
1172 // Rename potentially executable files, to help prevent exploits.
1173 if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
1174 $file->filemime = 'text/plain';
1175 $file->filepath .= '.txt';
1176 $file->filename .= '.txt';
1177 }
1178
1179 // If the destination is not provided, or is not writable, then use the
1180 // temporary directory.
1181 if (empty($destination) || file_check_path($destination) === FALSE) {
1182 $destination = file_directory_temp();
1183 }
1184
1185 $file->source = $source;
1186 $file->destination = file_destination(file_create_path($destination . '/' . $file->filename), $replace);
1187 // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
1188 // there's an existing file so we need to bail.
1189 if ($file->destination === FALSE) {
1190 drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)), 'error');
1191 return FALSE;
1192 }
1193
1194 // Add in our check of the the file name length.
1195 $validators['file_validate_name_length'] = array();
1196
1197 // Call the validation functions specified by this function's caller.
1198 $errors = file_validate($file, $validators);
1199
1200 // Check for errors.
1201 if (!empty($errors)) {
1202 $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
1203 if (count($errors) > 1) {
1204 $message .= theme('item_list', $errors);
1205 }
1206 else {
1207 $message .= ' ' . array_pop($errors);
1208 }
1209 form_set_error($source, $message);
1210 return FALSE;
1211 }
1212
1213 // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
1214 // directory. This overcomes open_basedir restrictions for future file
1215 // operations.
1216 $file->filepath = $file->destination;
1217 if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->filepath)) {
1218 form_set_error($source, t('File upload error. Could not move uploaded file.'));
1219 watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->filepath));
1220 return FALSE;
1221 }
1222
1223 // Set the permissions on the new file.
1224 drupal_chmod($file->filepath);
1225
1226 // If we are replacing an existing file re-use its database record.
1227 if ($replace == FILE_EXISTS_REPLACE) {
1228 $existing_files = file_load_multiple(array(), array('filepath' => $file->filepath));
1229 if (count($existing_files)) {
1230 $existing = reset($existing_files);
1231 $file->fid = $existing->fid;
1232 }
1233 }
1234
1235 // If we made it this far it's safe to record this file in the database.
1236 if ($file = file_save($file)) {
1237 // Add file to the cache.
1238 $upload_cache[$source] = $file;
1239 return $file;
1240 }
1241 return FALSE;
1242 }
1243
1244
1245 /**
1246 * Check that a file meets the criteria specified by the validators.
1247 *
1248 * After executing the validator callbacks specified hook_file_validate() will
1249 * also be called to allow other modules to report errors about the file.
1250 *
1251 * @param $file
1252 * A Drupal file object.
1253 * @param $validators
1254 * An optional, associative array of callback functions used to validate the
1255 * file. The keys are function names and the values arrays of callback
1256 * parameters which will be passed in after the file object. The
1257 * functions should return an array of error messages; an empty array
1258 * indicates that the file passed validation. The functions will be called in
1259 * the order specified.
1260 * @return
1261 * An array contaning validation error messages.
1262 *
1263 * @see hook_file_validate()
1264 */
1265 function file_validate(&$file, $validators = array()) {
1266 // Call the validation functions specified by this function's caller.
1267 $errors = array();
1268 foreach ($validators as $function => $args) {
1269 if (drupal_function_exists($function)) {
1270 array_unshift($args, $file);
1271 $errors = array_merge($errors, call_user_func_array($function, $args));
1272 }
1273 }
1274
1275 // Let other modules perform validation on the new file.
1276 return array_merge($errors, module_invoke_all('file_validate', $file));
1277 }
1278
1279 /**
1280 * Check for files with names longer than we can store in the database.
1281 *
1282 * @param $file
1283 * A Drupal file object.
1284 * @return
1285 * An array. If the file name is too long, it will contain an error message.
1286 */
1287 function file_validate_name_length($file) {
1288 $errors = array();
1289
1290 if (empty($file->filename)) {
1291 $errors[] = t("The file's name is empty. Please give a name to the file.");
1292 }
1293 if (strlen($file->filename) > 255) {
1294 $errors[] = t("The file's name exceeds the 255 characters limit. Please rename the file and try again.");
1295 }
1296 return $errors;
1297 }
1298
1299 /**
1300 * Check that the filename ends with an allowed extension.
1301 *
1302 * @param $file
1303 * A Drupal file object.
1304 * @param $extensions
1305 * A string with a space separated list of allowed extensions.
1306 * @return
1307 * An array. If the file extension is not allowed, it will contain an error
1308 * message.
1309 *
1310 * @see hook_file_validate()
1311 */
1312 function file_validate_extensions($file, $extensions) {
1313 $errors = array();
1314
1315 $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
1316 if (!preg_match($regex, $file->filename)) {
1317 $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
1318 }
1319 return $errors;
1320 }
1321
1322 /**
1323 * Check that the file's size is below certain limits.
1324 *
1325 * This check is not enforced for the user #1.
1326 *
1327 * @param $file
1328 * A Drupal file object.
1329 * @param $file_limit
1330 * An integer specifying the maximum file size in bytes. Zero indicates that
1331 * no limit should be enforced.
1332 * @param $user_limit
1333 * An integer specifying the maximum number of bytes the user is allowed.
1334 * Zero indicates that no limit should be enforced.
1335 * @return
1336 * An array. If the file size exceeds limits, it will contain an error
1337 * message.
1338 *
1339 * @see hook_file_validate()
1340 */
1341 function file_validate_size($file, $file_limit = 0, $user_limit = 0) {
1342 global $user;
1343
1344 $errors = array();
1345
1346 // Bypass validation for uid = 1.
1347 if ($user->uid != 1) {
1348 if ($file_limit && $file->filesize > $file_limit) {
1349 $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit)));
1350 }
1351
1352 // Save a query by only calling file_space_used() when a limit is provided.
1353 if ($user_limit && (file_space_used($user->uid) + $file->filesize) > $user_limit) {
1354 $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit)));
1355 }
1356 }
1357 return $errors;
1358 }
1359
1360 /**
1361 * Check that the file is recognized by image_get_info() as an image.
1362 *
1363 * @param $file
1364 * A Drupal file object.
1365 * @return
1366 * An array. If the file is not an image, it will contain an error message.
1367 *
1368 * @see hook_file_validate()
1369 */
1370 function file_validate_is_image($file) {
1371 $errors = array();
1372
1373 $info = image_get_info($file->filepath);
1374 if (!$info || empty($info['extension'])) {
1375 $errors[] = t('Only JPEG, PNG and GIF images are allowed.');
1376 }
1377
1378 return $errors;
1379 }
1380
1381 /**
1382 * If the file is an image verify that its dimensions are within the specified
1383 * maximum and minimum dimensions.
1384 *
1385 * Non-image files will be ignored. If a image toolkit is available the image
1386 * will be scalled to fit within the desired maximum dimensions.
1387 *
1388 * @param $file
1389 * A Drupal file object. This function may resize the file affecting its
1390 * size.
1391 * @param $maximum_dimensions
1392 * An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
1393 * an image toolkit is installed the image will be resized down to these
1394 * dimensions. A value of 0 indicates no restriction on size, so resizing
1395 * will be attempted.
1396 * @param $minimum_dimensions
1397 * An optional string in the form WIDTHxHEIGHT. This will check that the
1398 * image meets a minimum size. A value of 0 indicates no restriction.
1399 * @return
1400 * An array. If the file is an image and did not meet the requirements, it
1401 * will contain an error message.
1402 *
1403 * @see hook_file_validate()
1404 */
1405 function file_validate_image_resolution($file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
1406 $errors = array();
1407
1408 // Check first that the file is an image.
1409 if ($info = image_get_info($file->filepath)) {
1410 if ($maximum_dimensions) {
1411 // Check that it is smaller than the given dimensions.
1412 list($width, $height) = explode('x', $maximum_dimensions);
1413 if ($info['width'] > $width || $info['height'] > $height) {
1414 // Try to resize the image to fit the dimensions.
1415 if ($image = image_load($file->filepath)) {
1416 image_scale($image, $width, $height);
1417 image_save($image);
1418 $file->filesize = $image->info['file_size'];
1419 drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
1420 }
1421 else {
1422 $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
1423 }
1424 }
1425 }
1426
1427 if ($minimum_dimensions) {
1428 // Check that it is larger than the given dimensions.
1429 list($width, $height) = explode('x', $minimum_dimensions);
1430 if ($info['width'] < $width || $info['height'] < $height) {
1431 $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions));
1432 }
1433 }
1434 }
1435
1436 return $errors;
1437 }
1438
1439 /**
1440 * Save a string to the specified destination and create a database file entry.
1441 *
1442 * @param $data
1443 * A string containing the contents of the file.
1444 * @param $destination
1445 * A string containing the destination location. If no value is provided
1446 * then a randomly name will be generated and the file saved in Drupal's
1447 * files directory.
1448 * @param $replace
1449 * Replace behavior when the destination file already exists:
1450 * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
1451 * the destination name exists then its database entry will be updated. If
1452 * no database entry is found then a new one will be created.
1453 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
1454 * unique.
1455 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
1456 * @return
1457 * A file object, or FALSE on error.
1458 *
1459 * @see file_unmanaged_save_data()
1460 */
1461 function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
1462 global $user;
1463
1464 if ($filepath = file_unmanaged_save_data($data, $destination, $replace)) {
1465 // Create a file object.
1466 $file = new stdClass();
1467 $file->fid = NULL;
1468 $file->filepath = $filepath;
1469 $file->filename = basename($filepath);
1470 $file->filemime = file_get_mimetype($file->filepath);
1471 $file->uid = $user->uid;
1472 $file->status |= FILE_STATUS_PERMANENT;
1473 // If we are replacing an existing file re-use its database record.
1474 if ($replace == FILE_EXISTS_REPLACE) {
1475 $existing_files = file_load_multiple(array(), array('filepath' => $filepath));
1476 if (count($existing_files)) {
1477 $existing = reset($existing_files);
1478 $file->fid = $existing->fid;
1479 $file->filename = $existing->filename;
1480 }
1481 }
1482 // If we are renaming around an existing file (rather than a directory),
1483 // use its basename for the filename.
1484 else if ($replace == FILE_EXISTS_RENAME && is_file(file_create_path($destination))) {
1485 $file->filename = basename($destination);
1486 }
1487
1488 return file_save($file);
1489 }
1490 return FALSE;
1491 }
1492
1493 /**
1494 * Save a string to the specified destination without calling any hooks or
1495 * making any changes to the database.
1496 *
1497 * This function is identical to file_save_data() except the file will not be
1498 * saved to the files table and none of the file_* hooks will be called.
1499 *
1500 * @param $data
1501 * A string containing the contents of the file.
1502 * @param $destination
1503 * A string containing the destination location. If no value is provided
1504 * then a randomly name will be generated and the file saved in Drupal's
1505 * files directory.
1506 * @param $replace
1507 * Replace behavior when the destination file already exists:
1508 * - FILE_EXISTS_REPLACE - Replace the existing file.
1509 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
1510 * unique.
1511 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
1512 * @return
1513 * A string with the path of the resulting file, or FALSE on error.
1514 *
1515 * @see file_save_data()
1516 */
1517 function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
1518 // Write the data to a temporary file.
1519 $temp_name = tempnam(file_directory_temp(), 'file');
1520 if (file_put_contents($temp_name, $data) === FALSE) {
1521 drupal_set_message(t('The file could not be created.'), 'error');
1522 return FALSE;
1523 }
1524
1525 // Move the file to its final destination.
1526 return file_unmanaged_move($temp_name, $destination, $replace);
1527 }
1528
1529 /**
1530 * Transfer file using HTTP to client. Pipes a file through Drupal to the
1531 * client.
1532 *
1533 * @param $source
1534 * String specifying the file path to transfer.
1535 * @param $headers
1536 * An array of HTTP headers to send along with file.
1537 */
1538 function file_transfer($source, $headers) {
1539 if (ob_get_level()) {
1540 ob_end_clean();
1541 }
1542
1543 foreach ($headers as $name => $value) {
1544 drupal_set_header($name, $value);
1545 }
1546 drupal_send_headers();
1547
1548 $source = file_create_path($source);
1549
1550 // Transfer file in 1024 byte chunks to save memory usage.
1551 if ($fd = fopen($source, 'rb')) {
1552 while (!feof($fd)) {
1553 print fread($fd, 1024);
1554 }
1555 fclose($fd);
1556 }
1557 else {
1558 drupal_not_found();
1559 }
1560 exit();
1561 }
1562
1563 /**
1564 * Menu handler for private file transfers.
1565 *
1566 * Call modules that implement hook_file_download() to find out if a file is
1567 * accessible and what headers it should be transferred with. If a module
1568 * returns -1 drupal_access_denied() will be returned. If one or more modules
1569 * returned headers the download will start with the returned headers. If no
1570 * modules respond drupal_not_found() will be returned.
1571 *
1572 * @see hook_file_download()
1573 */
1574 function file_download() {
1575 // Merge remainder of arguments from GET['q'], into relative file path.
1576 $args = func_get_args();
1577 $filepath = implode('/', $args);
1578
1579 // Maintain compatibility with old ?file=paths saved in node bodies.
1580 if (isset($_GET['file'])) {
1581 $filepath = $_GET['file'];
1582 }
1583
1584 if (file_exists(file_create_path($filepath))) {
1585 // Let other modules provide headers and controls access to the file.
1586 $headers = module_invoke_all('file_download', $filepath);
1587 if (in_array(-1, $headers)) {
1588 return drupal_access_denied();
1589 }
1590 if (count($headers)) {
1591 file_transfer($filepath, $headers);
1592 }
1593 }
1594 return drupal_not_found();
1595 }
1596
1597
1598 /**
1599 * Finds all files that match a given mask in a given directory.
1600 *
1601 * Directories and files beginning with a period are excluded; this
1602 * prevents hidden files and directories (such as SVN working directories)
1603 * from being scanned.
1604 *
1605 * @param $dir
1606 * The base directory for the scan, without trailing slash.
1607 * @param $mask
1608 * The preg_match() regular expression of the files to find.
1609 * @param $options
1610 * An associative array of additional options, with the following keys:
1611 * - 'nomask'
1612 * The preg_match() regular expression of the files to ignore. Defaults to
1613 * '/(\.\.?|CVS)$/'.
1614 * - 'callback'
1615 * The callback function to call for each match. There is no default
1616 * callback.
1617 * - 'recurse'
1618 * When TRUE, the directory scan will recurse the entire tree starting at
1619 * the provided directory. Defaults to TRUE.
1620 * - 'key'
1621 * The key to be used for the returned array of files. Possible values are
1622 * 'filepath', for the path starting with $dir, 'filename', for the
1623 * basename of the file, and 'name' for the name of the file without an
1624 * extension. Defaults to 'filepath'.
1625 * - 'min_depth'
1626 * Minimum depth of directories to return files from. Defaults to 0.
1627 * @param $depth
1628 * Current depth of recursion. This parameter is only used internally and
1629 * should not be passed.
1630 * @return
1631 * An associative array (keyed on the provided key) of objects with
1632 * 'filepath', 'filename', and 'name' members corresponding to the
1633 * matching files.
1634 */
1635 function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
1636 // Merge in defaults.
1637 $options += array(
1638 'nomask' => '/(\.\.?|CVS)$/',
1639 'callback' => 0,
1640 'recurse' => TRUE,
1641 'key' => 'filepath',
1642 'min_depth' => 0,
1643 );
1644
1645 $options['key'] = in_array($options['key'], array('filepath', 'filename', 'name')) ? $options['key'] : 'filepath';
1646 $files = array();
1647 if (is_dir($dir) && $handle = opendir($dir)) {
1648 while (FALSE !== ($filename = readdir($handle))) {
1649 if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') {
1650 $filepath = "$dir/$filename";
1651 if (is_dir($filepath) && $options['recurse']) {
1652 // Give priority to files in this folder by merging them in after any subdirectory files.
1653 $files = array_merge(file_scan_directory($filepath, $mask, $options, $depth + 1), $files);
1654 }
1655 elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
1656 // Always use this match over anything already set in $files with the
1657 // same $$options['key'].
1658 $file = (object) array(
1659 'filepath' => $filepath,
1660 'filename' => $filename,
1661 'name' => pathinfo($filename, PATHINFO_FILENAME),
1662 );
1663 $key = $options['key'];
1664 $files[$file->$key] = $file;
1665 if ($options['callback']) {
1666 $options['callback']($filepath);
1667 }
1668 }
1669 }
1670 }
1671
1672 closedir($handle);
1673 }
1674
1675 return $files;
1676 }
1677
1678 /**
1679 * Determine the default temporary directory.
1680 *
1681 * @return
1682 * A string containing a temp directory.
1683 */
1684 function file_directory_temp() {
1685 $temporary_directory = variable_get('file_directory_temp');
1686
1687 if (is_null($temporary_directory)) {
1688 $directories = array();
1689
1690 // Has PHP been set with an upload_tmp_dir?
1691 if (ini_get('upload_tmp_dir')) {
1692 $directories[] = ini_get('upload_tmp_dir');
1693 }
1694
1695 // Operating system specific dirs.
1696 if (substr(PHP_OS, 0, 3) == 'WIN') {
1697 $directories[] = 'c:/windows/temp';
1698 $directories[] = 'c:/winnt/temp';
1699 }
1700 else {
1701 $directories[] = '/tmp';
1702 }
1703
1704 foreach ($directories as $directory) {
1705 if (!$temporary_directory && is_dir($directory)) {
1706 $temporary_directory = $directory;
1707 }
1708 }
1709
1710 // if a directory has been found, use it, otherwise default to 'files/tmp'
1711 $temporary_directory = $temporary_directory ? $temporary_directory : file_directory_path() . '/tmp';
1712 variable_set('file_directory_temp', $temporary_directory);
1713 }
1714
1715 return $temporary_directory;
1716 }
1717
1718 /**
1719 * Determine the default 'files' directory.
1720 *
1721 * @return
1722 * A string containing the path to Drupal's 'files' directory.
1723 */
1724 function file_directory_path() {
1725 return variable_get('file_directory_path', conf_path() . '/files');
1726 }
1727
1728 /**
1729 * Remove a possible leading file directory path from the given path.
1730 *
1731 * @param $path
1732 * Path to a file that may be in Drupal's files directory.
1733 * @return
1734 * String with Drupal's files directory removed from it.
1735 */
1736 function file_directory_strip($path) {
1737 // Strip file_directory_path from $path. We only include relative paths in
1738 // URLs.
1739 if (strpos($path, file_directory_path() . '/') === 0) {
1740 $path = trim(substr($path, strlen(file_directory_path())), '\\/');
1741 }
1742 return $path;
1743 }
1744
1745 /**
1746 * Determine the maximum file upload size by querying the PHP settings.
1747 *
1748 * @return
1749 * A file size limit in bytes based on the PHP upload_max_filesize and
1750 * post_max_size
1751 */
1752 function file_upload_max_size() {
1753 static $max_size = -1;
1754
1755 if ($max_size < 0) {
1756 $upload_max = parse_size(ini_get('upload_max_filesize'));
1757 $post_max = parse_size(ini_get('post_max_size'));
1758 $max_size = ($upload_max < $post_max) ? $upload_max : $post_max;
1759 }
1760 return $max_size;
1761 }
1762
1763 /**
1764 * Determine an Internet Media Type, or MIME type from a filename.
1765 *
1766 * @param $filename
1767 * Name of the file, including extension.
1768 * @param $mapping
1769 * An optional map of extensions to their mimetypes, in the form:
1770 * - 'mimetypes': a list of mimetypes, keyed by an identifier,
1771 * - 'extensions': the mapping itself, an associative array in which
1772 * the key is the extension (lowercase) and the value is the mimetype identifier.
1773 * If $mapping is omitted, the drupal variable mime_extension_mapping is checked
1774 * for a value and if that fails then file_default_mimetype_mapping() is called
1775 *
1776 * @return
1777 * The internet media type registered for the extension or application/octet-stream for unknown extensions.
1778 * @see
1779 * file_default_mimetype_mapping()
1780 */
1781 function file_get_mimetype($filename, $mapping = NULL) {
1782 if (!isset($mapping)) {
1783 $mapping = variable_get('mime_extension_mapping', NULL);
1784 if (!isset($mapping) && drupal_function_exists('file_default_mimetype_mapping')) {
1785 // The default file map, defined in file.mimetypes.inc is quite big.
1786 // We only load it when necessary.
1787 $mapping = file_default_mimetype_mapping();
1788 }
1789 }
1790
1791 $extension = '';
1792 $file_parts = explode('.', $filename);
1793
1794 // Remove the first part: a full filename should not match an extension.
1795 array_shift($file_parts);
1796
1797 // Iterate over the file parts, trying to find a match.
1798 // For my.awesome.image.jpeg, we try:
1799 // - jpeg
1800 // - image.jpeg, and
1801 // - awesome.image.jpeg
1802 while ($additional_part = array_pop($file_parts)) {
1803 $extension = strtolower($additional_part . ($extension ? '.' . $extension : ''));
1804 if (isset($mapping['extensions'][$extension])) {
1805 return $mapping['mimetypes'][$mapping['extensions'][$extension]];
1806 }
1807 }
1808
1809 return 'application/octet-stream';
1810 }
1811
1812 /**
1813 * Set the permissions on a file or directory.
1814 *
1815 * This function will use the 'file_chmod_directory' and 'file_chmod_file'
1816 * variables for the default modes for directories and uploaded/generated files.
1817 * By default these will give everyone read access so that users accessing the
1818 * files with a user account without the webserver group (e.g. via FTP) can read
1819 * these files, and give group write permissions so webserver group members
1820 * (e.g. a vhost account) can alter files uploaded and owned by the webserver.
1821 *
1822 * @param $path
1823 * String containing the path to a file or directory.
1824 * @param $mode
1825 * Integer value for the permissions. Consult PHP chmod() documentation for
1826 * more information.
1827 * @return
1828 * TRUE for success, FALSE in the event of an error.
1829 */
1830 function drupal_chmod($path, $mode = NULL) {
1831 if (!isset($mode)) {
1832 if (is_dir($path)) {
1833 $mode = variable_get('file_chmod_directory', 0775);
1834 }
1835 else {
1836 $mode = variable_get('file_chmod_file', 0664);
1837 }
1838 }
1839
1840 if (@chmod($path, $mode)) {
1841 return TRUE;
1842 }
1843
1844 watchdog('file', 'The file permissions could not be set on %path.', array('%path' => $path), WATCHDOG_ERROR);
1845 return FALSE;
1846 }
1847
1848 /**
1849 * @} End of "defgroup file".
1850 */
1851

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.