/[drupal]/drupal/includes/file.inc
ViewVC logotype

Contents of /drupal/includes/file.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.198 - (show annotations) (download) (as text)
Fri Oct 23 01:00:52 2009 UTC (5 weeks, 2 days ago) by dries
Branch: MAIN
Changes since 1.197: +2 -2 lines
File MIME type: text/x-php
- Patch #611032 by c960657: fixed bug with wrong variable used in file_build_uri(). Added tests.
1 <?php
2 // $Id: file.inc,v 1.197 2009/10/18 18:36:24 dries 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 * - uri: URI of the file.
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 used by file_prepare_directory() -- create directory if not present.
39 */
40 define('FILE_CREATE_DIRECTORY', 1);
41
42 /**
43 * Flag used by file_prepare_directory() -- file permissions may be changed.
44 */
45 define('FILE_MODIFY_PERMISSIONS', 2);
46
47 /**
48 * Flag for dealing with existing files: Appends number until name is unique.
49 */
50 define('FILE_EXISTS_RENAME', 0);
51
52 /**
53 * Flag for dealing with existing files: Replace the existing file.
54 */
55 define('FILE_EXISTS_REPLACE', 1);
56
57 /**
58 * Flag for dealing with existing files: Do nothing and return FALSE.
59 */
60 define('FILE_EXISTS_ERROR', 2);
61
62 /**
63 * File status -- This bit in the status indicates that the file is permanent
64 * and should not be deleted during file garbage collection process. Temporary
65 * files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron
66 * runs.
67 */
68 define('FILE_STATUS_PERMANENT', 1);
69
70 /**
71 * Methods to manage a registry of stream wrappers.
72 */
73
74 /**
75 * Drupal stream wrapper registry.
76 *
77 * A stream wrapper is an abstraction of a file system that allows Drupal to
78 * use the same set of methods to access both local files and remote resources.
79 *
80 * Provide a facility for managing and querying user-defined stream wrappers
81 * in PHP. PHP's internal stream_get_wrappers() doesn't return the class
82 * registered to handle a stream, which we need to be able to find the handler
83 * for class instantiation.
84 *
85 * If a module registers a scheme that is already registered with PHP, the
86 * existing scheme will be unregistered and replaced with the specified class.
87 *
88 * A stream is referenced as "scheme://target".
89 *
90 * @return
91 * Returns the entire Drupal stream wrapper registry.
92 * @see hook_stream_wrappers()
93 * @see hook_stream_wrappers_alter()
94 */
95 function file_get_stream_wrappers() {
96 $wrappers = &drupal_static(__FUNCTION__);
97
98 if (!isset($wrappers)) {
99 $wrappers = module_invoke_all('stream_wrappers');
100 drupal_alter('stream_wrappers', $wrappers);
101 $existing = stream_get_wrappers();
102 foreach ($wrappers as $scheme => $info) {
103 // We only register classes that implement our interface.
104 if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
105 // Record whether we are overriding an existing scheme.
106 if (in_array($scheme, $existing, TRUE)) {
107 $wrappers[$scheme]['override'] = TRUE;
108 stream_wrapper_unregister($scheme);
109 }
110 else {
111 $wrappers[$scheme]['override'] = FALSE;
112 }
113 stream_wrapper_register($scheme, $info['class']);
114 }
115 }
116 }
117 return $wrappers;
118 }
119
120 /**
121 * Returns the stream wrapper class name for a given scheme.
122 *
123 * @param $scheme
124 * Stream scheme.
125 * @return
126 * Return string if a scheme has a registered handler, or FALSE.
127 */
128 function file_stream_wrapper_get_class($scheme) {
129 $wrappers = file_get_stream_wrappers();
130 return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
131 }
132
133 /**
134 * Returns the scheme of a URI (e.g. a stream).
135 *
136 * @param $uri
137 * A stream, referenced as "scheme://target".
138 * @return
139 * A string containing the name of the scheme, or FALSE if none. For example,
140 * the URI "public://example.txt" would return "public".
141 */
142 function file_uri_scheme($uri) {
143 $data = explode('://', $uri, 2);
144
145 return count($data) == 2 ? $data[0] : FALSE;
146 }
147
148 /**
149 * Check that the scheme of a stream URI is valid.
150 *
151 * Confirms that there is a registered stream handler for the provided scheme
152 * and that it is callable. This is useful if you want to confirm a valid
153 * scheme without creating a new instance of the registered handler.
154 *
155 * @param $scheme
156 * A URI scheme, a stream is referenced as "scheme://target".
157 * @return
158 * Returns TRUE if the string is the name of a validated stream,
159 * or FALSE if the scheme does not have a registered handler.
160 */
161 function file_stream_wrapper_valid_scheme($scheme) {
162 // Does the scheme have a registered handler that is callable?
163 $class = file_stream_wrapper_get_class($scheme);
164 if (class_exists($class)) {
165 return TRUE;
166 }
167 else {
168 return FALSE;
169 }
170 }
171
172 /**
173 * Returns the target of a URI (e.g. a stream).
174 *
175 * @param $uri
176 * A stream, referenced as "scheme://target".
177 * @return
178 * A string containing the target (path), or FALSE if none.
179 * For example, the URI "public://sample/test.txt" would return
180 * "sample/test.txt".
181 */
182 function file_uri_target($uri) {
183 $data = explode('://', $uri, 2);
184
185 if (count($data) != 2) {
186 return FALSE;
187 }
188
189 // Remove erroneous beginning forward slash.
190 $data[1] = ltrim($data[1], '\/');
191
192 return $data[1];
193 }
194
195 /**
196 * Normalizes a URI by making it syntactically correct.
197 *
198 * A stream is referenced as "scheme://target".
199 *
200 * The following actions are taken:
201 * - Remove all occurrences of the wrapper's directory path
202 * - Remove trailing slashes from target
203 * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
204 *
205 * @param $uri
206 * String reference containing the URI to normalize.
207 * @return
208 * The normalized URI.
209 */
210 function file_stream_wrapper_uri_normalize($uri) {
211 $scheme = file_uri_scheme($uri);
212
213 if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
214 $target = file_uri_target($uri);
215
216 // Remove all occurrences of the wrapper's directory path.
217 $directory_path = file_stream_wrapper_get_instance_by_scheme($scheme)->getDirectoryPath();
218 $target = str_replace($directory_path, '', $target);
219
220 // Trim trailing slashes from target.
221 $target = rtrim($target, '/');
222
223 // Trim erroneous leading slashes from target.
224 $uri = $scheme . '://' . ltrim($target, '/');
225 }
226 return $uri;
227 }
228
229 /**
230 * Returns a reference to the stream wrapper class responsible for a given URI (stream).
231 *
232 * The scheme determines the stream wrapper class that should be
233 * used by consulting the stream wrapper registry.
234 *
235 * @param $uri
236 * A stream, referenced as "scheme://target".
237 * @return
238 * Returns a new stream wrapper object appropriate for the given URI or FALSE
239 * if no registered handler could be found. For example, a URI of
240 * "private://example.txt" would return a new private stream wrapper object
241 * (DrupalPrivateStreamWrapper).
242 */
243 function file_stream_wrapper_get_instance_by_uri($uri) {
244 $scheme = file_uri_scheme($uri);
245 $class = file_stream_wrapper_get_class($scheme);
246 if (class_exists($class)) {
247 $instance = new $class;
248 $instance->setUri($uri);
249 return $instance;
250 }
251 else {
252 return FALSE;
253 }
254 }
255
256 /**
257 * Returns a reference to the stream wrapper class responsible for a given scheme.
258 *
259 * This helper method returns a stream instance using a scheme. That is, the
260 * passed string does not contain a "://". For example, "public" is a scheme
261 * but "public://" is a URI (stream). This is because the later contains both
262 * a scheme and target despite target being empty.
263 *
264 * Note: the instance URI will be initialized to "scheme://" so that you can
265 * make the customary method calls as if you had retrieved an instance by URI.
266 *
267 * @param $scheme
268 * If the stream was "public://target", "public" would be the scheme.
269 * @return
270 * Returns a new stream wrapper object appropriate for the given $scheme.
271 * For example, for the public scheme a stream wrapper object
272 * (DrupalPublicStreamWrapper).
273 * FALSE is returned if no registered handler could be found.
274 */
275 function file_stream_wrapper_get_instance_by_scheme($scheme) {
276 $class = file_stream_wrapper_get_class($scheme);
277 if (class_exists($class)) {
278 $instance = new $class;
279 $instance->setUri($scheme . '://');
280 return $instance;
281 }
282 else {
283 return FALSE;
284 }
285 }
286
287 /**
288 * Creates a web-accessible URL for a stream to an external or local file.
289 *
290 * Compatibility: normal paths and stream wrappers.
291 * @see http://drupal.org/node/515192
292 *
293 * There are two kinds of local files:
294 * - "created files", i.e. those in the files directory (which is stored in
295 * the file_directory_path variable and can be retrieved using
296 * file_directory_path()). These are files that have either been uploaded by
297 * users or were generated automatically (for example through CSS
298 * aggregation).
299 * - "shipped files", i.e. those outside of the files directory, which ship as
300 * part of Drupal core or contributed modules or themes.
301 *
302 * @param $uri
303 * The URI to a file for which we need an external URL, or the path to a
304 * shipped file.
305 * @return
306 * A string containing a URL that may be used to access the file.
307 * If the provided string already contains a preceding 'http', nothing is done
308 * and the same string is returned. If a valid stream wrapper could not be
309 * found to generate an external URL, then FALSE will be returned.
310 */
311 function file_create_url($uri) {
312 // Allow the URI to be altered, e.g. to serve a file from a CDN or static
313 // file server.
314 drupal_alter('file_url', $uri);
315
316 $scheme = file_uri_scheme($uri);
317
318 if (!$scheme) {
319 // If this is not a properly formatted stream, then it is a shipped file.
320 // Therefor, return the URI with the base URL prepended.
321 return $GLOBALS['base_url'] . '/' . $uri;
322 }
323 elseif ($scheme == 'http' || $scheme == 'https') {
324 // Check for http so that we don't have to implement getExternalUrl() for
325 // the http wrapper.
326 return $uri;
327 }
328 else {
329 // Attempt to return an external URL using the appropriate wrapper.
330 if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
331 return $wrapper->getExternalUrl();
332 }
333 else {
334 return FALSE;
335 }
336 }
337 }
338
339 /**
340 * Check that the directory exists and is writable.
341 *
342 * Directories need to have execute permissions to be considered a directory by
343 * FTP servers, etc.
344 *
345 * @param &$directory
346 * A string reference containing the name of a directory path or URI. A
347 * trailing slash will be trimmed from a path.
348 * @param $options
349 * A bitmask to indicate if the directory should be created if it does
350 * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
351 * (FILE_MODIFY_PERMISSIONS).
352 * @return
353 * TRUE if the directory exists (or was created) and is writable. FALSE
354 * otherwise.
355 */
356 function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
357 if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
358 // Only trim if we're not dealing with a stream.
359 $directory = rtrim($directory, '/\\');
360 }
361
362 // Check if directory exists.
363 if (!is_dir($directory)) {
364 // Let mkdir() recursively create directories and use the default directory
365 // permissions.
366 if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
367 return drupal_chmod($directory);
368 }
369 return FALSE;
370 }
371 // The directory exists, so check to see if it is writable.
372 $writable = is_writable($directory);
373 if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
374 return drupal_chmod($directory);
375 }
376
377 return $writable;
378 }
379
380 /**
381 * If missing, create a .htaccess file in each Drupal files directory.
382 */
383 function file_ensure_htaccess() {
384 file_create_htaccess('public://', FALSE);
385 file_create_htaccess('private://', TRUE);
386 file_create_htaccess('temporary://', TRUE);
387 }
388
389 /**
390 * Creates an .htaccess file in the given directory.
391 *
392 * @param $directory
393 * The directory.
394 * @param $private
395 * FALSE indicates that $directory should be an open and public directory.
396 * The default is TRUE which indicates a private and protected directory.
397 */
398 function file_create_htaccess($directory, $private = TRUE) {
399 if (file_uri_scheme($directory)) {
400 $directory = file_stream_wrapper_uri_normalize($directory);
401 }
402 else {
403 $directory = rtrim($directory, '/\\');
404 }
405 $htaccess_path = $directory . '/.htaccess';
406
407 if (file_exists($htaccess_path)) {
408 // Short circuit if the .htaccess file already exists.
409 return;
410 }
411
412 if ($private) {
413 // Private .htaccess file.
414 $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks";
415 }
416 else {
417 // Public .htaccess file.
418 $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
419 }
420
421 // Write the .htaccess file.
422 if (file_put_contents($htaccess_path, $htaccess_lines)) {
423 drupal_chmod($htaccess_path, 0444);
424 }
425 else {
426 $variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
427 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);
428 }
429 }
430
431 /**
432 * Load file objects from the database.
433 *
434 * @param $fids
435 * An array of file IDs.
436 * @param $conditions
437 * An array of conditions to match against the {files} table. These
438 * should be supplied in the form array('field_name' => 'field_value').
439 *
440 * @return
441 * An array of file objects, indexed by fid.
442 *
443 * @see hook_file_load()
444 * @see file_load()
445 */
446 function file_load_multiple($fids = array(), $conditions = array()) {
447 return entity_load('file', $fids, $conditions);
448 }
449
450 /**
451 * Load a file object from the database.
452 *
453 * @param $fid
454 * A file ID.
455 * @return
456 * A file object.
457 *
458 * @see hook_file_load()
459 * @see file_load_multiple()
460 */
461 function file_load($fid) {
462 $files = file_load_multiple(array($fid), array());
463 return reset($files);
464 }
465
466 /**
467 * Save a file object to the database.
468 *
469 * If the $file->fid is not set a new record will be added. Re-saving an
470 * existing file will not change its status.
471 *
472 * @param $file
473 * A file object returned by file_load().
474 * @return
475 * The updated file object.
476 *
477 * @see hook_file_insert()
478 * @see hook_file_update()
479 */
480 function file_save(stdClass $file) {
481 $file->timestamp = REQUEST_TIME;
482 $file->filesize = filesize($file->uri);
483
484 if (empty($file->fid)) {
485 drupal_write_record('file', $file);
486 // Inform modules about the newly added file.
487 module_invoke_all('file_insert', $file);
488 }
489 else {
490 drupal_write_record('file', $file, 'fid');
491 // Inform modules that the file has been updated.
492 module_invoke_all('file_update', $file);
493 }
494
495 return $file;
496 }
497
498 /**
499 * Copy a file to a new location and adds a file record to the database.
500 *
501 * This function should be used when manipulating files that have records
502 * stored in the database. This is a powerful function that in many ways
503 * performs like an advanced version of copy().
504 * - Checks if $source and $destination are valid and readable/writable.
505 * - Checks that $source is not equal to $destination; if they are an error
506 * is reported.
507 * - If file already exists in $destination either the call will error out,
508 * replace the file or rename the file based on the $replace parameter.
509 * - Adds the new file to the files database. If the source file is a
510 * temporary file, the resulting file will also be a temporary file.
511 * @see file_save_upload() for details on temporary files.
512 *
513 * @param $source
514 * A file object.
515 * @param $destination
516 * A string containing the destination that $source should be copied to.
517 * This should be a stream wrapper URI. If this value is omitted, Drupal's
518 * public files scheme will be used, "public://".
519 * @param $replace
520 * Replace behavior when the destination file already exists:
521 * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
522 * the destination name exists then its database entry will be updated. If
523 * no database entry is found then a new one will be created.
524 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
525 * unique.
526 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
527 * @return
528 * File object if the copy is successful, or FALSE in the event of an error.
529 *
530 * @see file_unmanaged_copy()
531 * @see hook_file_copy()
532 */
533 function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
534 if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
535 $file = clone $source;
536 $file->fid = NULL;
537 $file->uri = $uri;
538 $file->filename = basename($uri);
539 // If we are replacing an existing file re-use its database record.
540 if ($replace == FILE_EXISTS_REPLACE) {
541 $existing_files = file_load_multiple(array(), array('uri' => $uri));
542 if (count($existing_files)) {
543 $existing = reset($existing_files);
544 $file->fid = $existing->fid;
545 $file->filename = $existing->filename;
546 }
547 }
548 // If we are renaming around an existing file (rather than a directory),
549 // use its basename for the filename.
550 elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
551 $file->filename = basename($destination);
552 }
553
554 $file = file_save($file);
555
556 // Inform modules that the file has been copied.
557 module_invoke_all('file_copy', $file, $source);
558
559 return $file;
560 }
561 return FALSE;
562 }
563
564 /**
565 * Copy a file to a new location without calling any hooks or making any
566 * changes to the database.
567 *
568 * This is a powerful function that in many ways performs like an advanced
569 * version of copy().
570 * - Checks if $source and $destination are valid and readable/writable.
571 * - Checks that $source is not equal to $destination; if they are an error
572 * is reported.
573 * - If file already exists in $destination either the call will error out,
574 * replace the file or rename the file based on the $replace parameter.
575 *
576 * @param $source
577 * A string specifying the filepath or URI of the original file.
578 * @param $destination
579 * A URI containing the destination that $source should be copied to. If
580 * NULL the default scheme will be used as the destination.
581 * @param $replace
582 * Replace behavior when the destination file already exists:
583 * - FILE_EXISTS_REPLACE - Replace the existing file.
584 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
585 * unique.
586 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
587 * @return
588 * The path to the new file, or FALSE in the event of an error.
589 *
590 * @see file_copy()
591 */
592 function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
593 $original_source = $source;
594 $original_destination = $destination;
595
596 // Assert that the source file actually exists.
597 $source = drupal_realpath($source);
598 if (!file_exists($source)) {
599 // @todo Replace drupal_set_message() calls with exceptions instead.
600 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');
601 return FALSE;
602 }
603
604 // Build a destination URI if necessary.
605 if (!isset($destination)) {
606 $destination = file_build_uri(basename($source));
607 }
608
609 // Assert that the destination contains a valid stream.
610 $destination_scheme = file_uri_scheme($destination);
611 if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
612 drupal_set_message(t('The specified file %file could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_unmanaged_copy() or a missing stream wrapper.', array('%file' => $original_source, '%destination' => $destination)), 'error');
613 }
614
615 // Prepare the destination directory.
616 if (file_prepare_directory($destination)) {
617 // The destination is already a directory, so append the source basename.
618 $destination = file_stream_wrapper_uri_normalize($destination . '/' . basename($source));
619 }
620 else {
621 // Perhaps $destination is a dir/file?
622 $dirname = drupal_dirname($destination);
623 if (!file_prepare_directory($dirname)) {
624 // The destination is not valid.
625 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' => $destination)), 'error');
626 return FALSE;
627 }
628 }
629
630 // Determine whether we can perform this operation based on overwrite rules.
631 $destination = file_destination($destination, $replace);
632 if ($destination === FALSE) {
633 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' => $destination)), 'error');
634 return FALSE;
635 }
636
637 // Assert that the source and destination filenames are not the same.
638 if (drupal_realpath($source) == drupal_realpath($destination)) {
639 drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
640 return FALSE;
641 }
642 // Make sure the .htaccess files are present.
643 file_ensure_htaccess();
644 // Perform the copy operation.
645 if (!@copy($source, $destination)) {
646 drupal_set_message(t('The specified file %file could not be copied.', array('%file' => $source)), 'error');
647 return FALSE;
648 }
649
650 // Set the permissions on the new file.
651 drupal_chmod($destination);
652
653 return $destination;
654 }
655
656 /**
657 * Given a relative path, construct a URI into Drupal's default files location.
658 */
659 function file_build_uri($path) {
660 $uri = variable_get('file_default_scheme', 'public') . '://' . $path;
661 return file_stream_wrapper_uri_normalize($uri);
662 }
663
664 /**
665 * Determines the destination path for a file depending on how replacement of
666 * existing files should be handled.
667 *
668 * @param $destination
669 * A string specifying the desired final URI or filepath.
670 * @param $replace
671 * Replace behavior when the destination file already exists.
672 * - FILE_EXISTS_REPLACE - Replace the existing file.
673 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
674 * unique.
675 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
676 * @return
677 * The destination filepath, or FALSE if the file already exists
678 * and FILE_EXISTS_ERROR is specified.
679 */
680 function file_destination($destination, $replace) {
681 if (file_exists($destination)) {
682 switch ($replace) {
683 case FILE_EXISTS_REPLACE:
684 // Do nothing here, we want to overwrite the existing file.
685 break;
686
687 case FILE_EXISTS_RENAME:
688 $basename = basename($destination);
689 $directory = drupal_dirname($destination);
690 $destination = file_create_filename($basename, $directory);
691 break;
692
693 case FILE_EXISTS_ERROR:
694 // Error reporting handled by calling function.
695 return FALSE;
696 }
697 }
698 return $destination;
699 }
700
701 /**
702 * Move a file to a new location and update the file's database entry.
703 *
704 * Moving a file is performed by copying the file to the new location and then
705 * deleting the original.
706 * - Checks if $source and $destination are valid and readable/writable.
707 * - Performs a file move if $source is not equal to $destination.
708 * - If file already exists in $destination either the call will error out,
709 * replace the file or rename the file based on the $replace parameter.
710 * - Adds the new file to the files database.
711 *
712 * @param $source
713 * A file object.
714 * @param $destination
715 * A string containing the destination that $source should be moved to. This
716 * must be a URI matching a Drupal stream wrapper. If this value is omitted,
717 * Drupal's 'files' directory will be used.
718 * @param $replace
719 * Replace behavior when the destination file already exists:
720 * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
721 * the destination name exists then its database entry will be updated and
722 * file_delete() called on the source file after hook_file_move is called.
723 * If no database entry is found then the source files record will be
724 * updated.
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 * Resulting file object for success, or FALSE in the event of an error.
730 *
731 * @see file_unmanaged_move()
732 * @see hook_file_move()
733 */
734 function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
735 if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
736 $delete_source = FALSE;
737
738 $file = clone $source;
739 $file->uri = $uri;
740 // If we are replacing an existing file re-use its database record.
741 if ($replace == FILE_EXISTS_REPLACE) {
742 $existing_files = file_load_multiple(array(), array('uri' => $uri));
743 if (count($existing_files)) {
744 $existing = reset($existing_files);
745 $delete_source = TRUE;
746 $file->fid = $existing->fid;
747 }
748 }
749 // If we are renaming around an existing file (rather than a directory),
750 // use its basename for the filename.
751 elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
752 $file->filename = basename($destination);
753 }
754
755 $file = file_save($file);
756
757 // Inform modules that the file has been moved.
758 module_invoke_all('file_move', $file, $source);
759
760 if ($delete_source) {
761 // Try a soft delete to remove original if it's not in use elsewhere.
762 file_delete($source);
763 }
764
765 return $file;
766 }
767 return FALSE;
768 }
769
770 /**
771 * Move a file to a new location without calling any hooks or making any
772 * changes to the database.
773 *
774 * @param $source
775 * A string specifying the filepath or URI of the original file.
776 * @param $destination
777 * A string containing the destination that $source should be moved to. This
778 * must be a URI matching a Drupal stream wrapper. If this value is omitted,
779 * Drupal's 'files' directory will be used.
780 * @param $replace
781 * Replace behavior when the destination file already exists:
782 * - FILE_EXISTS_REPLACE - Replace the existing file.
783 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
784 * unique.
785 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
786 * @return
787 * The URI of the moved file, or FALSE in the event of an error.
788 *
789 * @see file_move()
790 */
791 function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
792 $filepath = file_unmanaged_copy($source, $destination, $replace);
793 if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
794 return FALSE;
795 }
796 return $filepath;
797 }
798
799 /**
800 * Modify a filename as needed for security purposes.
801 *
802 * Dangerous file names will be altered; for instance, the file name
803 * "exploit.php.pps" will become "exploit.php_.pps". All extensions that are
804 * between 2 and 5 characters in length, internal to the file name, and not
805 * included in $extensions will be altered by adding an underscore. If variable
806 * 'allow_insecure_uploads' evaluates to TRUE, no alterations will be made.
807 *
808 * @param $filename
809 * File name to modify.
810 * @param $extensions
811 * A space-separated list of extensions that should not be altered.
812 * @param $alerts
813 * If TRUE, drupal_set_message() will be called to display a message if the
814 * file name was changed.
815 *
816 * @return
817 * The potentially modified $filename.
818 */
819 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
820 $original = $filename;
821
822 // Allow potentially insecure uploads for very savvy users and admin
823 if (!variable_get('allow_insecure_uploads', 0)) {
824 $whitelist = array_unique(explode(' ', trim($extensions)));
825
826 // Split the filename up by periods. The first part becomes the basename
827 // the last part the final extension.
828 $filename_parts = explode('.', $filename);
829 $new_filename = array_shift($filename_parts); // Remove file basename.
830 $final_extension = array_pop($filename_parts); // Remove final extension.
831
832 // Loop through the middle parts of the name and add an underscore to the
833 // end of each section that could be a file extension but isn't in the list
834 // of allowed extensions.
835 foreach ($filename_parts as $filename_part) {
836 $new_filename .= '.' . $filename_part;
837 if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
838 $new_filename .= '_';
839 }
840 }
841 $filename = $new_filename . '.' . $final_extension;
842
843 if ($alerts && $original != $filename) {
844 drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
845 }
846 }
847
848 return $filename;
849 }
850
851 /**
852 * Undo the effect of upload_munge_filename().
853 *
854 * @param $filename
855 * String with the filename to be unmunged.
856 * @return
857 * An unmunged filename string.
858 */
859 function file_unmunge_filename($filename) {
860 return str_replace('_.', '.', $filename);
861 }
862
863 /**
864 * Create a full file path from a directory and filename.
865 *
866 * If a file with the specified name already exists, an alternative will be
867 * used.
868 *
869 * @param $basename
870 * String filename
871 * @param $directory
872 * String containing the directory or parent URI.
873 * @return
874 * File path consisting of $directory and a unique filename based off
875 * of $basename.
876 */
877 function file_create_filename($basename, $directory) {
878 // A URI or path may already have a trailing slash or look like "public://".
879 if (substr($directory, -1) == '/') {
880 $separator = '';
881 }
882 else {
883 $separator = '/';
884 }
885
886 $destination = $directory . $separator . $basename;
887
888 if (file_exists($destination)) {
889 // Destination file already exists, generate an alternative.
890 $pos = strrpos($basename, '.');
891 if ($pos !== FALSE) {
892 $name = substr($basename, 0, $pos);
893 $ext = substr($basename, $pos);
894 }
895 else {
896 $name = $basename;
897 $ext = '';
898 }
899
900 $counter = 0;
901 do {
902 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
903 } while (file_exists($destination));
904 }
905
906 return $destination;
907 }
908
909 /**
910 * Delete a file and its database record.
911 *
912 * If the $force parameter is not TRUE hook_file_references() will be called
913 * to determine if the file is being used by any modules. If the file is being
914 * used is the delete will be canceled.
915 *
916 * @param $file
917 * A file object.
918 * @param $force
919 * Boolean indicating that the file should be deleted even if
920 * hook_file_references() reports that the file is in use.
921 * @return mixed
922 * TRUE for success, FALSE in the event of an error, or an array if the file
923 * is being used by another module. The array keys are the module's name and
924 * the values are the number of references.
925 *
926 * @see file_unmanaged_delete()
927 * @see hook_file_references()
928 * @see hook_file_delete()
929 */
930 function file_delete(stdClass $file, $force = FALSE) {
931 // If any module returns a value from the reference hook, the file will not
932 // be deleted from Drupal, but file_delete will return a populated array that
933 // tests as TRUE.
934 if (!$force && ($references = module_invoke_all('file_references', $file))) {
935 return $references;
936 }
937
938 // Let other modules clean up any references to the deleted file.
939 module_invoke_all('file_delete', $file);
940
941 // Make sure the file is deleted before removing its row from the
942 // database, so UIs can still find the file in the database.
943 if (file_unmanaged_delete($file->uri)) {
944 db_delete('file')->condition('fid', $file->fid)->execute();
945 return TRUE;
946 }
947 return FALSE;
948 }
949
950 /**
951 * Delete a file without calling any hooks or making any changes to the
952 * database.
953 *
954 * This function should be used when the file to be deleted does not have an
955 * entry recorded in the files table.
956 *
957 * @param $path
958 * A string containing a file path or (streamwrapper) URI.
959 * @return
960 * TRUE for success or path does not exist, or FALSE in the event of an
961 * error.
962 *
963 * @see file_delete()
964 * @see file_unmanaged_delete_recursive()
965 */
966 function file_unmanaged_delete($path) {
967 // Resolve streamwrapper URI to local path.
968 $path = drupal_realpath($path);
969 if (is_dir($path)) {
970 watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
971 return FALSE;
972 }
973 if (is_file($path)) {
974 return unlink($path);
975 }
976 // Return TRUE for non-existent file, but log that nothing was actually
977 // deleted, as the current state is the indended result.
978 if (!file_exists($path)) {
979 watchdog('file', 'The file %path was not deleted, because it does not exist.', array('%path' => $path), WATCHDOG_NOTICE);
980 return TRUE;
981 }
982 // We cannot handle anything other than files and directories. Log an error
983 // for everything else (sockets, symbolic links, etc).
984 watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), WATCHDOG_ERROR);
985 return FALSE;
986 }
987
988 /**
989 * Recursively delete all files and directories in the specified filepath.
990 *
991 * If the specified path is a directory then the function will call itself
992 * recursively to process the contents. Once the contents have been removed the
993 * directory will also be removed.
994 *
995 * If the specified path is a file then it will be passed to
996 * file_unmanaged_delete().
997 *
998 * Note that this only deletes visible files with write permission.
999 *
1000 * @param $path
1001 * A string containing eiher an URI or a file or directory path.
1002 * @return
1003 * TRUE for success or if path does not exist, FALSE in the event of an
1004 * error.
1005 *
1006 * @see file_unmanaged_delete()
1007 */
1008 function file_unmanaged_delete_recursive($path) {
1009 // Resolve streamwrapper URI to local path.
1010 $path = drupal_realpath($path);
1011 if (is_dir($path)) {
1012 $dir = dir($path);
1013 while (($entry = $dir->read()) !== FALSE) {
1014 if ($entry == '.' || $entry == '..') {
1015 continue;
1016 }
1017 $entry_path = $path . '/' . $entry;
1018 file_unmanaged_delete_recursive($entry_path);
1019 }
1020 $dir->close();
1021 return rmdir($path);
1022 }
1023 return file_unmanaged_delete($path);
1024 }
1025
1026 /**
1027 * Determine total disk space used by a single user or the whole filesystem.
1028 *
1029 * @param $uid
1030 * Optional. A user id, specifying NULL returns the total space used by all
1031 * non-temporary files.
1032 * @param $status
1033 * Optional. File Status to return. Combine with a bitwise OR(|) to return
1034 * multiple statuses. The default status is FILE_STATUS_PERMANENT.
1035 * @return
1036 * An integer containing the number of bytes used.
1037 */
1038 function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
1039 $query = db_select('file', 'f');
1040 // Use separate placeholders for the status to avoid a bug in some versions
1041 // of PHP. @see http://drupal.org/node/352956
1042 $query->where('f.status & :status1 = :status2', array(':status1' => $status, ':status2' => $status));
1043 $query->addExpression('SUM(f.filesize)', 'filesize');
1044 if (!is_null($uid)) {
1045 $query->condition('f.uid', $uid);
1046 }
1047 return $query->execute()->fetchField();
1048 }
1049
1050 /**
1051 * Saves a file upload to a new location.
1052 *
1053 * The file will be added to the {file} table as a temporary file. Temporary
1054 * files are periodically cleaned. To make the file a permanent file, assign
1055 * the status and use file_save() to save the changes.
1056 *
1057 * @param $source
1058 * A string specifying the filepath or URI of the uploaded file to save.
1059 * @param $validators
1060 * An optional, associative array of callback functions used to validate the
1061 * file. See file_validate() for a full discussion of the array format.
1062 * @param $destination
1063 * A string containing the URI $source should be copied to. Defaults to
1064 * "temporary://".
1065 * @param $replace
1066 * Replace behavior when the destination file already exists:
1067 * - FILE_EXISTS_REPLACE: Replace the existing file.
1068 * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is
1069 * unique.
1070 * - FILE_EXISTS_ERROR: Do nothing and return FALSE.
1071 * @return
1072 * An object containing the file information if the upload succeeded, FALSE
1073 * in the event of an error, or NULL if no file was uploaded. The
1074 * documentation for the "File interface" group, which you can find under
1075 * Related topics, or the header at the top of this file, documents the
1076 * components of a file object. In addition to the standard components,
1077 * this function adds:
1078 * - source: Path to the file before it is moved.
1079 * - destination: Path to the file after it is moved (same as 'uri').
1080 */
1081 function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
1082 global $user;
1083 static $upload_cache;
1084
1085 // Return cached objects without processing since the file will have
1086 // already been processed and the paths in _FILES will be invalid.
1087 if (isset($upload_cache[$source])) {
1088 return $upload_cache[$source];
1089 }
1090
1091 // Make sure there's an upload to process.
1092 if (empty($_FILES['files']['name'][$source])) {
1093 return NULL;
1094 }
1095
1096 // Check for file upload errors and return FALSE if a lower level system
1097 // error occurred. For a complete list of errors:
1098 // @see http://php.net/manual/en/features.file-upload.errors.php
1099 switch ($_FILES['files']['error'][$source]) {
1100 case UPLOAD_ERR_INI_SIZE:
1101 case UPLOAD_ERR_FORM_SIZE:
1102 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');
1103 return FALSE;
1104
1105 case UPLOAD_ERR_PARTIAL:
1106 case UPLOAD_ERR_NO_FILE:
1107 drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source])), 'error');
1108 return FALSE;
1109
1110 case UPLOAD_ERR_OK:
1111 // Final check that this is a valid upload, if it isn't, use the
1112 // default error handler.
1113 if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
1114 break;
1115 }
1116
1117 // Unknown error
1118 default:
1119 drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error');
1120 return FALSE;
1121 }
1122
1123 // Build the list of non-munged extensions.
1124 // @todo: this should not be here. we need to figure out the right place.
1125 $extensions = '';
1126 foreach ($user->roles as $rid => $name) {
1127 $extensions .= ' ' . variable_get("upload_extensions_$rid",
1128 variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
1129 }
1130
1131 // Begin building file object.
1132 $file = new stdClass();
1133 $file->uid = $user->uid;
1134 $file->status = 0;
1135 $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions);
1136 $file->uri = $_FILES['files']['tmp_name'][$source];
1137 $file->filemime = file_get_mimetype($file->filename);
1138 $file->filesize = $_FILES['files']['size'][$source];
1139
1140 // Rename potentially executable files, to help prevent exploits.
1141 if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
1142 $file->filemime = 'text/plain';
1143 $file->uri .= '.txt';
1144 $file->filename .= '.txt';
1145 }
1146
1147 // If the destination is not provided, use the temporary directory.
1148 if (empty($destination)) {
1149 $destination = 'temporary://';
1150 }
1151
1152 // Assert that the destination contains a valid stream.
1153 $destination_scheme = file_uri_scheme($destination);
1154 if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
1155 drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
1156 return FALSE;
1157 }
1158
1159 $file->source = $source;
1160 // A URI may already have a trailing slash or look like "public://".
1161 if (substr($destination, -1) != '/') {
1162 $destination .= '/';
1163 }
1164 $file->destination = file_destination($destination . $file->filename, $replace);
1165 // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
1166 // there's an existing file so we need to bail.
1167 if ($file->destination === FALSE) {
1168 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');
1169 return FALSE;
1170 }
1171
1172 // Add in our check of the the file name length.
1173 $validators['file_validate_name_length'] = array();
1174
1175 // Call the validation functions specified by this function's caller.
1176 $errors = file_validate($file, $validators);
1177
1178 // Check for errors.
1179 if (!empty($errors)) {
1180 $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
1181 if (count($errors) > 1) {
1182 $message .= theme('item_list', array('items' => $errors));
1183 }
1184 else {
1185 $message .= ' ' . array_pop($errors);
1186 }
1187 form_set_error($source, $message);
1188 return FALSE;
1189 }
1190
1191 // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
1192 // directory. This overcomes open_basedir restrictions for future file
1193 // operations.
1194 $file->uri = $file->destination;
1195 if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) {
1196 form_set_error($source, t('File upload error. Could not move uploaded file.'));
1197 watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
1198 return FALSE;
1199 }
1200
1201 // Set the permissions on the new file.
1202 drupal_chmod($file->uri);
1203
1204 // If we are replacing an existing file re-use its database record.
1205 if ($replace == FILE_EXISTS_REPLACE) {
1206 $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
1207 if (count($existing_files)) {
1208 $existing = reset($existing_files);
1209 $file->fid = $existing->fid;
1210 }
1211 }
1212
1213 // If we made it this far it's safe to record this file in the database.
1214 if ($file = file_save($file)) {
1215 // Add file to the cache.
1216 $upload_cache[$source] = $file;
1217 return $file;
1218 }
1219 return FALSE;
1220 }
1221
1222
1223 /**
1224 * Check that a file meets the criteria specified by the validators.
1225 *
1226 * After executing the validator callbacks specified hook_file_validate() will
1227 * also be called to allow other modules to report errors about the file.
1228 *
1229 * @param $file
1230 * A Drupal file object.
1231 * @param $validators
1232 * An optional, associative array of callback functions used to validate the
1233 * file. The keys are function names and the values arrays of callback
1234 * parameters which will be passed in after the file object. The
1235 * functions should return an array of error messages; an empty array
1236 * indicates that the file passed validation. The functions will be called in
1237 * the order specified.
1238 * @return
1239 * An array contaning validation error messages.
1240 *
1241 * @see hook_file_validate()
1242 */
1243 function file_validate(stdClass &$file, $validators = array()) {
1244 // Call the validation functions specified by this function's caller.
1245 $errors = array();
1246 foreach ($validators as $function => $args) {
1247 if (function_exists($function)) {
1248 array_unshift($args, $file);
1249 $errors = array_merge($errors, call_user_func_array($function, $args));
1250 }
1251 }
1252
1253 // Let other modules perform validation on the new file.
1254 return array_merge($errors, module_invoke_all('file_validate', $file));
1255 }
1256
1257 /**
1258 * Check for files with names longer than we can store in the database.
1259 *
1260 * @param $file
1261 * A Drupal file object.
1262 * @return
1263 * An array. If the file name is too long, it will contain an error message.
1264 */
1265 function file_validate_name_length(stdClass $file) {
1266 $errors = array();
1267
1268 if (empty($file->filename)) {
1269 $errors[] = t("The file's name is empty. Please give a name to the file.");
1270 }
1271 if (strlen($file->filename) > 240) {
1272 $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
1273 }
1274 return $errors;
1275 }
1276
1277 /**
1278 * Check that the filename ends with an allowed extension.
1279 *
1280 * @param $file
1281 * A Drupal file object.
1282 * @param $extensions
1283 * A string with a space separated list of allowed extensions.
1284 * @return
1285 * An array. If the file extension is not allowed, it will contain an error
1286 * message.
1287 *
1288 * @see hook_file_validate()
1289 */
1290 function file_validate_extensions(stdClass $file, $extensions) {
1291 $errors = array();
1292
1293 $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
1294 if (!preg_match($regex, $file->filename)) {
1295 $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
1296 }
1297 return $errors;
1298 }
1299
1300 /**
1301 * Check that the file's size is below certain limits.
1302 *
1303 * This check is not enforced for the user #1.
1304 *
1305 * @param $file
1306 * A Drupal file object.
1307 * @param $file_limit
1308 * An integer specifying the maximum file size in bytes. Zero indicates that
1309 * no limit should be enforced.
1310 * @param $user_limit
1311 * An integer specifying the maximum number of bytes the user is allowed.
1312 * Zero indicates that no limit should be enforced.
1313 * @return
1314 * An array. If the file size exceeds limits, it will contain an error
1315 * message.
1316 *
1317 * @see hook_file_validate()
1318 */
1319 function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
1320 global $user;
1321
1322 $errors = array();
1323
1324 // Bypass validation for uid = 1.
1325 if ($user->uid != 1) {
1326 if ($file_limit && $file->filesize > $file_limit) {
1327 $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit)));
1328 }
1329
1330 // Save a query by only calling file_space_used() when a limit is provided.
1331 if ($user_limit && (file_space_used($user->uid) + $file->filesize) > $user_limit) {
1332 $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)));
1333 }
1334 }
1335 return $errors;
1336 }
1337
1338 /**
1339 * Check that the file is recognized by image_get_info() as an image.
1340 *
1341 * @param $file
1342 * A Drupal file object.
1343 * @return
1344 * An array. If the file is not an image, it will contain an error message.
1345 *
1346 * @see hook_file_validate()
1347 */
1348 function file_validate_is_image(stdClass $file) {
1349 $errors = array();
1350
1351 $info = image_get_info($file->uri);
1352 if (!$info || empty($info['extension'])) {
1353 $errors[] = t('Only JPEG, PNG and GIF images are allowed.');
1354 }
1355
1356 return $errors;
1357 }
1358
1359 /**
1360 * If the file is an image verify that its dimensions are within the specified
1361 * maximum and minimum dimensions.
1362 *
1363 * Non-image files will be ignored. If a image toolkit is available the image
1364 * will be scalled to fit within the desired maximum dimensions.
1365 *
1366 * @param $file
1367 * A Drupal file object. This function may resize the file affecting its
1368 * size.
1369 * @param $maximum_dimensions
1370 * An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
1371 * an image toolkit is installed the image will be resized down to these
1372 * dimensions. A value of 0 indicates no restriction on size, so resizing
1373 * will be attempted.
1374 * @param $minimum_dimensions
1375 * An optional string in the form WIDTHxHEIGHT. This will check that the
1376 * image meets a minimum size. A value of 0 indicates no restriction.
1377 * @return
1378 * An array. If the file is an image and did not meet the requirements, it
1379 * will contain an error message.
1380 *
1381 * @see hook_file_validate()
1382 */
1383 function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
1384 $errors = array();
1385
1386 // Check first that the file is an image.
1387 if ($info = image_get_info($file->uri)) {
1388 if ($maximum_dimensions) {
1389 // Check that it is smaller than the given dimensions.
1390 list($width, $height) = explode('x', $maximum_dimensions);
1391 if ($info['width'] > $width || $info['height'] > $height) {
1392 // Try to resize the image to fit the dimensions.
1393 if ($image = image_load($file->uri)) {
1394 image_scale($image, $width, $height);
1395 image_save($image);
1396 $file->filesize = $image->info['file_size'];
1397 drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
1398 }
1399 else {
1400 $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
1401 }
1402 }