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