blob: 73d328001fdbe8c4a3d52040786f0f1efd3e0188 [file] [log] [blame]
<?php
/**
* This file is part of vfsStream.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package org\bovigo\vfs
*/
namespace org\bovigo\vfs;
/**
* Stream wrapper to mock file system requests.
*/
class vfsStreamWrapper
{
/**
* open file for reading
*/
const READ = 'r';
/**
* truncate file
*/
const TRUNCATE = 'w';
/**
* set file pointer to end, append new data
*/
const APPEND = 'a';
/**
* set file pointer to start, overwrite existing data
*/
const WRITE = 'x';
/**
* set file pointer to start, overwrite existing data; or create file if
* does not exist
*/
const WRITE_NEW = 'c';
/**
* file mode: read only
*/
const READONLY = 0;
/**
* file mode: write only
*/
const WRITEONLY = 1;
/**
* file mode: read and write
*/
const ALL = 2;
/**
* switch whether class has already been registered as stream wrapper or not
*
* @type bool
*/
protected static $registered = false;
/**
* root content
*
* @type vfsStreamContent
*/
protected static $root;
/**
* disk space quota
*
* @type Quota
*/
private static $quota;
/**
* file mode: read only, write only, all
*
* @type int
*/
protected $mode;
/**
* shortcut to file container
*
* @type vfsStreamFile
*/
protected $content;
/**
* shortcut to directory container
*
* @type vfsStreamDirectory
*/
protected $dir;
/**
* shortcut to directory container iterator
*
* @type vfsStreamDirectory
*/
protected $dirIterator;
/**
* method to register the stream wrapper
*
* Please be aware that a call to this method will reset the root element
* to null.
* If the stream is already registered the method returns silently. If there
* is already another stream wrapper registered for the scheme used by
* vfsStream a vfsStreamException will be thrown.
*
* @throws vfsStreamException
*/
public static function register()
{
self::$root = null;
self::$quota = Quota::unlimited();
if (true === self::$registered) {
return;
}
if (@stream_wrapper_register(vfsStream::SCHEME, __CLASS__) === false) {
throw new vfsStreamException('A handler has already been registered for the ' . vfsStream::SCHEME . ' protocol.');
}
self::$registered = true;
}
/**
* sets the root content
*
* @param vfsStreamContainer $root
* @return vfsStreamContainer
*/
public static function setRoot(vfsStreamContainer $root)
{
self::$root = $root;
return self::$root;
}
/**
* returns the root content
*
* @return vfsStreamContainer
*/
public static function getRoot()
{
return self::$root;
}
/**
* sets quota for disk space
*
* @param Quota $quota
* @since 1.1.0
*/
public static function setQuota(Quota $quota)
{
self::$quota = $quota;
}
/**
* returns content for given path
*
* @param string $path
* @return vfsStreamContent
*/
protected function getContent($path)
{
if (null === self::$root) {
return null;
}
if (self::$root->getName() === $path) {
return self::$root;
}
if ($this->isInRoot($path) && self::$root->hasChild($path) === true) {
return self::$root->getChild($path);
}
return null;
}
/**
* helper method to detect whether given path is in root path
*
* @param string $path
* @return bool
*/
private function isInRoot($path)
{
return substr($path, 0, strlen(self::$root->getName())) === self::$root->getName();
}
/**
* returns content for given path but only when it is of given type
*
* @param string $path
* @param int $type
* @return vfsStreamContent
*/
protected function getContentOfType($path, $type)
{
$content = $this->getContent($path);
if (null !== $content && $content->getType() === $type) {
return $content;
}
return null;
}
/**
* splits path into its dirname and the basename
*
* @param string $path
* @return string[]
*/
protected function splitPath($path)
{
$lastSlashPos = strrpos($path, '/');
if (false === $lastSlashPos) {
return array('dirname' => '', 'basename' => $path);
}
return array('dirname' => substr($path, 0, $lastSlashPos),
'basename' => substr($path, $lastSlashPos + 1)
);
}
/**
* helper method to resolve a path from /foo/bar/. to /foo/bar
*
* @param string $path
* @return string
*/
protected function resolvePath($path)
{
$newPath = array();
foreach (explode('/', $path) as $pathPart) {
if ('.' !== $pathPart) {
if ('..' !== $pathPart) {
$newPath[] = $pathPart;
} else {
array_pop($newPath);
}
}
}
return implode('/', $newPath);
}
/**
* open the stream
*
* @param string $path the path to open
* @param string $mode mode for opening
* @param string $options options for opening
* @param string $opened_path full path that was actually opened
* @return bool
*/
public function stream_open($path, $mode, $options, $opened_path)
{
$extended = ((strstr($mode, '+') !== false) ? (true) : (false));
$mode = str_replace(array('b', '+'), '', $mode);
if (in_array($mode, array('r', 'w', 'a', 'x', 'c')) === false) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Illegal mode ' . $mode . ', use r, w, a, x or c, flavoured with b and/or +', E_USER_WARNING);
}
return false;
}
$this->mode = $this->calculateMode($mode, $extended);
$path = $this->resolvePath(vfsStream::path($path));
$this->content = $this->getContentOfType($path, vfsStreamContent::TYPE_FILE);
if (null !== $this->content) {
if (self::WRITE === $mode) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('File ' . $path . ' already exists, can not open with mode x', E_USER_WARNING);
}
return false;
}
if (
(self::TRUNCATE === $mode || self::APPEND === $mode) &&
$this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false
) {
return false;
}
if (self::TRUNCATE === $mode) {
$this->content->openWithTruncate();
} elseif (self::APPEND === $mode) {
$this->content->openForAppend();
} else {
if (!$this->content->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Permission denied', E_USER_WARNING);
}
return false;
}
$this->content->open();
}
return true;
}
$content = $this->createFile($path, $mode, $options);
if (false === $content) {
return false;
}
$this->content = $content;
return true;
}
/**
* creates a file at given path
*
* @param string $path the path to open
* @param string $mode mode for opening
* @param string $options options for opening
* @return bool
*/
private function createFile($path, $mode = null, $options = null)
{
$names = $this->splitPath($path);
if (empty($names['dirname']) === true) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('File ' . $names['basename'] . ' does not exist', E_USER_WARNING);
}
return false;
}
$dir = $this->getContentOfType($names['dirname'], vfsStreamContent::TYPE_DIR);
if (null === $dir) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Directory ' . $names['dirname'] . ' does not exist', E_USER_WARNING);
}
return false;
} elseif ($dir->hasChild($names['basename']) === true) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Directory ' . $names['dirname'] . ' already contains a director named ' . $names['basename'], E_USER_WARNING);
}
return false;
}
if (self::READ === $mode) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Can not open non-existing file ' . $path . ' for reading', E_USER_WARNING);
}
return false;
}
if ($dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
trigger_error('Can not create new file in non-writable path ' . $names['dirname'], E_USER_WARNING);
}
return false;
}
return vfsStream::newFile($names['basename'])->at($dir);
}
/**
* calculates the file mode
*
* @param string $mode opening mode: r, w, a or x
* @param bool $extended true if + was set with opening mode
* @return int
*/
protected function calculateMode($mode, $extended)
{
if (true === $extended) {
return self::ALL;
}
if (self::READ === $mode) {
return self::READONLY;
}
return self::WRITEONLY;
}
/**
* closes the stream
*
* @see https://github.com/mikey179/vfsStream/issues/40
*/
public function stream_close()
{
$this->content->lock($this, LOCK_UN);
}
/**
* read the stream up to $count bytes
*
* @param int $count amount of bytes to read
* @return string
*/
public function stream_read($count)
{
if (self::WRITEONLY === $this->mode) {
return '';
}
if ($this->content->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return '';
}
return $this->content->read($count);
}
/**
* writes data into the stream
*
* @param string $data
* @return int amount of bytes written
*/
public function stream_write($data)
{
if (self::READONLY === $this->mode) {
return 0;
}
if ($this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return 0;
}
if (self::$quota->isLimited()) {
$data = substr($data, 0, self::$quota->spaceLeft(self::$root->sizeSummarized()));
}
return $this->content->write($data);
}
/**
* truncates a file to a given length
*
* @param int $size length to truncate file to
* @return bool
* @since 1.1.0
*/
public function stream_truncate($size)
{
if (self::READONLY === $this->mode) {
return false;
}
if ($this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return false;
}
if ($this->content->getType() !== vfsStreamContent::TYPE_FILE) {
return false;
}
if (self::$quota->isLimited() && $this->content->size() < $size) {
$maxSize = self::$quota->spaceLeft(self::$root->sizeSummarized());
if (0 === $maxSize) {
return false;
}
if ($size > $maxSize) {
$size = $maxSize;
}
}
return $this->content->truncate($size);
}
/**
* sets metadata like owner, user or permissions
*
* @param string $path
* @param int $option
* @param mixed $var
* @return bool
* @since 1.1.0
*/
public function stream_metadata($path, $option, $var)
{
$path = $this->resolvePath(vfsStream::path($path));
$content = $this->getContent($path);
switch ($option) {
case STREAM_META_TOUCH:
if (null === $content) {
$content = $this->createFile($path);
}
if (isset($var[0])) {
$content->lastModified($var[0]);
}
if (isset($var[1])) {
$content->lastAccessed($var[1]);
}
return true;
case STREAM_META_OWNER_NAME:
return false;
case STREAM_META_OWNER:
if (null === $content) {
return false;
}
return $this->doPermChange($path,
$content,
function() use ($content, $var)
{
$content->chown($var);
}
);
case STREAM_META_GROUP_NAME:
return false;
case STREAM_META_GROUP:
if (null === $content) {
return false;
}
return $this->doPermChange($path,
$content,
function() use ($content, $var)
{
$content->chgrp($var);
}
);
case STREAM_META_ACCESS:
if (null === $content) {
return false;
}
return $this->doPermChange($path,
$content,
function() use ($content, $var)
{
$content->chmod($var);
}
);
default:
return false;
}
}
/**
* executes given permission change when necessary rights allow such a change
*
* @param string $path
* @param vfsStreamAbstractContent $content
* @param Closure $change
* @return bool
*/
private function doPermChange($path, vfsStreamAbstractContent $content, \Closure $change)
{
if (!$content->isOwnedByUser(vfsStream::getCurrentUser())) {
return false;
}
if (self::$root->getName() !== $path) {
$names = $this->splitPath($path);
$parent = $this->getContent($names['dirname']);
if (!$parent->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
return false;
}
}
$change();
return true;
}
/**
* checks whether stream is at end of file
*
* @return bool
*/
public function stream_eof()
{
return $this->content->eof();
}
/**
* returns the current position of the stream
*
* @return int
*/
public function stream_tell()
{
return $this->content->getBytesRead();
}
/**
* seeks to the given offset
*
* @param int $offset
* @param int $whence
* @return bool
*/
public function stream_seek($offset, $whence)
{
return $this->content->seek($offset, $whence);
}
/**
* flushes unstored data into storage
*
* @return bool
*/
public function stream_flush()
{
return true;
}
/**
* returns status of stream
*
* @return array
*/
public function stream_stat()
{
$fileStat = array('dev' => 0,
'ino' => 0,
'mode' => $this->content->getType() | $this->content->getPermissions(),
'nlink' => 0,
'uid' => $this->content->getUser(),
'gid' => $this->content->getGroup(),
'rdev' => 0,
'size' => $this->content->size(),
'atime' => $this->content->fileatime(),
'mtime' => $this->content->filemtime(),
'ctime' => $this->content->filectime(),
'blksize' => -1,
'blocks' => -1
);
return array_merge(array_values($fileStat), $fileStat);
}
/**
* retrieve the underlaying resource
*
* Please note that this method always returns false as there is no
* underlaying resource to return.
*
* @param int $cast_as
* @since 0.9.0
* @see https://github.com/mikey179/vfsStream/issues/3
* @return bool
*/
public function stream_cast($cast_as)
{
return false;
}
/**
* set lock status for stream
*
* @param int $operation
* @return bool
* @since 0.10.0
* @see https://github.com/mikey179/vfsStream/issues/6
* @see https://github.com/mikey179/vfsStream/issues/31
* @see https://github.com/mikey179/vfsStream/issues/40
*/
public function stream_lock($operation)
{
if ((LOCK_NB & $operation) == LOCK_NB) {
$operation = $operation - LOCK_NB;
}
return $this->content->lock($this, $operation);
}
/**
* sets options on the stream
*
* @param int $option key of option to set
* @param int $arg1
* @param int $arg2
* @return bool
* @since 0.10.0
* @see https://github.com/mikey179/vfsStream/issues/15
* @see http://www.php.net/manual/streamwrapper.stream-set-option.php
*/
public function stream_set_option($option, $arg1, $arg2)
{
switch ($option) {
case STREAM_OPTION_BLOCKING:
// break omitted
case STREAM_OPTION_READ_TIMEOUT:
// break omitted
case STREAM_OPTION_WRITE_BUFFER:
// break omitted
default:
// nothing to do here
}
return false;
}
/**
* remove the data under the given path
*
* @param string $path
* @return bool
*/
public function unlink($path)
{
$realPath = $this->resolvePath(vfsStream::path($path));
$content = $this->getContent($realPath);
if (null === $content) {
return false;
}
if ($content->getType() !== vfsStreamContent::TYPE_FILE) {
trigger_error('unlink(' . $path . '): Operation not permitted', E_USER_WARNING);
return false;
}
return $this->doUnlink($realPath);
}
/**
* removes a path
*
* @param string $path
* @return bool
*/
protected function doUnlink($path)
{
if (self::$root->getName() === $path) {
// delete root? very brave. :)
self::$root = null;
clearstatcache();
return true;
}
$names = $this->splitPath($path);
$content = $this->getContent($names['dirname']);
if (!$content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
return false;
}
clearstatcache();
return $content->removeChild($names['basename']);
}
/**
* rename from one path to another
*
* @param string $path_from
* @param string $path_to
* @return bool
* @author Benoit Aubuchon
*/
public function rename($path_from, $path_to)
{
$srcRealPath = $this->resolvePath(vfsStream::path($path_from));
$dstRealPath = $this->resolvePath(vfsStream::path($path_to));
$srcContent = $this->getContent($srcRealPath);
if (null == $srcContent) {
trigger_error(' No such file or directory', E_USER_WARNING);
return false;
}
$dstNames = $this->splitPath($dstRealPath);
$dstParentContent = $this->getContent($dstNames['dirname']);
if (null == $dstParentContent) {
trigger_error('No such file or directory', E_USER_WARNING);
return false;
}
if (!$dstParentContent->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
trigger_error('Permission denied', E_USER_WARNING);
return false;
}
if ($dstParentContent->getType() !== vfsStreamContent::TYPE_DIR) {
trigger_error('Target is not a directory', E_USER_WARNING);
return false;
}
// remove old source first, so we can rename later
// (renaming first would lead to not being able to remove the old path)
if (!$this->doUnlink($srcRealPath)) {
return false;
}
$dstContent = $srcContent;
// Renaming the filename
$dstContent->rename($dstNames['basename']);
// Copying to the destination
$dstParentContent->addChild($dstContent);
return true;
}
/**
* creates a new directory
*
* @param string $path
* @param int $mode
* @param int $options
* @return bool
*/
public function mkdir($path, $mode, $options)
{
$umask = vfsStream::umask();
if (0 < $umask) {
$permissions = $mode & ~$umask;
} else {
$permissions = $mode;
}
$path = $this->resolvePath(vfsStream::path($path));
if (null !== $this->getContent($path)) {
trigger_error('mkdir(): Path vfs://' . $path . ' exists', E_USER_WARNING);
return false;
}
if (null === self::$root) {
self::$root = vfsStream::newDirectory($path, $permissions);
return true;
}
$maxDepth = count(explode('/', $path));
$names = $this->splitPath($path);
$newDirs = $names['basename'];
$dir = null;
$i = 0;
while ($dir === null && $i < $maxDepth) {
$dir = $this->getContent($names['dirname']);
$names = $this->splitPath($names['dirname']);
if (null == $dir) {
$newDirs = $names['basename'] . '/' . $newDirs;
}
$i++;
}
if (null === $dir
|| $dir->getType() !== vfsStreamContent::TYPE_DIR
|| $dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return false;
}
$recursive = ((STREAM_MKDIR_RECURSIVE & $options) !== 0) ? (true) : (false);
if (strpos($newDirs, '/') !== false && false === $recursive) {
return false;
}
vfsStream::newDirectory($newDirs, $permissions)->at($dir);
return true;
}
/**
* removes a directory
*
* @param string $path
* @param int $options
* @return bool
* @todo consider $options with STREAM_MKDIR_RECURSIVE
*/
public function rmdir($path, $options)
{
$path = $this->resolvePath(vfsStream::path($path));
$child = $this->getContentOfType($path, vfsStreamContent::TYPE_DIR);
if (null === $child) {
return false;
}
// can only remove empty directories
if (count($child->getChildren()) > 0) {
return false;
}
if (self::$root->getName() === $path) {
// delete root? very brave. :)
self::$root = null;
clearstatcache();
return true;
}
$names = $this->splitPath($path);
$dir = $this->getContentOfType($names['dirname'], vfsStreamContent::TYPE_DIR);
if ($dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return false;
}
clearstatcache();
return $dir->removeChild($child->getName());
}
/**
* opens a directory
*
* @param string $path
* @param int $options
* @return bool
*/
public function dir_opendir($path, $options)
{
$path = $this->resolvePath(vfsStream::path($path));
$this->dir = $this->getContentOfType($path, vfsStreamContent::TYPE_DIR);
if (null === $this->dir || $this->dir->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
return false;
}
$this->dirIterator = $this->dir->getIterator();
return true;
}
/**
* reads directory contents
*
* @return string
*/
public function dir_readdir()
{
$dir = $this->dirIterator->current();
if (null === $dir) {
return false;
}
$this->dirIterator->next();
return $dir->getName();
}
/**
* reset directory iteration
*
* @return bool
*/
public function dir_rewinddir()
{
return $this->dirIterator->rewind();
}
/**
* closes directory
*
* @return bool
*/
public function dir_closedir()
{
$this->dirIterator = null;
return true;
}
/**
* returns status of url
*
* @param string $path path of url to return status for
* @param int $flags flags set by the stream API
* @return array
*/
public function url_stat($path, $flags)
{
$content = $this->getContent($this->resolvePath(vfsStream::path($path)));
if (null === $content) {
if (($flags & STREAM_URL_STAT_QUIET) != STREAM_URL_STAT_QUIET) {
trigger_error(' No such file or directory: ' . $path, E_USER_WARNING);
}
return false;
}
$fileStat = array('dev' => 0,
'ino' => 0,
'mode' => $content->getType() | $content->getPermissions(),
'nlink' => 0,
'uid' => $content->getUser(),
'gid' => $content->getGroup(),
'rdev' => 0,
'size' => $content->size(),
'atime' => $content->fileatime(),
'mtime' => $content->filemtime(),
'ctime' => $content->filectime(),
'blksize' => -1,
'blocks' => -1
);
return array_merge(array_values($fileStat), $fileStat);
}
}
?>