<?php
/**
*
*/
class Loco_fs_File {
/**
* @var Loco_fs_FileWriter
*/
private $w;
/**
* Full original path to file
* @var string
*/
private $path;
/**
* Cached pathinfo() data
* @var array
*/
private $info;
/**
* Base path which path has been normalized against
* @var string
*/
private $base;
/**
* Flag set when current path is relative
* @var bool
*/
private $rel;
/**
* Check if a path is absolute and return fixed slashes for readability
* @return string fixed path, or "" if not absolute
*/
public static function abs( $path ){
if( $path = (string) $path ){
$chr1 = $path{0};
// return unmodified path if starts "/"
if( '/' === $chr1 ){
return $path;
}
// Windows drive path if "X:" or network path if "\\"
if( isset($path{1}) ){
$chr2 = $path{1};
if( ':' === $chr2 || ( '\\' === $chr1 && '\\' === $chr2 ) ){
return strtoupper($chr1).$chr2.strtr( substr($path,2), '\\', '/' );
}
}
}
// else path is relative, so return falsey string
return '';
}
/**
* @internal
*/
public function __construct( $path ){
$this->setPath( $path );
}
/**
* Internally set path value and flag whether relative or absolute
*/
private function setPath( $path ){
$path = (string) $path;
if( $fixed = self::abs($path) ){
$path = $fixed;
$this->rel = false;
}
else {
$this->rel = true;
}
if( $path !== $this->path ){
$this->path = $path;
$this->info = null;
}
return $path;
}
/**
* @return array
*/
public function isAbsolute(){
return ! $this->rel;
}
/**
* @internal
*/
public function __clone(){
$this->cloneWriteContext( $this->w );
}
/**
* Copy write context with oursel
* @return
*/
private function cloneWriteContext( Loco_fs_FileWriter $context = null ){
if( $context ){
$context = clone $context;
$this->w = $context->setFile($this);
}
return $this;
}
/**
* Get file system context for operations that *modify* the file system.
* Read operations and operations that stat the file will always do so directly.
* @return Loco_fs_FileWriter
*/
public function getWriteContext(){
if( ! $this->w ){
$this->w = new Loco_fs_FileWriter( $this );
}
return $this->w;
}
/**
* @internal
*/
private function pathinfo(){
return is_array($this->info) ? $this->info : ( $this->info = pathinfo($this->path) );
}
/**
* @return bool
*/
public function exists(){
return file_exists( $this->path );
}
/**
* @return bool
*/
public function writable(){
return $this->getWriteContext()->writable();
}
/**
* @return bool
*/
public function deletable(){
$parent = $this->getParent();
if( $parent && $parent->writable() ){
// sticky directory requires that either the file its parent is owned by effective user
if( $parent->mode() & 01000 ){
$writer = $this->getWriteContext();
if( $writer->isDirect() && ( $uid = Loco_compat_PosixExtension::getuid() ) ){
return $uid === $this->uid() || $uid === $parent->uid();
}
// else delete operation won't be done directly, so can't pre-empt sticky problems
// TODO is it worth comparing FTP username etc.. for ownership?
}
// defaulting to "deletable" based on fact that parent is writable.
return true;
}
return false;
}
/**
* Get owner uid
* @return int
*/
public function uid(){
return fileowner($this->path);
}
/**
* Get group gid
* @return int
*/
public function gid(){
return filegroup($this->path);
}
/**
* Check if file can't be overwitten when existant, nor created when non-existant
* This does not check permissions recursively as directory trees are not built implicitly
* @return bool
*/
public function locked(){
if( $this->exists() ){
return ! $this->writable();
}
if( $dir = $this->getParent() ){
return ! $dir->writable();
}
return true;
}
/**
* Check if full path can be built to non-existant file.
* @return bool
*/
public function creatable(){
$file = $this;
while( $file = $file->getParent() ){
if( $file->exists() ){
return $file->writable();
}
}
return false;
}
/**
* @return string
*/
public function dirname(){
$info = $this->pathinfo();
return $info['dirname'];
}
/**
* @return string
*/
public function basename(){
$info = $this->pathinfo();
return $info['basename'];
}
/**
* @return string
*/
public function filename(){
$info = $this->pathinfo();
return $info['filename'];
}
/**
* @return string
*/
public function extension(){
$info = $this->pathinfo();
return isset($info['extension']) ? $info['extension'] : '';
}
/**
* @return string
*/
public function getPath(){
return $this->path;
}
/**
* @return int
*/
public function modified(){
return filemtime( $this->path );
}
/**
* @return int
*/
public function size(){
return filesize( $this->path );
}
/**
* @return int
*/
public function mode(){
if( is_link($this->path) ){
$stat = lstat( $this->path );
$mode = $stat[2];
}
else {
$mode = fileperms($this->path);
}
return $mode;
}
/**
* Set file mode
* @return Loco_fs_File
*/
public function chmod( $mode, $recursive = false ){
$this->getWriteContext()->chmod( $mode, $recursive );
return $this->clearStat();
}
/**
* Clear stat cache if any file data has changed
* @return Loco_fs_File
*/
public function clearStat(){
$this->info = null;
// PHP 5.3.0 Added optional clear_realpath_cache and filename parameters.
if( version_compare( PHP_VERSION, '5.3.0', '>=' ) ){
clearstatcache( true, $this->path );
}
// else no choice but to drop entire stat cache
else {
clearstatcache();
}
return $this;
}
/**
* @return string
*/
public function __toString(){
return $this->getPath();
}
/**
* Check if passed path is equal to ours
* @param string
* @return bool
*/
public function equal( $path ){
return $this->path === (string) $path;
}
/**
* Normalize path for string comparison, resolves redundant dots and slashes.
* @param string path to prefix
* @return string
*/
public function normalize( $base = '' ){
if( $path = self::abs($base) ){
$base = $path;
}
if( $base !== $this->base ){
$path = $this->path;
if( '' === $path ){
$this->setPath($base);
}
else {
if( ! $this->rel || ! $base ){
$b = array();
}
else {
$b = self::explode( $base, array() );
}
$b = self::explode( $path, $b );
$this->setPath( implode('/',$b) );
}
$this->base = $base;
}
return $this->path;
}
/**
*
*/
private static function explode( $path, array $b ){
$a = explode( '/', $path );
foreach( $a as $i => $s ){
if( '' === $s ){
if( 0 !== $i ){
continue;
}
}
if( '.' === $s ){
continue;
}
if( '..' === $s ){
if( array_pop($b) ){
continue;
}
}
$b[] = $s;
}
return $b;
}
/**
* Get path relative to given location, unless path is already relative
* @return string
*/
public function getRelativePath( $base ){
$path = $this->normalize();
if( $abspath = self::abs($path) ){
// base may needs require normalizing
$file = new Loco_fs_File($base);
$base = $file->normalize();
$length = strlen($base);
// if we are below given base path, return ./relative
if( substr($path,0,$length) === $base ){
++$length;
if( isset($path{$length}) ){
return substr( $path, $length );
}
// else paths were idenitcal
return '';
}
// else attempt to find nearest common root
$i = 0;
$source = explode('/',$base);
$target = explode('/',$path);
while( isset($source[$i]) && isset($target[$i]) && $source[$i] === $target[$i] ){
$i++;
}
if( $i > 1 ){
$depth = count($source) - $i;
$build = array_merge( array_fill( 0, $depth, '..' ), array_slice( $target, $i ) );
$path = implode( '/', $build );
}
}
// else return unmodified
return $path;
}
/**
* @return bool
*/
public function isDirectory(){
if( file_exists($this->path) ){
return is_dir($this->path);
}
return ! $this->extension();
}
/**
* Load contents of file into a string
* @return string
*/
public function getContents(){
return file_get_contents( $this->path );
}
/**
* Check if path is under a theme directory
* @return bool
*/
public function underThemeDirectory(){
return Loco_fs_Locations::getThemes()->check( $this->path );
}
/**
* Check if path is under a plugin directory
* @return bool
*/
public function underPluginDirectory(){
return Loco_fs_Locations::getPlugins()->check( $this->path );
}
/**
* Check if path is under a global system directory
* @return bool
*/
public function underGlobalDirectory(){
return Loco_fs_Locations::getGlobal()->check( $this->path );
}
/**
* @return Loco_fs_Directory
*/
public function getParent(){
$path = $this->dirname();
if( '.' !== $path && $this->path !== $path ){
$dir = new Loco_fs_Directory( $path );
$dir->cloneWriteContext( $this->w );
return $dir;
}
}
/**
* Copy this file for real
* @throws Loco_error_Exception
* @return Loco_fs_File new file
*/
public function copy( $dest ){
$copy = clone $this;
$copy->path = $dest;
$copy->clearStat();
$this->getWriteContext()->copy( $copy );
return $copy;
}
/**
* Delete this file for real
* @throws Loco_error_Exception
* @return Loco_fs_File
*/
public function unlink(){
$recursive = $this->isDirectory();
$this->getWriteContext()->delete( $recursive );
return $this->clearStat();
}
/**
* Copy this object with an alternative file extension
* @return Loco_fs_File
*/
public function cloneExtension( $ext ){
$snip = strlen( $this->extension() );
$file = clone $this;
if( $snip ){
$file->path = substr_replace( $this->path, $ext, - $snip );
}
else {
$file->path .= '.'.$ext;
}
$file->info = null;
return $file;
}
/**
* Ensure full parent directory tree exists
* @return Loco_fs_Directory
*/
public function createParent(){
if( $dir = $this->getParent() ){
if( ! $dir->exists() ){
$dir->mkdir();
}
}
return $dir;
}
/**
* @return int bytes written to file
*/
public function putContents( $data ){
$this->getWriteContext()->putContents($data);
$this->clearStat();
return $this->size();
}
}