diff --git a/includes/file.inc b/includes/file.inc index 6a28a0c..7862670 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -6,6 +6,23 @@ * API for handling file uploads and server file management. */ +// Stream wrapper interface and base class implementation +require_once DRUPAL_ROOT . '/includes/stream_wrapper.inc'; + +// Stream wrapper registry +require_once DRUPAL_ROOT . '/includes/stream_wrapper_registry.inc'; + +// Core stream wrapper implementations +require_once DRUPAL_ROOT . '/includes/public_stream_wrapper.inc'; +require_once DRUPAL_ROOT . '/includes/private_stream_wrapper.inc'; +require_once DRUPAL_ROOT . '/includes/temp_stream_wrapper.inc'; + +// Register core wrappers +$manager = drupal_stream_wrapper_registry::singleton(); +$manager->register('public', 'drupal_public_stream_wrapper'); +$manager->register('private', 'drupal_private_stream_wrapper'); +$manager->register('temp', 'drupal_temp_stream_wrapper'); + /** * @defgroup file File interface * @{ @@ -242,15 +259,16 @@ function file_check_path(&$path) { * path of the source. */ function file_check_location($source, $directory = '') { - $check = realpath($source); + $check = drupal_realpath($source); + if ($check) { $source = $check; } else { // This file does not yet exist. - $source = realpath(dirname($source)) . '/' . basename($source); + $source = drupal_realpath(dirname($source)) . '/' . basename($source); } - $directory = realpath($directory); + $directory = drupal_realpath($directory); if ($directory && strpos($source, $directory) !== 0) { return FALSE; } @@ -445,7 +463,7 @@ function file_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) * @see file_copy() */ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { - $source = realpath($source); + $source = drupal_realpath($source); if (!file_exists($source)) { drupal_set_message(t('The specified 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 FALSE; @@ -473,7 +491,7 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST // 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($destination)) { + if ($source == drupal_realpath($destination)) { drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error'); return FALSE; } @@ -1561,7 +1579,7 @@ function file_get_mimetype($filename, $mapping = NULL) { } /** - * Set the permissions on a file or directory. + * Set the permissions on a URI (stream). * * This function will use the 'file_chmod_directory' and 'file_chmod_file' * variables for the default modes for directories and uploaded/generated files. @@ -1570,32 +1588,145 @@ function file_get_mimetype($filename, $mapping = NULL) { * these files, and give group write permissions so webserver group members * (e.g. a vhost account) can alter files uploaded and owned by the webserver. * - * @param $path + * PHP's chmod does not support stream wrappers so we use our wrapper implementation + * which interfaces with chmod() by default. Contrib wrappers can override this + * bahavior in their implementations as needed. + * + * @see http://php.net/manual/en/function.chmod.php + * + * @param $uri * String containing the path to a file or directory. * @param $mode * Integer value for the permissions. Consult PHP chmod() documentation for * more information. * @return - * TRUE for success, FALSE in the event of an error. + * TRUE for success, FALSE in the event of an error. Errors are logged by watchdog. */ -function drupal_chmod($path, $mode = NULL) { +function drupal_chmod($uri, $mode = NULL) { + if (!isset($mode)) { - if (is_dir($path)) { + if (is_dir($uri)) { $mode = variable_get('file_chmod_directory', 0775); } else { $mode = variable_get('file_chmod_file', 0664); } } - - if (@chmod($path, $mode)) { - return TRUE; + + // If this URI is a stream, pass it off to the appropriate stream wrapper. + // Otherwise, attempt PHP's chmod. This allows use of drupal_chmod even + // for unmanaged files outside of the stream wrapper interface. + if (stream_scheme($uri)) { + if (stream_wrapper($uri)->stream_chmod($uri, $mode)) { + return TRUE; + } + } + else { + if (@chmod($uri, $mode)) { + return TRUE; + } } - watchdog('file', 'The file permissions could not be set on %path.', array('%path' => $path), WATCHDOG_ERROR); + watchdog('file', 'The file permissions could not be set on %uri.', array('%uri' => $uri), WATCHDOG_ERROR); return FALSE; } /** + * Get canonicalized absolute path of a file or directory + * + * PHP's realpath() does not properly support streams, so this function + * fills that gap. If a stream wrapped URI is provided, it will be passed + * to the registered wrapper for handling. If the URI does not contain a + * scheme or the wrapper implementation does not implement realpath, then + * FALSE will be returned. + * + * @see http://php.net/manual/en/function.realpath.php + * + * @param string $uri + * A string containing a path to a stream (file, directory, etc.) + * @return mixed + * A string containing the absolute pathname, or FALSE on failure. + */ +function drupal_realpath($uri) { + + // If this URI is a stream, pass it off to the appropriate stream wrapper. + // Otherwise, attempt PHP's realpath. This allows use of drupal_realpath even + // for unmanaged files outside of the stream wrapper interface. + if (stream_scheme($uri)) + return stream_wrapper($uri)->stream_realpath($uri); + else + return realpath($uri); +} + +/** + * Create file with unique file name. + * + * Stream wrapper friendly PHP tempnam() replacement. Creates a file with a unique + * filename, with access permission set to 0600, in the predeclared temp directory. + * Drupal will perform garbage collection on these files during cron runs based on + * DRUPAL_MAXIMUM_TEMP_FILE_AGE. + * + * Note the absense of param $dir, unlike PHP's tempnam. $dir is determined by the + * temp stream wrapper. It first tries the temp directory specified in + * admin/settings/file-system and if that directory does not exist it will fall back + * to a system default tmp directory. + * + * @see http://php.net/manual/en/function.tempnam.php + * + * @param string $prefix + * The prefix of the generated temporary filename. Note: Windows uses only + * the first three characters of prefix. + * @return mixed + * Returns the new temporary filename, or FALSE on failure. + */ +function drupal_tempnam($prefix) { + + /* + TODO This implementation will change once we get the rest of the API sorted. + */ + return tempnam(stream_wrapper('temp://')->interpolate_uri(''), $prefix); +} + +/** + * Get the scheme of a URI (stream). + * + * A stream is referenced as scheme://target. + * + * @param $uri + * A stream, referenced as scheme://target + * @return mixed + * A string containing the name of the scheme, or FALSE if none. + * For example, the URI public://example.txt would return public. + */ +function stream_scheme($uri) { + $data = explode('://', $uri, 2); + return count($data) == 2 ? $data[0] : FALSE; +} + +/** + * Get reference to stream wrapper class responsible for given URI (stream). + * + * The scheme determines the stream wrapper class that should be + * used by consulting the stream wrapper registry. + * + * @param $uri + * A stream, referenced as scheme://target + * @return mixed + * Returns a new stream wrapper object appropriate for the given URI. + * For example, a URI of public://example.txt would return a new + * private stream wrapper object (drupal_private_stream_wrapper). + * FALSE is returned if no registered handler could be found. + */ +function stream_wrapper($uri) { + $registry = drupal_stream_wrapper_registry::singleton(); + $class = $registry->class_name(stream_scheme($uri)); + + if (class_exists($class)) + return new $class; + else + return FALSE; +} + +/** * @} End of "defgroup file". */ diff --git a/includes/private_stream_wrapper.inc b/includes/private_stream_wrapper.inc new file mode 100644 index 0000000..40ea235 --- /dev/null +++ b/includes/private_stream_wrapper.inc @@ -0,0 +1,32 @@ +path_key, $this->path_default); + + return url('system/files/' . parse_url($uri, PHP_URL_PATH), array('absolute' => TRUE)); + } +} diff --git a/includes/public_stream_wrapper.inc b/includes/public_stream_wrapper.inc new file mode 100644 index 0000000..b22543b --- /dev/null +++ b/includes/public_stream_wrapper.inc @@ -0,0 +1,75 @@ +path_key, $this->path_default); + + // just in case stream_public_path is s3://, ftp://, etc. Don't call PHP's + // drupal_realpath(). + if (parse_url($basepath, PHP_URL_SCHEME)) { + $path = $basepath . parse_url($uri, PHP_URL_PATH); + } + else { + // interpolate relative paths for basepath, and strip relative paths from + // url path. + $path = drupal_realpath($basepath) . str_replace('/..','', parse_url($uri, PHP_URL_PATH)); + } + + return $path; + } + + /** + * Override html_uri(). + * + * Return the HTML URI of a public file. + */ + function html_uri($uri) { + $basepath = variable_get($this->path_key, $this->path_default); + $path = parse_url($uri, PHP_URL_PATH); + + return $GLOBALS['base_url'] . '/' . $basepath . '/' . str_replace('\\', '/', $path); + } + + /** + * Override mime(). + * + * Return the mime type of a file. + */ + function mime($uri) { + return file_get_mimetype(basename($uri)); + } + + /** + * Override stream_realpath(). + * + * Interpolates the given URI and then passes it to PHP's realpath. + */ + function stream_realpath($uri) { + return realpath($this->interpolate_uri($uri)); + } + +} diff --git a/includes/stream_wrapper.inc b/includes/stream_wrapper.inc new file mode 100644 index 0000000..930c4d7 --- /dev/null +++ b/includes/stream_wrapper.inc @@ -0,0 +1,443 @@ +interpolate_uri($uri); + $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($uri, $mode) : @fopen($uri, $mode); + + if ((bool)$this->handle && $options & STREAM_USE_PATH) { + $opened_url = $uri; + } + + resource_debug("stream opened: $this->handle"); + + return (bool)$this->handle; + } + + /** + * Support for flock(). + * + * @see http://php.net/manual/en/streamwrapper.stream-lock.php + * + * @param int $operation + * @return bool + * Always returns TRUE. + */ + function stream_lock($operation) { + if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) { + return flock($this->handle, $operation); + } + + return TRUE; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @see http://php.net/manual/en/streamwrapper.stream-read.php + * + * @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) { + resource_debug("stream_read: $this->handle"); + + return fread($this->handle, $count); + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * @see http://php.net/manual/en/streamwrapper.stream-write.php + * + * @param $data + * The string to be written. + * @return int + * The number of bytes written. + */ + public function stream_write($data) { + resource_debug("stream_write: $this->handle"); + + return fwrite($this->handle, $data); + } + + /** + * Support for feof(). + * + * @see http://php.net/manual/en/streamwrapper.stream-eof.php + * + * @return bool + * TRUE if end-of-file has been reached. + */ + public function stream_eof() { + return feof($this->handle); + } + + /** + * Support for fseek(). + * + * @see http://php.net/manual/en/streamwrapper.stream-seek.php + * + * @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->handle, $offset, $whence); + } + + /** + * Support for fflush(). + * + * @see http://php.net/manual/en/streamwrapper.stream-flush.php + * + * @return + * TRUE if data was successfully stored (or there was no data to store). + */ + public function stream_flush() { + return fflush($this->handle); + } + + /** + * Support for ftell(). + * + * @see http://php.net/manual/en/streamwrapper.stream-tell.php + * + * @return + * The current offset in bytes from the beginning of file. + */ + public function stream_tell() { + return ftell($this->handle); + } + + /** + * Support for fstat(). + * + * @see http://php.net/manual/en/streamwrapper.stream-stat.php + * + * @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->handle); + } + + /** + * Support for fclose(). + * + * @see http://php.net/manual/en/streamwrapper.stream-close.php + * + * @return + * TRUE if stream was successfully closed. + */ + public function stream_close() { + resource_debug("streamclose: $this->handle"); + + return fclose($this->handle); + } + + /** + * Support for unlink(). + * + * @see http://php.net/manual/en/streamwrapper.unlink.php + * + * @param $uri + * A string containing the uri to the resource to delete. + * @return + * TRUE if resource was successfully deleted. + */ + public function unlink($uri) { + return unlink($this->interpolate_uri($uri)); + } + + /** + * Support for rename(). + * + * @see http://php.net/manual/en/streamwrapper.rename.php + * + * @param $from_uri, + * The uri to the file to rename. + * @param $to_uri + * The new uri for file. + * @return + * TRUE if file was successfully renamed. + */ + public function rename($from_uri, $to_uri) { + return rename($this->interpolate_uri($from_uri), $this->interpolate_uri($to_uri)); + } + + /** + * Support for mkdir(). + * + * @see http://php.net/manual/en/streamwrapper.mkdir.php + * + * @param $uri + * A string containing the url 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($uri, $mode, $options) { + $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE); + if ($options & STREAM_REPORT_ERRORS) { + return mkdir($this->interpolate_uri($uri), $mode, $recursive); + } + else { + return @mkdir($this->interpolate_uri($uri), $mode, $recursive); + } + } + + /** + * Support for rmdir(). + * + * @see http://php.net/manual/en/streamwrapper.rmdir.php + * + * @param $uri + * A string containing the url to the directory to delete. + * @param $options + * A bit mask of STREAM_REPORT_ERRORS. + * @return + * TRUE if directory was successfully removed. + */ + public function rmdir($uri, $options) { + if ($options & STREAM_REPORT_ERRORS) { + return rmdir($this->interpolate_uri($uri)); + } + else { + return @rmdir($this->interpolate_uri($uri)); + } + } + + /** + * Support for stat(). + * + * @see http://php.net/manual/en/streamwrapper.url-stat.php + * + * @param $uri + * A string containing the url 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($uri, $flags) { + return ($flags & STREAM_URL_STAT_QUIET) ? @stat($this->interpolate_uri($uri)) : stat($this->interpolate_uri($uri)); + } + + /** + * Support for opendir(). + * + * @see http://php.net/manual/en/streamwrapper.dir-opendir.php + * + * @param $uri + * A string containing the url to the directory to open. + * @param $options + * Unknown (parameter is not documented in PHP Manual). + * @return + * TRUE on success. + */ + public function dir_opendir($uri, $options) { + $this->handle = opendir($this->interpolate_uri($uri)); + + return (bool)$this->handle; + } + + /** + * Support for readdir(). + * + * @see http://php.net/manual/en/streamwrapper.dir-readdir.php + * + * @return + * The next filename, or FALSE if there are no more files in the directory. + */ + public function dir_readdir() { + return readdir($this->handle); + } + + /** + * Support for rewinddir(). + * + * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php + * + * @return + * TRUE on success. + */ + public function dir_rewinddir() { + return rewinddir($this->handle); + } + + /** + * Support for closedir(). + * + * @see http://php.net/manual/en/streamwrapper.dir-closedir.php + * + * @return + * TRUE on success. + */ + public function dir_closedir() { + return closedir($this->handle); + } +} \ No newline at end of file diff --git a/includes/stream_wrapper_registry.inc b/includes/stream_wrapper_registry.inc new file mode 100644 index 0000000..ca2ebaf --- /dev/null +++ b/includes/stream_wrapper_registry.inc @@ -0,0 +1,113 @@ +wrappers[$scheme] = $class_name; + + return stream_wrapper_register($scheme, $class_name); + } + + /** + * Unregister a stream wrapper. + * + * @see: http://php.net/manual/en/function.stream-wrapper-unregister.php + * + * @param string $scheme + * URI scheme. + * @return bool + * result of stream_wrapper_unregister() + */ + function unregister($scheme) { + if (stream_wrapper_unregister($scheme)) { + unset(self::$this->wrappers[$scheme]); + return TRUE; + } + + return FALSE; + } + + /** + * Return the stream wrapper class name for a given scheme. + * + * @param string $scheme + * Stream scheme. + * @return mixed + * Return string if a scheme has a registered handler, or FALSE. + */ + function class_name($scheme) { + if (empty(self::$this->wrappers[$scheme])) { + return FALSE; + } + + return self::$this->wrappers[$scheme]; + } + + /** + * Return the stream class name for a given scheme. + * + * @param string $scheme + * Stream scheme. + * @return mixed + * Return string if a scheme has a registered handler, or FALSE. + */ + function scheme($class) { + return array_search(self::$this->wrappers, $class); + } + + /** + * Return the entire Drupal stream wrapper registry. + * + * @return array + */ + function wrappers() { + return self::$this->wrappers; + } +} diff --git a/includes/temp_stream_wrapper.inc b/includes/temp_stream_wrapper.inc new file mode 100644 index 0000000..9e013dc --- /dev/null +++ b/includes/temp_stream_wrapper.inc @@ -0,0 +1,32 @@ +path_key, $this->path_default); + + return url('system/files/' . parse_url($uri, PHP_URL_PATH), array('absolute' => TRUE)); + } +} diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test index 18bc651..3e87610 100644 --- a/modules/simpletest/tests/file.test +++ b/modules/simpletest/tests/file.test @@ -41,6 +41,33 @@ function file_test_file_scan_callback($filepath, $reset = FALSE) { } /** + * Helper class for testing the stream wrapper registry. + * + * Dummy stream wrapper implementation (dummy://). + */ +class drupal_dummy_stream_wrapper extends drupal_stream_wrapper { + + /** + * Override interpolate_uri(). + * + * Return a dummy path for testing. + */ + function interpolate_uri($uri) { + return '/dummy/example.txt'; + } + + /** + * Override html_uri(). + * + * Return the HTML URI of a public file. + */ + function html_uri($uri) { + return '/dummy/example.txt'; + } + +} + +/** * Base class for file tests that adds some additional file specific * assertions and helper functions. */ @@ -401,7 +428,7 @@ class FileValidatorTest extends DrupalWebTestCase { if (image_get_toolkit()) { // Copy the image so that the original doesn't get resized. $temp_dir = file_directory_temp(); - copy(realpath('misc/druplicon.png'), realpath($temp_dir) . '/druplicon.png'); + copy(drupal_realpath('misc/druplicon.png'), drupal_realpath($temp_dir) . '/druplicon.png'); $this->image->filepath = $temp_dir . '/druplicon.png'; $errors = file_validate_image_resolution($this->image, '10x5'); @@ -411,7 +438,7 @@ class FileValidatorTest extends DrupalWebTestCase { $this->assertTrue($info['width'] <= 10, t('Image scaled to correct width.'), 'File'); $this->assertTrue($info['height'] <= 5, t('Image scaled to correct height.'), 'File'); - unlink(realpath($temp_dir . '/druplicon.png')); + unlink(drupal_realpath($temp_dir . '/druplicon.png')); } else { // TODO: should check that the error is returned if no toolkit is available. @@ -505,14 +532,14 @@ class FileUnmanagedSaveDataTest extends FileTestCase { $filepath = file_unmanaged_save_data($contents); $this->assertTrue($filepath, t('Unnamed file saved correctly.')); $this->assertEqual(file_directory_path(), dirname($filepath), t("File was placed in Drupal's files directory.")); - $this->assertEqual($contents, file_get_contents(realpath($filepath)), t('Contents of the file are correct.')); + $this->assertEqual($contents, file_get_contents(drupal_realpath($filepath)), t('Contents of the file are correct.')); // Provide a filename. $filepath = file_unmanaged_save_data($contents, 'asdf.txt', FILE_EXISTS_REPLACE); $this->assertTrue($filepath, t('Unnamed file saved correctly.')); $this->assertEqual(file_directory_path(), dirname($filepath), t("File was placed in Drupal's files directory.")); $this->assertEqual('asdf.txt', basename($filepath), t('File was named correctly.')); - $this->assertEqual($contents, file_get_contents(realpath($filepath)), t('Contents of the file are correct.')); + $this->assertEqual($contents, file_get_contents(drupal_realpath($filepath)), t('Contents of the file are correct.')); $this->assertFilePermissions($filepath, variable_get('file_chmod_file', 0664)); } } @@ -552,7 +579,7 @@ class FileSaveUploadTest extends FileHookTestCase { // Upload with replace to gurantee there's something there. $edit = array( 'file_test_replace' => FILE_EXISTS_REPLACE, - 'files[file_test_upload]' => realpath($this->image->filepath) + 'files[file_test_upload]' => drupal_realpath($this->image->filepath) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); $this->assertResponse(200, t('Received a 200 response for posted test file.')); @@ -579,7 +606,7 @@ class FileSaveUploadTest extends FileHookTestCase { // Upload a second file. $max_fid_before = db_query('SELECT MAX(fid) AS fid FROM {files}')->fetchField(); $image2 = current($this->drupalGetTestFiles('image')); - $edit = array('files[file_test_upload]' => realpath($image2->filepath)); + $edit = array('files[file_test_upload]' => drupal_realpath($image2->filepath)); $this->drupalPost('file-test/upload', $edit, t('Submit')); $this->assertResponse(200, t('Received a 200 response for posted test file.')); $this->assertRaw(t('You WIN!')); @@ -604,7 +631,7 @@ class FileSaveUploadTest extends FileHookTestCase { function testExistingRename() { $edit = array( 'file_test_replace' => FILE_EXISTS_RENAME, - 'files[file_test_upload]' => realpath($this->image->filepath) + 'files[file_test_upload]' => drupal_realpath($this->image->filepath) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); $this->assertResponse(200, t('Received a 200 response for posted test file.')); @@ -620,7 +647,7 @@ class FileSaveUploadTest extends FileHookTestCase { function testExistingReplace() { $edit = array( 'file_test_replace' => FILE_EXISTS_REPLACE, - 'files[file_test_upload]' => realpath($this->image->filepath) + 'files[file_test_upload]' => drupal_realpath($this->image->filepath) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); $this->assertResponse(200, t('Received a 200 response for posted test file.')); @@ -636,7 +663,7 @@ class FileSaveUploadTest extends FileHookTestCase { function testExistingError() { $edit = array( 'file_test_replace' => FILE_EXISTS_ERROR, - 'files[file_test_upload]' => realpath($this->image->filepath) + 'files[file_test_upload]' => drupal_realpath($this->image->filepath) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); $this->assertResponse(200, t('Received a 200 response for posted test file.')); @@ -2031,3 +2058,51 @@ class FileMimeTypeTest extends DrupalWebTestCase { } } } + +/** + * Tests stream wrapper registry. + */ +class StreamWrapperRegistryUnitTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => t('Stream Wrapper Registry'), + 'description' => t('Tests stream wrapper registry.'), + 'group' => t('File'), + ); + } + + /** + * Test stream wrapper registry. + */ + function testStreamWrapperRegistry() { + $scheme = 'dummy'; + $filename = 'example.txt'; + $class = 'drupal_dummy_stream_wrapper'; + $uri = $scheme .'://'. $filename; + + // Register a dummy wrapper + $registry = drupal_stream_wrapper_registry::singleton(); + $registry->register($scheme, $class); + $this->assertTrue(stream_wrapper($uri), 'Dummy stream wrapper registered'); + + // $this->assertEqual($this->scheme, stream_scheme($this->uri), 'stream_scheme() with valid stream wrapped URI.'); + // $this->assertFalse(stream_scheme('/foo/bar'), 'stream_scheme() with faulty URI.'); + //$this->assertEqual('/dummy/example.txt', stream_wrapper($dummy_uri)->interpolate_uri($dummy_uri), 'Stream wrapper URI interpolation.'); + + // Unregister dummy wrapper + $registry->unregister($scheme); + $this->assertFalse(stream_wrapper($uri), 'Dummy stream wrapper unregistered.'); + } + + /** + * Confirm core stream wrappers are available. + */ + function testCoreStreamWrapperAvailability() { + + $filename = 'example.txt'; + $this->assertTrue(stream_wrapper('public://'. $filename), 'Public stream wrapper registered and available.'); + $this->assertTrue(stream_wrapper('private://'. $filename), 'Private stream wrapper registered and available.'); + $this->assertTrue(stream_wrapper('temp://'. $filename), 'Temp stream wrapper registered and available.'); + } +} \ No newline at end of file