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 */ |
