Commit 5468b47b authored by Dries's avatar Dries
Browse files

- Patch #227232 by dopry, c960657, jmstacey, pwolanin, aaron, drewish: added...

- Patch #227232 by dopry, c960657, jmstacey, pwolanin, aaron, drewish: added initial support for PHP file wrappers.
parent 1aec2983
......@@ -103,6 +103,9 @@ Drupal 7.0, xxxx-xx-xx (development version)
uploading a site logo--that don't require the overhead of databases and
hooks, the current unmanaged copy, move and delete operations have been
preserved but renamed to file_unmanaged_*().
* Rewrote file handling to use PHP stream wrappers to enable support for
both public and private files and to support pluggable storage mechanisms
and access to remote resources (e.g. S3 storage or Flickr photos).
- Image handling:
* Improved image handling, including better support for add-on image
libraries.
......
......@@ -3451,7 +3451,8 @@ function _drupal_bootstrap_full() {
fix_gpc_magic();
// Load all enabled modules
module_load_all();
// Make sure all stream wrappers are registered.
file_get_stream_wrappers();
// Let all modules take action before menu system handles the request
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
......
......@@ -6,6 +6,13 @@
* API for handling file uploads and server file management.
*/
/**
* Stream wrapper code is included here because there are cases where
* File API is needed before a bootstrap, or in an alternate order (e.g.
* maintenance theme).
*/
require_once DRUPAL_ROOT . '/includes/stream_wrappers.inc';
/**
* @defgroup file File interface
* @{
......@@ -76,6 +83,221 @@
*/
define('FILE_STATUS_PERMANENT', 1);
/**
* Methods to manage a registry of stream wrappers.
*/
/**
* Drupal stream wrapper registry.
*
* A stream wrapper is an abstraction of a file system that allows Drupal to
* use the same set of methods to access both local files and remote resources.
*
* Provide a facility 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, which we need to be able to find the handler
* for class instantiation.
*
* If a module registers a scheme that is already registered with PHP, the
* existing scheme will be unregistered and replaced with the specified class.
*
* A stream is referenced as "scheme://target".
*
* @return
* Returns the entire Drupal stream wrapper registry.
* @see hook_stream_wrappers()
* @see hook_stream_wrappers_alter()
*/
function file_get_stream_wrappers() {
$wrappers = &drupal_static(__FUNCTION__);
if (!isset($wrappers)) {
$wrappers = module_invoke_all('stream_wrappers');
drupal_alter('stream_wrappers', $wrappers);
$existing = stream_get_wrappers();
foreach ($wrappers as $scheme => $info) {
// We only register classes that implement our interface.
if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
// Record whether we are overriding an existing scheme.
if (in_array($scheme, $existing, TRUE)) {
$wrappers[$scheme]['override'] = TRUE;
stream_wrapper_unregister($scheme);
}
else {
$wrappers[$scheme]['override'] = FALSE;
}
stream_wrapper_register($scheme, $info['class']);
}
}
}
return $wrappers;
}
/**
* Returns the stream wrapper class name for a given scheme.
*
* @param $scheme
* Stream scheme.
* @return
* Return string if a scheme has a registered handler, or FALSE.
*/
function file_stream_wrapper_get_class($scheme) {
$wrappers = file_get_stream_wrappers();
return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
}
/**
* Returns the scheme of a URI (e.g. a stream).
*
* @param $uri
* A stream, referenced as "scheme://target".
* @return
* A string containing the name of the scheme, or FALSE if none. For example,
* the URI "public://example.txt" would return "public".
*/
function file_uri_scheme($uri) {
$data = explode('://', $uri, 2);
return count($data) == 2 ? $data[0] : FALSE;
}
/**
* Check that the scheme of a stream URI is valid.
*
* Confirms that there is a registered stream handler for the provided scheme
* and that it is callable. This is useful if you want to confirm a valid
* scheme without creating a new instance of the registered handler.
*
* @param $scheme
* A URI scheme, a stream is referenced as "scheme://target".
* @return
* Returns TRUE if the string is the name of a validated stream,
* or FALSE if the scheme does not have a registered handler.
*/
function file_stream_wrapper_valid_scheme($scheme) {
// Does the scheme have a registered handler that is callable?
$class = file_stream_wrapper_get_class($scheme);
if (class_exists($class)) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Returns the target of a URI (e.g. a stream).
*
* @param $uri
* A stream, referenced as "scheme://target".
* @return
* A string containing the target (path), or FALSE if none.
* For example, the URI "public://sample/test.txt" would return
* "sample/test.txt".
*/
function file_uri_target($uri) {
$data = explode('://', $uri, 2);
if (count($data) != 2) {
return FALSE;
}
// Remove erroneous beginning forward slash.
$data[1] = ltrim($data[1], '\/');
return $data[1];
}
/**
* Normalizes a URI by making it syntactically correct.
*
* A stream is referenced as "scheme://target".
*
* The following actions are taken:
* - Remove all occurrences of the wrapper's directory path
* - Remove trailing slashes from target
* - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
*
* @param $uri
* String reference containing the URI to normalize.
*/
function file_stream_wrapper_uri_normalize($uri) {
$scheme = file_uri_scheme($uri);
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
$target = file_uri_target($uri);
// Remove all occurrences of the wrapper's directory path.
$directory_path = file_stream_wrapper_get_instance_by_scheme($scheme)->getDirectoryPath();
$target = str_replace($directory_path, '', $target);
// Trim trailing slashes from target.
$target = rtrim($target, '/');
// Trim erroneous leading slashes from target.
$uri = $scheme . '://' . ltrim($target, '/');
}
return $uri;
}
/**
* Returns a reference to the stream wrapper class responsible for a 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
* Returns a new stream wrapper object appropriate for the given URI or FALSE
* if no registered handler could be found. For example, a URI of
* "private://example.txt" would return a new private stream wrapper object
* (DrupalPrivateStreamWrapper).
*/
function file_stream_wrapper_get_instance_by_uri($uri) {
$scheme = file_uri_scheme($uri);
$class = file_stream_wrapper_get_class($scheme);
if (class_exists($class)) {
$instance = new $class;
$instance->setUri($uri);
return $instance;
}
else {
return FALSE;
}
}
/**
* Returns a reference to the stream wrapper class responsible for a given scheme.
*
* This helper method returns a stream instance using a scheme. That is, the
* passed string does not contain a "://". For example, "public" is a scheme
* but "public://" is a URI (stream). This is because the later contains both
* a scheme and target despite target being empty.
*
* Note: the instance URI will be initialized to "scheme://" so that you can
* make the customary method calls as if you had retrieved an instance by URI.
*
* @param $scheme
* If the stream was "public://target", "public" would be the scheme.
* @return
* Returns a new stream wrapper object appropriate for the given $scheme.
* For example, for the public scheme a stream wrapper object
* (DrupalPublicStreamWrapper).
* FALSE is returned if no registered handler could be found.
*/
function file_stream_wrapper_get_instance_by_scheme($scheme) {
$class = file_stream_wrapper_get_class($scheme);
if (class_exists($class)) {
$instance = new $class;
$instance->setUri($scheme . '://');
return $instance;
}
else {
return FALSE;
}
}
/**
* Create the download path to a file.
*
......
<?php
// $Id$
/**
* @file
* Drupal stream wrapper interface.
*
* Provides a Drupal interface and classes to implement PHP stream wrappers for
* public, private, and temporary files.
*
* A stream wrapper is an abstraction of a file system that allows Drupal to
* use the same set of methods to access both local files and remote resources.
*
* Note that PHP 5.2 fopen() only supports URIs of the form "scheme://target"
* despite the fact that according to RFC 3986 a URI's scheme component
* delimiter is in general just ":", not "://". Becuase of this PHP limitation
* and for consistency Drupal will only accept URIs of form "scheme://target".
*
* @link http://www.faqs.org/rfcs/rfc3986.html
* @link http://bugs.php.net/bug.php?id=47070
*/
/**
* Generic PHP stream wrapper interface.
*
* @see http://www.php.net/manual/en/class.streamwrapper.php
*/
interface StreamWrapperInterface {
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 StreamWrapperInterface with methods expected by Drupal stream
* wrapper classes.
*/
interface DrupalStreamWrapperInterface extends StreamWrapperInterface {
/**
* Set the absolute stream resource URI.
*
* This allows you to set the URI. Generally is only called by the factory
* method.
*
* @param $uri
* A string containing the URI that should be used for this instance.
*/
function setUri($uri);
/**
* Returns the stream resource URI.
*
* @return
* Returns the current URI of the instance.
*/
public function getUri();
/**
* Returns a web accessible URL for the resource.
*
* This function should return a URL that can be embedded in a web page
* and accessed from a browser. For example, the external URL of
* "youtube://xIpLd0WQKCY" might be
* "http://www.youtube.com/watch?v=xIpLd0WQKCY".
*
* @return
* Returns a string containing a web accessible URL for the resource.
*/
public function getExternalUrl();
/**
* Returns the MIME type of the resource.
*
* @param $uri
* The URI, path, or filename.
* @param $mapping
* An optional map of extensions to their mimetypes, in the form:
* - 'mimetypes': a list of mimetypes, keyed by an identifier,
* - 'extensions': the mapping itself, an associative array in which
* the key is the extension and the value is the mimetype identifier.
* @return
* Returns a string containing the MIME type of the resource.
*/
public static function getMimeType($uri, $mapping = NULL);
/**
* Changes permissions of the resource.
*
* PHP lacks this functionality and it is not part of the official stream
* wrapper interface. This is a custom implementation for Drupal.
*
* @param $mode
* Integer value for the permissions. Consult PHP chmod() documentation
* for more information.
* @return
* Returns TRUE on success or FALSE on failure.
*/
public function chmod($mode);
/**
* Returns canonical, absolute path of the resource.
*
* 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.
*
* @return
* Returns a string with absolute pathname on success (implemented
* by core wrappers), or FALSE on failure or if the registered
* wrapper does not provide an implementation.
*/
public function realpath();
}
/**
* Drupal stream wrapper base class for local files.
*
* This class provides a complete stream wrapper implementation. URIs such as
* "public://example.txt" are expanded to a normal filesystem path such as
* "sites/default/files/example.txt" and then PHP filesystem functions are
* invoked.
*
* DrupalLocalStreamWrapper implementations need to implement at least the
* getDirectoryPath() and getExternalUrl() methods.
*/
abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface {
/**
* Stream context resource.
*
* @var Resource
*/
public $context;
/**
* A generic resource handle.
*
* @var Resource
*/
public $handle = NULL;
/**
* Instance URI (stream).
*
* A stream is referenced as "scheme://target".
*
* @var String
*/
protected $uri;
/**
* Gets the path that the wrapper is responsible for.
*
* @return
* String specifying the path.
*/
abstract function getDirectoryPath();
/**
* Base implementation of setUri().
*/
function setUri($uri) {
$this->uri = $uri;
}
/**
* Base implementation of getUri().
*/
function getUri() {
return $this->uri;
}
/**
* Base implementation of getMimeType().
*/
static function getMimeType($uri, $mapping = NULL) {
if (!isset($mapping)) {
$mapping = variable_get('mime_extension_mapping', NULL);
if (!isset($mapping) && drupal_function_exists('file_default_mimetype_mapping')) {
// The default file map, defined in file.mimetypes.inc is quite big.
// We only load it when necessary.
$mapping = file_default_mimetype_mapping();
}
}
$extension = '';
$file_parts = explode('.', basename($uri));
// Remove the first part: a full filename should not match an extension.
array_shift($file_parts);
// Iterate over the file parts, trying to find a match.
// For my.awesome.image.jpeg, we try:
// - jpeg
// - image.jpeg, and
// - awesome.image.jpeg
while ($additional_part = array_pop($file_parts)) {
$extension = strtolower($additional_part . ($extension ? '.' . $extension : ''));
if (isset($mapping['extensions'][$extension])) {
return $mapping['mimetypes'][$mapping['extensions'][$extension]];
}
}
return 'application/octet-stream';
}
/**
* Base implementation of chmod().
*/
function chmod($mode) {
return @chmod($this->realpath(), $mode);
}
/**
* Base implementaiton of realpath().
*/
function realpath() {
return @realpath($this->getDirectoryPath() . '/' . file_uri_target($this->uri));
}
/**
* Return the local filesystem path.
*
* @param $uri
* Optional URI, supplied when doing a move or rename.
*/
protected function getLocalPath($uri = NULL) {
if (!isset($uri)) {
$uri = $this->uri;
}
return $this->getDirectoryPath() . '/' . file_uri_target($uri);
}
/**
* Support for fopen(), file_get_contents(), file_put_contents() etc.
*
* @param $uri
* 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
* Returns TRUE if file was opened successfully.
* @see http://php.net/manual/en/streamwrapper.stream-open.php
*/
public function stream_open($uri, $mode, $options, &$opened_path) {
$this->uri = $uri;
$path = $this->getLocalPath();
$this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
if ((bool)$this->handle && $options & STREAM_USE_PATH) {
$opened_url = $path;
}
return (bool)$this->handle;
}
/**
* Support for flock().
*
* @param $operation
* One of the following:
* - LOCK_SH to acquire a shared lock (reader).
* - LOCK_EX to acquire an exclusive lock (writer).
* - LOCK_UN to release a lock (shared or exclusive).
* - LOCK_NB if you don't want flock() to block while locking (not
* supported on Windows).
* @return
* Always returns TRUE at the present time.
* @see http://php.net/manual/en/streamwrapper.stream-lock.php
*/
public 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.
*
* @param $count
* Maximum number of bytes to be read.
* @return
* The string that was read, or FALSE in case of an error.
* @see http://php.net/manual/en/streamwrapper.stream-read.php
*/
public function stream_read($count) {
return fread($this->handle, $count);
}
/**
* Support for fwrite(), file_put_contents() etc.
*
* @param $data
* The string to be written.
* @return
* The number of bytes written (integer).
* @see http://php.net/manual/en/streamwrapper.stream-write.php
*/
public function stream_write($data) {
return fwrite($this->handle, $data);
}
/**
* Support for feof().
*
* @return
* TRUE if end-of-file has been reached.
* @see http://php.net/manual/en/streamwrapper.stream-eof.php
*/
public function stream_eof() {
return feof($this->handle);
}
/**
* Support for fseek().
*
* @param $offset
* The byte offset to got to.
* @param $whence
* SEEK_SET, SEEK_CUR, or SEEK_END.
* @return
* TRUE on success.
* @see http://php.net/manual/en/streamwrapper.stream-seek.php
*/
public function stream_seek($offset, $whence) {
return fseek($this->handle, $offset, $whence);
}
/**
* Support for fflush().
*
* @return
* TRUE if data was successfully stored (or there was no data to store).
* @see http://php.net/manual/en/streamwrapper.stream-flush.php
*/
public function stream_flush() {
return fflush($this->handle);
}
/**
* Support for ftell().
*
* @return
* The current offset in bytes from the beginning of file.
* @see http://php.net/manual/en/streamwrapper.stream-tell.php
*/
public function stream_tell() {
return ftell($this->handle);
}
/**
* Support for fstat().
*
* @return
* An array with file status, or FALSE in case of an error - see fstat()
* for a description of this array.
* @see http://php.net/manual/en/streamwrapper.stream-stat.php
*/
public function stream_stat() {