Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.76 diff -u -8 -p -r1.76 system.admin.inc --- modules/system/system.admin.inc 10 May 2008 07:32:02 -0000 1.76 +++ modules/system/system.admin.inc 25 May 2008 20:06:35 -0000 @@ -1443,17 +1443,17 @@ function system_file_system_settings() { '#after_build' => array('system_check_directory'), ); $form['file_directory_temp'] = array( '#type' => 'textfield', '#title' => t('Temporary directory'), '#default_value' => file_directory_temp(), '#maxlength' => 255, - '#description' => t('A file system path where uploaded files will be stored during previews.'), + '#description' => t('A file system path where temporary files may be stored.'), '#after_build' => array('system_check_directory'), ); $form['file_downloads'] = array( '#type' => 'radios', '#title' => t('Download method'), '#default_value' => variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC), '#options' => array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using HTTP directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')), @@ -2258,9 +2258,9 @@ function theme_system_themes_form($form) } $rows[] = $row; } $header = array(t('Screenshot'), t('Name'), t('Version'), t('Enabled'), t('Default'), t('Operations')); $output = theme('table', $header, $rows); $output .= drupal_render($form); return $output; -} \ No newline at end of file +} Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.124 diff -u -8 -p -r1.124 file.inc --- includes/file.inc 23 Apr 2008 18:17:41 -0000 1.124 +++ includes/file.inc 25 May 2008 20:06:35 -0000 @@ -28,16 +28,67 @@ define('FILE_EXISTS_ERROR', 2); * * If you wish to add custom statuses for use by contrib modules please expand as * binary flags and consider the first 8 bits reserved. (0,1,2,4,8,16,32,64,128) */ define('FILE_STATUS_TEMPORARY', 0); define('FILE_STATUS_PERMANENT', 1); /** + * Get canonicalized absolute path of a file or directory. The Windows path + * separator "\" is converted to "/", and consecutive "/" characters are stripped. + * If path is a directory, the trailing "/" is stripped. + * For regular files: + * - Symbolic links are expanded. + * - "/./" and "/../" segments are resolved. + * For paths with protocol/wrapper prefix (e.g. "mywrapper://foo/bar.txt"): + * - Paths containing "/../" are blocked (FALSE is returned). + * - Paths are assumed to be case-sensitive (no case normalization is done). + * + * @code + * // Returns "/foo/bar/boo", or FALSE if the file does not exist: + * file_realpath('/foo//bar/./baz/..\\boo'); + * + * // Returns FALSE due to "/../": + * file_realpath('mywrapper://foo/bar/../baz'); + * @endcode + * + * @param $path A string containing a path to a file or directory. + * @return A string containing the absolute path to the file/directory, + * or FALSE if file/directory does not exist. + */ +function file_realpath($path) { + // Does $path include an explicit protocol/wrapper prefix "foo://" (not a + // Windows drive letter "C:/temp")? + if (preg_match('@^([a-z0-9.+-]{2,})://(.*)@i', $path, $reg)) { + // Replace "\" and "//" with "/", except when "//" is preceded by a colon + // (this indicates a nested stream wrapper prefix, e.g. "foo://bar://". + $wrappedPath = preg_replace('@(? $directory))); @chmod($directory, 0775); // Necessary for non-webserver users. } else { if ($form_item) { form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory))); } return FALSE; @@ -123,17 +174,17 @@ function file_check_directory(&$director return FALSE; } } if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) { $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"; if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) { fclose($fp); - chmod($directory . '/.htaccess', 0664); + @chmod($directory . '/.htaccess', 0664); } else { $variables = array('%directory' => $directory, '!htaccess' => '
' . nl2br(check_plain($htaccess_lines))); 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: !htaccess", $variables)); watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $variables, WATCHDOG_ERROR); } } @@ -174,25 +225,33 @@ function file_check_path(&$path) { * file_check_location('/www/example.com/files/../../../etc/passwd', '/www/example.com/files'); * @endcode * * @param $source A string set to the file to check. * @param $directory A string where the file should be located. * @return 0 for invalid path or the real path of the source. */ function file_check_location($source, $directory = '') { - $check = realpath($source); + $check = file_realpath($source); if ($check) { $source = $check; } else { // This file does not yet exist - $source = realpath(dirname($source)) . '/' . basename($source); + $dirname = file_realpath(dirname($source)); + $basename = basename($source); + + // Make sure $source is located in an existing directory + if (!$dirname || $basename == "..") { + return 0; + } + + $source = $dirname . '/' . $basename; } - $directory = realpath($directory); + $directory = file_realpath($directory); if ($directory && strpos($source, $directory) !== 0) { return 0; } return $source; } /** * Copies a file to a new location. This is a powerful function that in many ways @@ -231,30 +290,30 @@ function file_copy(&$source, $dest = 0, if (is_object($source)) { $file = $source; $source = $file->filepath; if (!$basename) { $basename = $file->filename; } } - $source = realpath($source); + $source = file_realpath($source); if (!file_exists($source)) { 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'); return 0; } // If the destination file is not specified then use the filename of the source file. $basename = $basename ? $basename : basename($source); $dest = $directory . '/' . $basename; // Make sure source and destination filenames are not the same, makes no sense // to copy it if they are. In fact copying the file will most likely result in // a 0 byte file. Which is bad. Real bad. - if ($source != realpath($dest)) { + if ($source != file_realpath($dest)) { if (!$dest = file_destination($dest, $replace)) { 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'); return FALSE; } if (!@copy($source, $dest)) { drupal_set_message(t('The selected file %file could not be copied.', array('%file' => $source)), 'error'); return 0; @@ -748,17 +807,17 @@ function file_validate_image_resolution( * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique * - FILE_EXISTS_ERROR - Do nothing and return FALSE. * * @return A string containing the resulting filename or 0 on error */ function file_save_data($data, $dest, $replace = FILE_EXISTS_RENAME) { $temp = file_directory_temp(); // On Windows, tempnam() requires an absolute path, so we use realpath(). - $file = tempnam(realpath($temp), 'file'); + $file = tempnam(file_realpath($temp), 'file'); if (!$fp = fopen($file, 'wb')) { drupal_set_message(t('The file could not be created.'), 'error'); return 0; } fwrite($fp, $data); fclose($fp); if (!file_move($file, $dest, $replace)) { @@ -908,17 +967,20 @@ function file_scan_directory($dir, $mask closedir($handle); } return $files; } /** - * Determine the default temporary directory. + * Determine the default temporary directory. This may be used for storing + * temporary files within a single request. + * If a temporary file is to be used in several requests, it should be saved in + * file_directory_path with its status set to FILE_STATUS_TEMPORARY. * * @return A string containing a temp directory. */ function file_directory_temp() { $temporary_directory = variable_get('file_directory_temp', NULL); if (is_null($temporary_directory)) { $directories = array(); Index: includes/file.test =================================================================== RCS file: includes/file.test diff -N includes/file.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/file.test 25 May 2008 20:06:35 -0000 @@ -0,0 +1,160 @@ + t('Filesystem functions'), + 'description' => t('Create, delete, rename etc.'), + 'group' => t('File') + ); + } + + /** + * Test file_realpath(). + */ + function testFileRealpath() { + $file_directory_path = file_realpath(file_directory_path()); + $this->assertTrue(is_dir($file_directory_path)); + $this->assertNotEqual(substr($file_directory_path, 0, -1), '/'); + + $this->assertFalse(file_realpath($file_directory_path . '/not-found')); + + $file = $file_directory_path . '/foo.txt'; + file_put_contents($file, 'lorem ipsum'); + $this->assertTrue(is_file($file)); + $this->assertFalse(is_dir($file)); + $this->assertEqual(file_realpath($file), $file); + + $this->assertEqual(file_realpath($file_directory_path . '//foo.txt'), + $file); + } + + /** + * Test file_check_directory(). + */ + function testFileCheckDirectory() { + $this->assertTrue(file_check_directory(file_directory_path())); + $this->assertTrue(file_exists(file_directory_path() . '/.htaccess')); + + $dir = file_directory_path() . '/dir'; + $this->assertFalse(file_check_directory($dir)); + $this->assertTrue(file_check_directory($dir, FILE_CREATE_DIRECTORY)); + $this->assertTrue(file_check_directory($dir)); + } + + /** + * Test file_check_location(). + */ + function testFileCheckLocation() { + $file = file_directory_path() . '/foo/bar.txt'; + mkdir(dirname($file), 0777, true); + file_put_contents($file, 'lorem ipsum'); + + $this->assertTrue(file_check_location(file_directory_path() . '/', file_directory_path())); + $this->assertTrue(file_check_location($file, file_directory_path())); + $this->assertFalse(file_check_location(file_directory_path() . '/..', file_directory_path())); + } + + /** + * Test file_save_data(). + */ + function testFileSaveData() { + $file_directory_path = file_directory_path(); + $file = file_directory_path() . '/foo.txt'; + $file_0 = file_directory_path() . '/foo_0.txt'; + $this->assertEqual(file_save_data('abc', $file, FILE_EXISTS_REPLACE), $file); + $this->assertEqual(filesize($file), 3); + + $this->assertEqual(file_save_data('abcd', $file, FILE_EXISTS_REPLACE), $file); + $this->assertEqual(filesize($file), 4); + + $this->assertFalse(file_save_data('abcde', $file, FILE_EXISTS_ERROR)); + $this->assertEqual(filesize($file), 4); + + $this->assertEqual(file_save_data('abcdef', $file, FILE_EXISTS_RENAME), $file_0); + $this->assertEqual(filesize($file_0), 6); + } + + /** + * Test file_scan_directory(). + */ + function testFileScanDirectory() { + $files = array( + 'foobar.txt', + 'subdir1/foo.txt', + 'subdir2/subsubdir/bar.txt', + ); + foreach ($files as &$file) { + $file = file_directory_path() . '/' . $file; + if (!is_dir(dirname($file))) { + mkdir(dirname($file), 0777, true); + } + file_put_contents($file, 'lorem ipsum'); + } + $files_found = array_keys(file_scan_directory(file_directory_path(), '.*')); + sort($files); + sort($files_found); + $this->assertEqual($files, $files_found); + } +} + + +/** + * Inherits FileTestCase to allow running of all tests using a dummy stream wrapper. + */ +class FileStreamWrapperTestCase extends FileTestCase { + private $prefix; + private $original_directory_path; + + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('Filesystem functions (stream wrapper)'), + 'description' => t('Run all file tests with file_directory_path set to a path with a stream wrapper prefix.'), + 'group' => t('File') + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp(); + $this->prefix = 'dummy-stream-wrapper-' . uniqid(); + stream_wrapper_register($this->prefix, 'FileDummyStreamWrapper'); + $this->original_directory_path = file_directory_path(); + variable_set('file_directory_path', $this->prefix . '://' . file_realpath(file_directory_path())); + } + + /** + * Implementation of tearDown(). + */ + function tearDown() { + variable_set('file_directory_path', $this->original_directory_path); + stream_wrapper_unregister($this->prefix); + parent::tearDown(); + } + + /** + * Test file_realpath(). + */ + function testStreamWrapperFileRealpath() { + $file = file_directory_path() . '/foo/bar.txt'; + mkdir(dirname($file), 0777, true); + + file_put_contents($file, 'lorem ipsum'); + + // "/../" is forbidden + $this->assertFalse(file_realpath(file_directory_path() . '/../foo/bar.txt')); + $this->assertFalse(file_realpath(file_directory_path() . '/..')); + } +} + Index: includes/file.dummystreamwrapper.inc =================================================================== RCS file: includes/file.dummystreamwrapper.inc diff -N includes/file.dummystreamwrapper.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/file.dummystreamwrapper.inc 25 May 2008 20:06:35 -0000 @@ -0,0 +1,275 @@ +fileHandle = fopen($nestedPath, $mode); + } else { + $this->fileHandle = @fopen($nestedPath, $mode); + } + return (bool) $this->fileHandle; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @param $count + * Maximum number of bytes to be read + * @return + * The string that was read, or FALSE in case of an error + */ + public function stream_read($count) { + return fread($this->fileHandle, $count); + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * @param $data + * The string to be written + * @return + * The number of bytes written + */ + public function stream_write($data) { + return fwrite($this->fileHandle, $data); + } + + /** + * Support for feof(). + * + * @return + * TRUE if end-of-file has been reached + */ + public function stream_eof() { + return feof($this->fileHandle); + } + + /** + * Support for fseek(). + * + * @param $offset + * The byte offset to got to + * @param $whence + * SEEK_SET, SEEK_CUR, or SEEK_END + * @return + * TRUE on success + */ + public function stream_seek($offset, $whence) { + return fseek($this->fileHandle, $offset, $whence); + } + + /** + * Support for fflush(). + * + * @return + * TRUE if data was successfully stored (or there was no data to store) + */ + public function stream_flush() { + return fflush($this->fileHandle); + } + + /** + * Support for ftell(). + * + * @return + * The current offset in bytes from the beginning of file + */ + public function stream_tell() { + return ftell($this->fileHandle); + } + + /** + * Support for fstat(). + * + * @return + * An array with file status, or false in case of an error - see fstat() + * for a description of this array + */ + public function stream_stat() { + return fstat($this->fileHandle); + } + + /** + * Support for fclose(). + */ + public function stream_close() { + return fclose($this->fileHandle); + } + + /** + * Support for unlink(). + * + * @param $path + * A string containing the path to the file to delete + * @return + * TRUE if file was successfully deleted + */ + public function unlink($path) { + return unlink(self::getNestedPath($path)); + } + + /** + * Support for rename(). + * + * @param $fromPath + * The path to the file to rename + * @param $toPath + * The new path to the file + * + * @return + * TRUE, if file was successfully renamed + */ + public function rename($fromPath, $toPath) { + return rename(self::getNestedPath($fromPath), self::getNestedPath($toPath)); + } + + /** + * Support for mkdir(). + * + * @param $path + * A string containing the path to the directory to create + * @param $mode + * Permission flags - see mkdir() + * @param $options + * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE + * @return + * TRUE if directory was successfully created + */ + public function mkdir($path, $mode, $options) { + $nestedPath = self::getNestedPath($path); + $recursive = (bool) $options & STREAM_MKDIR_RECURSIVE; + if ($options & STREAM_REPORT_ERRORS) { + return mkdir($nestedPath, $mode, $recursive); + } else { + return @mkdir($nestedPath, $mode, $recursive); + } + } + + /** + * Support for rmdir(). + * + * @param $path + * A string containing the path to the directory to delete + * @param $options + * A bit mask of STREAM_REPORT_ERRORS + * @return bool true if directory was successfully removed + */ + public function rmdir($path, $options) { + $nestedPath = self::getNestedPath($path); + if ($options & STREAM_REPORT_ERRORS) { + return rmdir($nestedPath); + } else { + return @rmdir($nestedPath); + } + } + + /** + * Support for stat(). + * + * @param $path + * A string containing the path to get information about + * @param $flags + * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET + * @return + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array + */ + public function url_stat($path, $flags) { + $nestedPath = self::getNestedPath($path); + return ($flags & STREAM_URL_STAT_QUIET) + ? (file_exists($nestedPath) ? stat($nestedPath) : false) + : stat($nestedPath); + } + + /** + * Support for opendir(). + * + * @param $path + * A string containing the path to the directory to open + * @param $options + * Unknown (parameter is not documented in PHP Manual) + * @return + * TRUE on success + */ + public function dir_opendir($path, $options) { + $this->dirHandle = opendir(self::getNestedPath($path)); + return (bool) $this->dirHandle; + } + + /** + * Support for readdir(). + * + * @return + * The next filename, or FALSE if there are no more files in the directory + */ + public function dir_readdir() { + return readdir($this->dirHandle); + } + + /** + * Support for rewinddir(). + * + * @return + * TRUE on success + */ + public function dir_rewinddir() { + return rewinddir($this->dirHandle); + } + + /** + * Support for closedir(). + * + * @return + * TRUE on success + */ + public function dir_closedir() { + return closedir($this->dirHandle); + } + + /** + * Strips the stream wrapper prefix from the specified path. + * + * @param $path + * A string containing a path with a stream wrapper prefix + * @return + * The without a stream wrapper prefix + */ + private static function getNestedPath($path) { + return preg_replace('@^([a-z0-9.+-]{2,})://@i', '', $path); + } +} + Index: includes/image.gd.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/image.gd.inc,v retrieving revision 1.6 diff -u -8 -p -r1.6 image.gd.inc --- includes/image.gd.inc 14 Apr 2008 17:48:33 -0000 1.6 +++ includes/image.gd.inc 25 May 2008 20:06:35 -0000 @@ -191,31 +191,40 @@ function image_gd_open($file, $extension } /** * GD helper to write an image resource to a destination file. * * @param $res * An image resource created with image_gd_open(). * @param $destination - * A string file path where the iamge should be saved. + * A string file path where the image should be saved. * @param $extension * A string containing one of the following extensions: gif, jpg, jpeg, png. * @return * Boolean indicating success. */ function image_gd_close($res, $destination, $extension) { $extension = str_replace('jpg', 'jpeg', $extension); $close_func = 'image' . $extension; if (!function_exists($close_func)) { return FALSE; } + + // These functions do not support stream wrappers + $output_file = file_is_wrapper($destination) ? tempnam(file_realpath(file_directory_temp()), 'image') : $destination; if ($extension == 'jpeg') { - return $close_func($res, $destination, variable_get('image_jpeg_quality', 75)); + $ok = $close_func($res, $output_file, variable_get('image_jpeg_quality', 75)); } else { - return $close_func($res, $destination); + $ok = $close_func($res, $output_file); + } + + if ($ok && $destination != $output_file) { + $ok = copy($output_file, $destination); + @unlink($output_file); } + return $ok; } /** * @} End of "ingroup image". */