comparison includes/file.inc @ 1:c1f4ac30525a 6.0

Drupal 6.0
author Franck Deroche <webmaster@defr.org>
date Tue, 23 Dec 2008 14:28:28 +0100
parents
children fff6d4c8c043
comparison
equal deleted inserted replaced
0:5a113a1c4740 1:c1f4ac30525a
1 <?php
2 // $Id: file.inc,v 1.121.2.1 2008/02/07 18:20:37 goba Exp $
3
4 /**
5 * @file
6 * API for handling file uploads and server file management.
7 */
8
9 /**
10 * @defgroup file File interface
11 * @{
12 * Common file handling functions.
13 */
14
15 define('FILE_DOWNLOADS_PUBLIC', 1);
16 define('FILE_DOWNLOADS_PRIVATE', 2);
17 define('FILE_CREATE_DIRECTORY', 1);
18 define('FILE_MODIFY_PERMISSIONS', 2);
19 define('FILE_EXISTS_RENAME', 0);
20 define('FILE_EXISTS_REPLACE', 1);
21 define('FILE_EXISTS_ERROR', 2);
22
23 /**
24 * A files status can be one of two values: temporary or permanent. The status
25 * for each file Drupal manages is stored in the {files} tables. If the status
26 * is temporary Drupal's file garbage collection will delete the file and
27 * remove it from the files table after a set period of time.
28 *
29 * If you wish to add custom statuses for use by contrib modules please expand as
30 * binary flags and consider the first 8 bits reserved. (0,1,2,4,8,16,32,64,128)
31 */
32 define('FILE_STATUS_TEMPORARY', 0);
33 define('FILE_STATUS_PERMANENT', 1);
34
35 /**
36 * Create the download path to a file.
37 *
38 * @param $path A string containing the path of the file to generate URL for.
39 * @return A string containing a URL that can be used to download the file.
40 */
41 function file_create_url($path) {
42 // Strip file_directory_path from $path. We only include relative paths in urls.
43 if (strpos($path, file_directory_path() .'/') === 0) {
44 $path = trim(substr($path, strlen(file_directory_path())), '\\/');
45 }
46 switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
47 case FILE_DOWNLOADS_PUBLIC:
48 return $GLOBALS['base_url'] .'/'. file_directory_path() .'/'. str_replace('\\', '/', $path);
49 case FILE_DOWNLOADS_PRIVATE:
50 return url('system/files/'. $path, array('absolute' => TRUE));
51 }
52 }
53
54 /**
55 * Make sure the destination is a complete path and resides in the file system
56 * directory, if it is not prepend the file system directory.
57 *
58 * @param $dest A string containing the path to verify. If this value is
59 * omitted, Drupal's 'files' directory will be used.
60 * @return A string containing the path to file, with file system directory
61 * appended if necessary, or FALSE if the path is invalid (i.e. outside the
62 * configured 'files' or temp directories).
63 */
64 function file_create_path($dest = 0) {
65 $file_path = file_directory_path();
66 if (!$dest) {
67 return $file_path;
68 }
69 // file_check_location() checks whether the destination is inside the Drupal files directory.
70 if (file_check_location($dest, $file_path)) {
71 return $dest;
72 }
73 // check if the destination is instead inside the Drupal temporary files directory.
74 else if (file_check_location($dest, file_directory_temp())) {
75 return $dest;
76 }
77 // Not found, try again with prefixed directory path.
78 else if (file_check_location($file_path .'/'. $dest, $file_path)) {
79 return $file_path .'/'. $dest;
80 }
81 // File not found.
82 return FALSE;
83 }
84
85 /**
86 * Check that the directory exists and is writable. Directories need to
87 * have execute permissions to be considered a directory by FTP servers, etc.
88 *
89 * @param $directory A string containing the name of a directory path.
90 * @param $mode A Boolean value to indicate if the directory should be created
91 * if it does not exist or made writable if it is read-only.
92 * @param $form_item An optional string containing the name of a form item that
93 * any errors will be attached to. This is useful for settings forms that
94 * require the user to specify a writable directory. If it can't be made to
95 * work, a form error will be set preventing them from saving the settings.
96 * @return FALSE when directory not found, or TRUE when directory exists.
97 */
98 function file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
99 $directory = rtrim($directory, '/\\');
100
101 // Check if directory exists.
102 if (!is_dir($directory)) {
103 if (($mode & FILE_CREATE_DIRECTORY) && @mkdir($directory)) {
104 drupal_set_message(t('The directory %directory has been created.', array('%directory' => $directory)));
105 @chmod($directory, 0775); // Necessary for non-webserver users.
106 }
107 else {
108 if ($form_item) {
109 form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory)));
110 }
111 return FALSE;
112 }
113 }
114
115 // Check to see if the directory is writable.
116 if (!is_writable($directory)) {
117 if (($mode & FILE_MODIFY_PERMISSIONS) && @chmod($directory, 0775)) {
118 drupal_set_message(t('The permissions of directory %directory have been changed to make it writable.', array('%directory' => $directory)));
119 }
120 else {
121 form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory)));
122 watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR);
123 return FALSE;
124 }
125 }
126
127 if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) {
128 $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
129 if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) {
130 fclose($fp);
131 chmod($directory .'/.htaccess', 0664);
132 }
133 else {
134 $variables = array('%directory' => $directory, '!htaccess' => '<br />'. nl2br(check_plain($htaccess_lines)));
135 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));
136 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);
137 }
138 }
139
140 return TRUE;
141 }
142
143 /**
144 * Checks path to see if it is a directory, or a dir/file.
145 *
146 * @param $path A string containing a file path. This will be set to the
147 * directory's path.
148 * @return If the directory is not in a Drupal writable directory, FALSE is
149 * returned. Otherwise, the base name of the path is returned.
150 */
151 function file_check_path(&$path) {
152 // Check if path is a directory.
153 if (file_check_directory($path)) {
154 return '';
155 }
156
157 // Check if path is a possible dir/file.
158 $filename = basename($path);
159 $path = dirname($path);
160 if (file_check_directory($path)) {
161 return $filename;
162 }
163
164 return FALSE;
165 }
166
167 /**
168 * Check if a file is really located inside $directory. Should be used to make
169 * sure a file specified is really located within the directory to prevent
170 * exploits.
171 *
172 * @code
173 * // Returns FALSE:
174 * file_check_location('/www/example.com/files/../../../etc/passwd', '/www/example.com/files');
175 * @endcode
176 *
177 * @param $source A string set to the file to check.
178 * @param $directory A string where the file should be located.
179 * @return 0 for invalid path or the real path of the source.
180 */
181 function file_check_location($source, $directory = '') {
182 $check = realpath($source);
183 if ($check) {
184 $source = $check;
185 }
186 else {
187 // This file does not yet exist
188 $source = realpath(dirname($source)) .'/'. basename($source);
189 }
190 $directory = realpath($directory);
191 if ($directory && strpos($source, $directory) !== 0) {
192 return 0;
193 }
194 return $source;
195 }
196
197 /**
198 * Copies a file to a new location. This is a powerful function that in many ways
199 * performs like an advanced version of copy().
200 * - Checks if $source and $dest are valid and readable/writable.
201 * - Performs a file copy if $source is not equal to $dest.
202 * - If file already exists in $dest either the call will error out, replace the
203 * file or rename the file based on the $replace parameter.
204 *
205 * @param $source A string specifying the file location of the original file.
206 * This parameter will contain the resulting destination filename in case of
207 * success.
208 * @param $dest A string containing the directory $source should be copied to.
209 * If this value is omitted, Drupal's 'files' directory will be used.
210 * @param $replace Replace behavior when the destination file already exists.
211 * - FILE_EXISTS_REPLACE - Replace the existing file
212 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
213 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
214 * @return True for success, FALSE for failure.
215 */
216 function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) {
217 $dest = file_create_path($dest);
218
219 $directory = $dest;
220 $basename = file_check_path($directory);
221
222 // Make sure we at least have a valid directory.
223 if ($basename === FALSE) {
224 $source = is_object($source) ? $source->filepath : $source;
225 drupal_set_message(t('The selected file %file could not be uploaded, because the destination %directory is not properly configured.', array('%file' => $source, '%directory' => $dest)), 'error');
226 watchdog('file system', 'The selected file %file could not be uploaded, because the destination %directory could not be found, or because its permissions do not allow the file to be written.', array('%file' => $source, '%directory' => $dest), WATCHDOG_ERROR);
227 return 0;
228 }
229
230 // Process a file upload object.
231 if (is_object($source)) {
232 $file = $source;
233 $source = $file->filepath;
234 if (!$basename) {
235 $basename = $file->filename;
236 }
237 }
238
239 $source = realpath($source);
240 if (!file_exists($source)) {
241 drupal_set_message(t('The selected file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $source)), 'error');
242 return 0;
243 }
244
245 // If the destination file is not specified then use the filename of the source file.
246 $basename = $basename ? $basename : basename($source);
247 $dest = $directory .'/'. $basename;
248
249 // Make sure source and destination filenames are not the same, makes no sense
250 // to copy it if they are. In fact copying the file will most likely result in
251 // a 0 byte file. Which is bad. Real bad.
252 if ($source != realpath($dest)) {
253 if (!$dest = file_destination($dest, $replace)) {
254 drupal_set_message(t('The selected file %file could not be copied, because a file by that name already exists in the destination.', array('%file' => $source)), 'error');
255 return FALSE;
256 }
257
258 if (!@copy($source, $dest)) {
259 drupal_set_message(t('The selected file %file could not be copied.', array('%file' => $source)), 'error');
260 return 0;
261 }
262
263 // Give everyone read access so that FTP'd users or
264 // non-webserver users can see/read these files,
265 // and give group write permissions so group members
266 // can alter files uploaded by the webserver.
267 @chmod($dest, 0664);
268 }
269
270 if (isset($file) && is_object($file)) {
271 $file->filename = $basename;
272 $file->filepath = $dest;
273 $source = $file;
274 }
275 else {
276 $source = $dest;
277 }
278
279 return 1; // Everything went ok.
280 }
281
282 /**
283 * Determines the destination path for a file depending on how replacement of
284 * existing files should be handled.
285 *
286 * @param $destination A string specifying the desired path.
287 * @param $replace Replace behavior when the destination file already exists.
288 * - FILE_EXISTS_REPLACE - Replace the existing file
289 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
290 * unique
291 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
292 * @return The destination file path or FALSE if the file already exists and
293 * FILE_EXISTS_ERROR was specified.
294 */
295 function file_destination($destination, $replace) {
296 if (file_exists($destination)) {
297 switch ($replace) {
298 case FILE_EXISTS_RENAME:
299 $basename = basename($destination);
300 $directory = dirname($destination);
301 $destination = file_create_filename($basename, $directory);
302 break;
303
304 case FILE_EXISTS_ERROR:
305 drupal_set_message(t('The selected file %file could not be copied, because a file by that name already exists in the destination.', array('%file' => $destination)), 'error');
306 return FALSE;
307 }
308 }
309 return $destination;
310 }
311
312 /**
313 * Moves a file to a new location.
314 * - Checks if $source and $dest are valid and readable/writable.
315 * - Performs a file move if $source is not equal to $dest.
316 * - If file already exists in $dest either the call will error out, replace the
317 * file or rename the file based on the $replace parameter.
318 *
319 * @param $source A string specifying the file location of the original file.
320 * This parameter will contain the resulting destination filename in case of
321 * success.
322 * @param $dest A string containing the directory $source should be copied to.
323 * If this value is omitted, Drupal's 'files' directory will be used.
324 * @param $replace Replace behavior when the destination file already exists.
325 * - FILE_EXISTS_REPLACE - Replace the existing file
326 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
327 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
328 * @return True for success, FALSE for failure.
329 */
330 function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) {
331 $path_original = is_object($source) ? $source->filepath : $source;
332
333 if (file_copy($source, $dest, $replace)) {
334 $path_current = is_object($source) ? $source->filepath : $source;
335
336 if ($path_original == $path_current || file_delete($path_original)) {
337 return 1;
338 }
339 drupal_set_message(t('The removal of the original file %file has failed.', array('%file' => $path_original)), 'error');
340 }
341 return 0;
342 }
343
344 /**
345 * Munge the filename as needed for security purposes. For instance the file
346 * name "exploit.php.pps" would become "exploit.php_.pps".
347 *
348 * @param $filename The name of a file to modify.
349 * @param $extensions A space separated list of extensions that should not
350 * be altered.
351 * @param $alerts Whether alerts (watchdog, drupal_set_message()) should be
352 * displayed.
353 * @return $filename The potentially modified $filename.
354 */
355 function file_munge_filename($filename, $extensions, $alerts = TRUE) {
356 $original = $filename;
357
358 // Allow potentially insecure uploads for very savvy users and admin
359 if (!variable_get('allow_insecure_uploads', 0)) {
360 $whitelist = array_unique(explode(' ', trim($extensions)));
361
362 // Split the filename up by periods. The first part becomes the basename
363 // the last part the final extension.
364 $filename_parts = explode('.', $filename);
365 $new_filename = array_shift($filename_parts); // Remove file basename.
366 $final_extension = array_pop($filename_parts); // Remove final extension.
367
368 // Loop through the middle parts of the name and add an underscore to the
369 // end of each section that could be a file extension but isn't in the list
370 // of allowed extensions.
371 foreach ($filename_parts as $filename_part) {
372 $new_filename .= '.'. $filename_part;
373 if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
374 $new_filename .= '_';
375 }
376 }
377 $filename = $new_filename .'.'. $final_extension;
378
379 if ($alerts && $original != $filename) {
380 drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
381 }
382 }
383
384 return $filename;
385 }
386
387 /**
388 * Undo the effect of upload_munge_filename().
389 *
390 * @param $filename string filename
391 * @return string
392 */
393 function file_unmunge_filename($filename) {
394 return str_replace('_.', '.', $filename);
395 }
396
397 /**
398 * Create a full file path from a directory and filename. If a file with the
399 * specified name already exists, an alternative will be used.
400 *
401 * @param $basename string filename
402 * @param $directory string directory
403 * @return
404 */
405 function file_create_filename($basename, $directory) {
406 $dest = $directory .'/'. $basename;
407
408 if (file_exists($dest)) {
409 // Destination file already exists, generate an alternative.
410 if ($pos = strrpos($basename, '.')) {
411 $name = substr($basename, 0, $pos);
412 $ext = substr($basename, $pos);
413 }
414 else {
415 $name = $basename;
416 }
417
418 $counter = 0;
419 do {
420 $dest = $directory .'/'. $name .'_'. $counter++ . $ext;
421 } while (file_exists($dest));
422 }
423
424 return $dest;
425 }
426
427 /**
428 * Delete a file.
429 *
430 * @param $path A string containing a file path.
431 * @return TRUE for success, FALSE for failure.
432 */
433 function file_delete($path) {
434 if (is_file($path)) {
435 return unlink($path);
436 }
437 }
438
439 /**
440 * Determine total disk space used by a single user or the whole filesystem.
441 *
442 * @param $uid
443 * An optional user id. A NULL value returns the total space used
444 * by all files.
445 */
446 function file_space_used($uid = NULL) {
447 if (isset($uid)) {
448 return (int) db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE uid = %d', $uid));
449 }
450 return (int) db_result(db_query('SELECT SUM(filesize) FROM {files}'));
451 }
452
453 /**
454 * Saves a file upload to a new location. The source file is validated as a
455 * proper upload and handled as such.
456 *
457 * The file will be added to the files table as a temporary file. Temporary files
458 * are periodically cleaned. To make the file permanent file call
459 * file_set_status() to change its status.
460 *
461 * @param $source
462 * A string specifying the name of the upload field to save.
463 * @param $validators
464 * An optional, associative array of callback functions used to validate the
465 * file. The keys are function names and the values arrays of callback
466 * parameters which will be passed in after the user and file objects. The
467 * functions should return an array of error messages, an empty array
468 * indicates that the file passed validation. The functions will be called in
469 * the order specified.
470 * @param $dest
471 * A string containing the directory $source should be copied to. If this is
472 * not provided or is not writable, the temporary directory will be used.
473 * @param $replace
474 * A boolean indicating whether an existing file of the same name in the
475 * destination directory should overwritten. A false value will generate a
476 * new, unique filename in the destination directory.
477 * @return
478 * An object containing the file information, or 0 in the event of an error.
479 */
480 function file_save_upload($source, $validators = array(), $dest = FALSE, $replace = FILE_EXISTS_RENAME) {
481 global $user;
482 static $upload_cache;
483
484 // Add in our check of the the file name length.
485 $validators['file_validate_name_length'] = array();
486
487 // Return cached objects without processing since the file will have
488 // already been processed and the paths in _FILES will be invalid.
489 if (isset($upload_cache[$source])) {
490 return $upload_cache[$source];
491 }
492
493 // If a file was uploaded, process it.
494 if (isset($_FILES['files']) && $_FILES['files']['name'][$source] && is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
495 // Check for file upload errors and return FALSE if a
496 // lower level system error occurred.
497 switch ($_FILES['files']['error'][$source]) {
498 // @see http://php.net/manual/en/features.file-upload.errors.php
499 case UPLOAD_ERR_OK:
500 break;
501
502 case UPLOAD_ERR_INI_SIZE:
503 case UPLOAD_ERR_FORM_SIZE:
504 drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $source, '%maxsize' => format_size(file_upload_max_size()))), 'error');
505 return 0;
506
507 case UPLOAD_ERR_PARTIAL:
508 case UPLOAD_ERR_NO_FILE:
509 drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $source)), 'error');
510 return 0;
511
512 // Unknown error
513 default:
514 drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $source)), 'error');
515 return 0;
516 }
517
518 // Build the list of non-munged extensions.
519 // @todo: this should not be here. we need to figure out the right place.
520 $extensions = '';
521 foreach ($user->roles as $rid => $name) {
522 $extensions .= ' '. variable_get("upload_extensions_$rid",
523 variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
524 }
525
526 // Begin building file object.
527 $file = new stdClass();
528 $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions);
529 $file->filepath = $_FILES['files']['tmp_name'][$source];
530 $file->filemime = $_FILES['files']['type'][$source];
531
532 // Rename potentially executable files, to help prevent exploits.
533 if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
534 $file->filemime = 'text/plain';
535 $file->filepath .= '.txt';
536 $file->filename .= '.txt';
537 }
538
539 // If the destination is not provided, or is not writable, then use the
540 // temporary directory.
541 if (empty($dest) || file_check_path($dest) === FALSE) {
542 $dest = file_directory_temp();
543 }
544
545 $file->source = $source;
546 $file->destination = file_destination(file_create_path($dest .'/'. $file->filename), $replace);
547 $file->filesize = $_FILES['files']['size'][$source];
548
549 // Call the validation functions.
550 $errors = array();
551 foreach ($validators as $function => $args) {
552 array_unshift($args, $file);
553 $errors = array_merge($errors, call_user_func_array($function, $args));
554 }
555
556 // Check for validation errors.
557 if (!empty($errors)) {
558 $message = t('The selected file %name could not be uploaded.', array('%name' => $file->filename));
559 if (count($errors) > 1) {
560 $message .= '<ul><li>'. implode('</li><li>', $errors) .'</li></ul>';
561 }
562 else {
563 $message .= ' '. array_pop($errors);
564 }
565 form_set_error($source, $message);
566 return 0;
567 }
568
569 // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary directory.
570 // This overcomes open_basedir restrictions for future file operations.
571 $file->filepath = $file->destination;
572 if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->filepath)) {
573 form_set_error($source, t('File upload error. Could not move uploaded file.'));
574 watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->filepath));
575 return 0;
576 }
577
578 // If we made it this far it's safe to record this file in the database.
579 $file->uid = $user->uid;
580 $file->status = FILE_STATUS_TEMPORARY;
581 $file->timestamp = time();
582 drupal_write_record('files', $file);
583
584 // Add file to the cache.
585 $upload_cache[$source] = $file;
586 return $file;
587 }
588 return 0;
589 }
590
591 /**
592 * Check for files with names longer than we can store in the database.
593 *
594 * @param $file
595 * A Drupal file object.
596 * @return
597 * An array. If the file name is too long, it will contain an error message.
598 */
599 function file_validate_name_length($file) {
600 $errors = array();
601
602 if (strlen($file->filename) > 255) {
603 $errors[] = t('Its name exceeds the 255 characters limit. Please rename the file and try again.');
604 }
605 return $errors;
606 }
607
608 /**
609 * Check that the filename ends with an allowed extension. This check is not
610 * enforced for the user #1.
611 *
612 * @param $file
613 * A Drupal file object.
614 * @param $extensions
615 * A string with a space separated
616 * @return
617 * An array. If the file extension is not allowed, it will contain an error message.
618 */
619 function file_validate_extensions($file, $extensions) {
620 global $user;
621
622 $errors = array();
623
624 // Bypass validation for uid = 1.
625 if ($user->uid != 1) {
626 $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i';
627 if (!preg_match($regex, $file->filename)) {
628 $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
629 }
630 }
631 return $errors;
632 }
633
634 /**
635 * Check that the file's size is below certain limits. This check is not
636 * enforced for the user #1.
637 *
638 * @param $file
639 * A Drupal file object.
640 * @param $file_limit
641 * An integer specifying the maximum file size in bytes. Zero indicates that
642 * no limit should be enforced.
643 * @param $$user_limit
644 * An integer specifying the maximum number of bytes the user is allowed. Zero
645 * indicates that no limit should be enforced.
646 * @return
647 * An array. If the file size exceeds limits, it will contain an error message.
648 */
649 function file_validate_size($file, $file_limit = 0, $user_limit = 0) {
650 global $user;
651
652 $errors = array();
653
654 // Bypass validation for uid = 1.
655 if ($user->uid != 1) {
656 if ($file_limit && $file->filesize > $file_limit) {
657 $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit)));
658 }
659
660 $total_size = file_space_used($user->uid) + $file->filesize;
661 if ($user_limit && $total_size > $user_limit) {
662 $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)));
663 }
664 }
665 return $errors;
666 }
667
668 /**
669 * Check that the file is recognized by image_get_info() as an image.
670 *
671 * @param $file
672 * A Drupal file object.
673 * @return
674 * An array. If the file is not an image, it will contain an error message.
675 */
676 function file_validate_is_image(&$file) {
677 $errors = array();
678
679 $info = image_get_info($file->filepath);
680 if (!$info || empty($info['extension'])) {
681 $errors[] = t('Only JPEG, PNG and GIF images are allowed.');
682 }
683
684 return $errors;
685 }
686
687 /**
688 * If the file is an image verify that its dimensions are within the specified
689 * maximum and minimum dimensions. Non-image files will be ignored.
690 *
691 * @param $file
692 * A Drupal file object. This function may resize the file affecting its size.
693 * @param $maximum_dimensions
694 * An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
695 * an image toolkit is installed the image will be resized down to these
696 * dimensions. A value of 0 indicates no restriction on size, so resizing
697 * will be attempted.
698 * @param $minimum_dimensions
699 * An optional string in the form WIDTHxHEIGHT. This will check that the image
700 * meets a minimum size. A value of 0 indicates no restriction.
701 * @return
702 * An array. If the file is an image and did not meet the requirements, it
703 * will contain an error message.
704 */
705 function file_validate_image_resolution(&$file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
706 $errors = array();
707
708 // Check first that the file is an image.
709 if ($info = image_get_info($file->filepath)) {
710 if ($maximum_dimensions) {
711 // Check that it is smaller than the given dimensions.
712 list($width, $height) = explode('x', $maximum_dimensions);
713 if ($info['width'] > $width || $info['height'] > $height) {
714 // Try to resize the image to fit the dimensions.
715 if (image_get_toolkit() && image_scale($file->filepath, $file->filepath, $width, $height)) {
716 drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
717
718 // Clear the cached filesize and refresh the image information.
719 clearstatcache();
720 $info = image_get_info($file->filepath);
721 $file->filesize = $info['file_size'];
722 }
723 else {
724 $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
725 }
726 }
727 }
728
729 if ($minimum_dimensions) {
730 // Check that it is larger than the given dimensions.
731 list($width, $height) = explode('x', $minimum_dimensions);
732 if ($info['width'] < $width || $info['height'] < $height) {
733 $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions));
734 }
735 }
736 }
737
738 return $errors;
739 }
740
741 /**
742 * Save a string to the specified destination.
743 *
744 * @param $data A string containing the contents of the file.
745 * @param $dest A string containing the destination location.
746 * @param $replace Replace behavior when the destination file already exists.
747 * - FILE_EXISTS_REPLACE - Replace the existing file
748 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
749 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
750 *
751 * @return A string containing the resulting filename or 0 on error
752 */
753 function file_save_data($data, $dest, $replace = FILE_EXISTS_RENAME) {
754 $temp = file_directory_temp();
755 // On Windows, tempnam() requires an absolute path, so we use realpath().
756 $file = tempnam(realpath($temp), 'file');
757 if (!$fp = fopen($file, 'wb')) {
758 drupal_set_message(t('The file could not be created.'), 'error');
759 return 0;
760 }
761 fwrite($fp, $data);
762 fclose($fp);
763
764 if (!file_move($file, $dest, $replace)) {
765 return 0;
766 }
767
768 return $file;
769 }
770
771 /**
772 * Set the status of a file.
773 *
774 * @param file A Drupal file object
775 * @param status A status value to set the file to.
776 * @return FALSE on failure, TRUE on success and $file->status will contain the
777 * status.
778 */
779 function file_set_status(&$file, $status) {
780 if (db_query('UPDATE {files} SET status = %d WHERE fid = %d', $status, $file->fid)) {
781 $file->status = $status;
782 return TRUE;
783 }
784 return FALSE;
785 }
786
787 /**
788 * Transfer file using http to client. Pipes a file through Drupal to the
789 * client.
790 *
791 * @param $source File to transfer.
792 * @param $headers An array of http headers to send along with file.
793 */
794 function file_transfer($source, $headers) {
795 ob_end_clean();
796
797 foreach ($headers as $header) {
798 // To prevent HTTP header injection, we delete new lines that are
799 // not followed by a space or a tab.
800 // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
801 $header = preg_replace('/\r?\n(?!\t| )/', '', $header);
802 drupal_set_header($header);
803 }
804
805 $source = file_create_path($source);
806
807 // Transfer file in 1024 byte chunks to save memory usage.
808 if ($fd = fopen($source, 'rb')) {
809 while (!feof($fd)) {
810 print fread($fd, 1024);
811 }
812 fclose($fd);
813 }
814 else {
815 drupal_not_found();
816 }
817 exit();
818 }
819
820 /**
821 * Call modules that implement hook_file_download() to find out if a file is
822 * accessible and what headers it should be transferred with. If a module
823 * returns -1 drupal_access_denied() will be returned. If one or more modules
824 * returned headers the download will start with the returned headers. If no
825 * modules respond drupal_not_found() will be returned.
826 */
827 function file_download() {
828 // Merge remainder of arguments from GET['q'], into relative file path.
829 $args = func_get_args();
830 $filepath = implode('/', $args);
831
832 // Maintain compatibility with old ?file=paths saved in node bodies.
833 if (isset($_GET['file'])) {
834 $filepath = $_GET['file'];
835 }
836
837 if (file_exists(file_create_path($filepath))) {
838 $headers = module_invoke_all('file_download', $filepath);
839 if (in_array(-1, $headers)) {
840 return drupal_access_denied();
841 }
842 if (count($headers)) {
843 file_transfer($filepath, $headers);
844 }
845 }
846 return drupal_not_found();
847 }
848
849
850 /**
851 * Finds all files that match a given mask in a given directory.
852 * Directories and files beginning with a period are excluded; this
853 * prevents hidden files and directories (such as SVN working directories)
854 * from being scanned.
855 *
856 * @param $dir
857 * The base directory for the scan, without trailing slash.
858 * @param $mask
859 * The regular expression of the files to find.
860 * @param $nomask
861 * An array of files/directories to ignore.
862 * @param $callback
863 * The callback function to call for each match.
864 * @param $recurse
865 * When TRUE, the directory scan will recurse the entire tree
866 * starting at the provided directory.
867 * @param $key
868 * The key to be used for the returned array of files. Possible
869 * values are "filename", for the path starting with $dir,
870 * "basename", for the basename of the file, and "name" for the name
871 * of the file without an extension.
872 * @param $min_depth
873 * Minimum depth of directories to return files from.
874 * @param $depth
875 * Current depth of recursion. This parameter is only used internally and should not be passed.
876 *
877 * @return
878 * An associative array (keyed on the provided key) of objects with
879 * "path", "basename", and "name" members corresponding to the
880 * matching files.
881 */
882 function file_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse = TRUE, $key = 'filename', $min_depth = 0, $depth = 0) {
883 $key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename');
884 $files = array();
885
886 if (is_dir($dir) && $handle = opendir($dir)) {
887 while ($file = readdir($handle)) {
888 if (!in_array($file, $nomask) && $file[0] != '.') {
889 if (is_dir("$dir/$file") && $recurse) {
890 // Give priority to files in this folder by merging them in after any subdirectory files.
891 $files = array_merge(file_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse, $key, $min_depth, $depth + 1), $files);
892 }
893 elseif ($depth >= $min_depth && ereg($mask, $file)) {
894 // Always use this match over anything already set in $files with the same $$key.
895 $filename = "$dir/$file";
896 $basename = basename($file);
897 $name = substr($basename, 0, strrpos($basename, '.'));
898 $files[$$key] = new stdClass();
899 $files[$$key]->filename = $filename;
900 $files[$$key]->basename = $basename;
901 $files[$$key]->name = $name;
902 if ($callback) {
903 $callback($filename);
904 }
905 }
906 }
907 }
908
909 closedir($handle);
910 }
911
912 return $files;
913 }
914
915 /**
916 * Determine the default temporary directory.
917 *
918 * @return A string containing a temp directory.
919 */
920 function file_directory_temp() {
921 $temporary_directory = variable_get('file_directory_temp', NULL);
922
923 if (is_null($temporary_directory)) {
924 $directories = array();
925
926 // Has PHP been set with an upload_tmp_dir?
927 if (ini_get('upload_tmp_dir')) {
928 $directories[] = ini_get('upload_tmp_dir');
929 }
930
931 // Operating system specific dirs.
932 if (substr(PHP_OS, 0, 3) == 'WIN') {
933 $directories[] = 'c:\\windows\\temp';
934 $directories[] = 'c:\\winnt\\temp';
935 $path_delimiter = '\\';
936 }
937 else {
938 $directories[] = '/tmp';
939 $path_delimiter = '/';
940 }
941
942 foreach ($directories as $directory) {
943 if (!$temporary_directory && is_dir($directory)) {
944 $temporary_directory = $directory;
945 }
946 }
947
948 // if a directory has been found, use it, otherwise default to 'files/tmp' or 'files\\tmp';
949 $temporary_directory = $temporary_directory ? $temporary_directory : file_directory_path() . $path_delimiter .'tmp';
950 variable_set('file_directory_temp', $temporary_directory);
951 }
952
953 return $temporary_directory;
954 }
955
956 /**
957 * Determine the default 'files' directory.
958 *
959 * @return A string containing the path to Drupal's 'files' directory.
960 */
961 function file_directory_path() {
962 return variable_get('file_directory_path', conf_path() .'/files');
963 }
964
965 /**
966 * Determine the maximum file upload size by querying the PHP settings.
967 *
968 * @return
969 * A file size limit in bytes based on the PHP upload_max_filesize and post_max_size
970 */
971 function file_upload_max_size() {
972 static $max_size = -1;
973
974 if ($max_size < 0) {
975 $upload_max = parse_size(ini_get('upload_max_filesize'));
976 $post_max = parse_size(ini_get('post_max_size'));
977 $max_size = ($upload_max < $post_max) ? $upload_max : $post_max;
978 }
979 return $max_size;
980 }
981
982 /**
983 * @} End of "defgroup file".
984 */