Index: includes/file.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/file.inc,v
retrieving revision 1.171
diff -u -p -r1.171 file.inc
--- includes/file.inc	9 Jun 2009 21:33:12 -0000	1.171
+++ includes/file.inc	13 Jun 2009 13:31:43 -0000
@@ -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
  * @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, $d
   // 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, $m
 }
 
 /**
- * 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, $m
  * 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".
  */
Index: modules/simpletest/tests/file.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v
retrieving revision 1.32
diff -u -p -r1.32 file.test
--- modules/simpletest/tests/file.test	2 Jun 2009 13:42:40 -0000	1.32
+++ modules/simpletest/tests/file.test	13 Jun 2009 13:31:43 -0000
@@ -41,6 +41,33 @@ function file_test_file_scan_callback($f
 }
 
 /**
+ * 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 DrupalWe
     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 DrupalWe
       $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 
     $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 FileHoo
     // 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 FileHoo
     // 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 FileHoo
   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 FileHoo
   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 FileHoo
   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 DrupalWeb
     }
   }
 }
+
+/**
+ * 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
--- /dev/null	2009-05-20 00:27:51.000000000 -0400
+++ includes/private_stream_wrapper.inc	2009-06-13 09:33:21.000000000 -0400
@@ -0,0 +1,32 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Private (private://) stream wrapper class.
+ *
+ * Provides support for storing privately accessible 
+ * files with the Drupal file interface.
+ */
+ 
+/**
+ * Drupal private stream wrapper class.
+ *
+ * Extends drupal_public_stream_wrapper.
+ */
+class drupal_private_stream_wrapper extends drupal_public_stream_wrapper {
+  
+  private $path_key     = 'stream_private_path';
+  private $path_default = 'sites/default/files-private';
+
+  /**
+   * Override html_uri.
+   *
+   * Return the HTML URI of a private file.
+   */
+  function html_uri($uri) {
+    $basepath = variable_get($this->path_key, $this->path_default);
+    
+    return url('system/files/' . parse_url($uri, PHP_URL_PATH), array('absolute' => TRUE));
+  }
+}
--- /dev/null	2009-05-20 00:27:51.000000000 -0400
+++ includes/public_stream_wrapper.inc	2009-06-13 09:33:21.000000000 -0400
@@ -0,0 +1,75 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Public (public://) stream wrapper class.
+ *
+ * Provides support for storing publicly accessible 
+ * files with the Drupal file interface.
+ */
+ 
+/**
+ * Drupal public stream wrapper class.
+ *
+ * Extends drupal_stream_wrapper.
+ */
+class drupal_public_stream_wrapper extends drupal_stream_wrapper {
+
+  // A handle to the file opened by stream_open().
+  private $path_key     = 'stream_public_path';
+  private $path_default = 'sites/default/files';
+
+  /**
+   * Override interpolate_uri().
+   *
+   * Interpolate the URI path, adding the public files path.
+   */
+  function interpolate_uri($uri) {
+    $basepath = variable_get($this->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));
+   }
+  
+}
--- /dev/null	2009-05-20 00:27:51.000000000 -0400
+++ includes/temp_stream_wrapper.inc	2009-06-13 09:33:21.000000000 -0400
@@ -0,0 +1,32 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Temp (temp://) stream wrapper class.
+ *
+ * Provides support for storing temporarily accessible 
+ * files with the Drupal file interface.
+ */
+ 
+/**
+ * Drupal temp stream wrapper class.
+ *
+ * Extends drupal_public_stream_wrapper.
+ */
+class drupal_temp_stream_wrapper extends drupal_public_stream_wrapper {
+  
+  private $path_key     = 'file_directory_temp';
+  private $path_default = 'sites/default/files/tmp';
+
+  /**
+   * Override html_uri.
+   *
+   * Return the HTML URI of a private file.
+   */
+  function html_uri($uri) {
+    $basepath = variable_get($this->path_key, $this->path_default);
+    
+    return url('system/files/' . parse_url($uri, PHP_URL_PATH), array('absolute' => TRUE));
+  }
+}
--- /dev/null	2009-05-20 00:27:51.000000000 -0400
+++ includes/stream_wrapper.inc	2009-06-13 09:33:21.000000000 -0400
@@ -0,0 +1,443 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Drupal stream wrapper interface.
+ *
+ * Provides a Drupal interface and base class to PHP stream wrappers.
+ */
+ 
+/**
+ * Generic PHP stream wrapper interface.
+ *
+ * @see: http://php.net/manual/en/function.stream-wrapper-register.php
+ */
+interface stream_wrapper_interface {
+  public function stream_open($uri, $mode, $options, &$opened_url);
+  public function stream_close();
+  public function stream_lock($operation);
+  public function stream_read($count);
+  public function stream_write($data);
+  public function stream_eof();
+  public function stream_seek($offset, $whence);
+  public function stream_flush();
+  public function stream_tell();
+  public function stream_stat();
+  public function unlink($uri);
+  public function rename($from_uri, $to_uri);
+  public function mkdir($uri, $mode, $options);
+  public function rmdir($uri, $options);
+  public function url_stat($uri, $flags);
+  public function dir_opendir($uri, $options);
+  public function dir_readdir();
+  public function dir_rewinddir();
+  public function dir_closedir();
+}
+
+/**
+ * Drupal stream wrapper extension.
+ *
+ * Extend the stream_wrapper_interface with methods expected by 
+ * Drupal stream wrapper classes.
+ */
+interface drupal_stream_wrapper_interface extends stream_wrapper_interface {
+  
+  /**
+   * Return an absolute stream resource URL.
+   *
+   * @param $uri
+   *   Stream wrapper resource url.
+   * @return string
+   */
+  function interpolate_uri($uri);
+
+  /**
+   * Return the html accessible URL for a resource.
+   *
+   * @param $uri
+   *   Stream wrapper resource url.
+   * @return string
+   */
+  function html_uri($uri);
+
+  /**
+   * Return the mime type of a resource.
+   *
+   * @param $uri
+   *   Stream wrapper resource url.
+   * @return string
+   */
+  function mime($uri);
+  
+  /**
+   * Change permissions of stream.
+   *
+   * PHP lacks this functionality and it is not part of the official
+   * stream wrapper interface. This is a custom implementation for
+   * Drupal.
+   *
+   * @param string $uri
+   * @param mixed $mode
+   * @return bool
+   *   Returns TRUE on success or FALSE on failure.
+   */
+  function stream_chmod($uri, $mode);
+  
+  /**
+   * Returns canonicalized absolute pathname.
+   *
+   * Implementation placeholder. PHP's realpath does not support 
+   * stream wrappers. We provide this as a default so that
+   * individual wrappers may implement their own solutions.
+   * The base class will always return FALSE.
+   *
+   * @param string $uri
+   * @return mixed
+   *   A string with absolute pathname on success (implemented
+   *   by core wrappers), or FALSE on failure or the registered
+   *   wrapper does not provide an implementation.
+   */
+  function stream_realpath($uri);
+  
+}
+
+/**
+ * Drupal stream wrapper base class.
+ *
+ * This class provides a complete stream wrapper implementation. It passes
+ * incoming URI's through an interpolation method and then recursively calls
+ * the invoking PHP filesystem function.
+ *
+ * drupal_stream_wrapper implementations need to override at least the interpolate_uri
+ * method to rewrite the URI before is it passed back to the calling function.
+ */
+abstract class drupal_stream_wrapper implements drupal_stream_wrapper_interface {
+  
+  private $handle = NULL;
+
+  function interpolate_uri($uri) {
+    return $uri;
+  }
+
+  function html_uri($uri) {
+    return $uri;
+  }
+
+  function mime($uri) {
+    return 'application/octet-stream';
+  }
+  
+  function stream_chmod($uri, $mode) {
+    return @chmod($uri, $mode);
+  }
+  
+  function stream_realpath($uri) {
+    return FALSE;
+  }
+
+  /**
+   * Support for fopen(), file_get_contents(), file_put_contents() etc.
+   *
+   * @see http://php.net/manual/en/streamwrapper.stream-open.php
+   *
+   * @param $path
+   *   A string containing the path to the file to open.
+   * @param $mode
+   *   The file mode ("r", "wb" etc.).
+   * @param $options
+   *   A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
+   * @param &$opened_path
+   *   A string containing the path actually opened.
+   * @return bool
+   *   TRUE if file was opened successfully.
+   */
+  public function stream_open($uri, $mode, $options, &$opened_url) {
+    resource_debug("stream open: $uri");
+    
+    $uri          = $this->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
--- /dev/null	2009-05-20 00:27:51.000000000 -0400
+++ includes/stream_wrapper_registry.inc	2009-06-13 09:33:21.000000000 -0400
@@ -0,0 +1,113 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Drupal stream wrapper manager
+ *
+ * Provide a class for managing and querying user defined stream wrappers
+ * in PHP. PHP's internal stream_get_wrappers doesn't return the class
+ * registered to handle a stream. We need to be able to find the handler
+ * for class instantiation.
+ *
+ * A stream is referenced as: scheme://target
+ */
+ 
+/**
+ * Drupal stream wrapper manager class
+ */ 
+class drupal_stream_wrapper_registry {
+  
+  private $wrappers = array(); // stream wrapper registry
+
+  // private constructor to enforce singleton.
+  private function __construct() { }
+
+  /**
+   * Load the singleton instance of the stream_wrapper_manager.
+   *
+   * @return object
+   *   A stream_wrapper_manager instance.
+   */
+  public static function singleton() {
+    static $instance = NULL;
+    if (is_null($instance)) {
+      $instance = new drupal_stream_wrapper_registry();
+    }
+    return $instance;
+  }
+
+  /**
+   * Register a stream wrapper scheme.
+   *
+   * @see: http://php.net/manual/en/function.stream-wrapper-register.php
+   *
+   * @param string $scheme 
+   *   URI scheme.
+   * @param string $class
+   *   Class name for the stream wrapper.
+   * @return bool 
+   *   result of stream_wrapper_register()
+   */
+  function register($scheme, $class_name) {
+    $this->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;
+  }
+}
