Mercurial > defr > drupal > core
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 */ |