<?php

/* 
 *  $Id: FileSystem.php 4046 2005-04-22 13:41:56Z chregu $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information please see
 * <http://phing.info>. 
 */

/**
 * This is an abstract class for platform specific filesystem implementations
 * you have to implement each method in the platform specific filesystem implementation
 * classes Your local filesytem implementation must extend this class.
 * You should also use this class as a template to write your local implementation
 * Some native PHP filesystem specific methods are abstracted here as well. Anyway
 * you _must_ always use this methods via a File object (that by nature uses the
 * *FileSystem drivers to access the real filesystem via this class using natives.
 *
 * FIXME:
 *  - Error handling reduced to min fallthrough runtime excetions
 *    more precise errorhandling is done by the File class
 *    
 * @author Charlie Killian <charlie@tizac.com>
 * @author Hans Lellelid <hans@xmpl.org>
 * @version $Revision: 1.9 $
 * @package phing.system.io
 */
abstract class FileSystem {    

    /* properties for simple boolean attributes */
    const BA_EXISTS    = 0x01;
    const BA_REGULAR   = 0x02;
    const BA_DIRECTORY = 0x04;
    const BA_HIDDEN    = 0x08;
    
    /** Instance for getFileSystem() method. */
    private static $fs;
    
    /**
     * Static method to return the FileSystem singelton representing
     * this platform's local filesystem driver.
     */
    function getFileSystem() {
        if (self::$fs === null) {
            switch(Phing::getProperty('host.fstype')) {
                case 'UNIX':
                    include_once 'phing/system/io/UnixFileSystem.php';
                    self::$fs = new UnixFileSystem();
                break;
                case 'WIN32':
                    include_once 'phing/system/io/Win32FileSystem.php';
                    self::$fs = new Win32FileSystem();
                break;
                case 'WINNT':
                    include_once 'phing/system/io/WinNTFileSystem.php';
                    self::$fs = new WinNTFileSystem();
                break;
                default:
                    throw new Exception("Host uses unsupported filesystem, unable to proceed");
            }
        }
        return self::$fs;
    }

    /* -- Normalization and construction -- */

    /**
     * Return the local filesystem's name-separator character.
     */
    abstract function getSeparator();

    /**
     * Return the local filesystem's path-separator character.
     */
    abstract function getPathSeparator();

    /**
     * Convert the given pathname string to normal form.  If the string is
     * already in normal form then it is simply returned.
     */
    abstract function normalize($strPath);

    /**
     * Compute the length of this pathname string's prefix.  The pathname
     * string must be in normal form.
     */
    abstract function prefixLength($pathname);

    /**
     * Resolve the child pathname string against the parent.
     * Both strings must be in normal form, and the result
     * will be a string in normal form.
     */
    abstract function resolve($parent, $child);
    
    /**
     * Resolve the given abstract pathname into absolute form.  Invoked by the
     * getAbsolutePath and getCanonicalPath methods in the File class.
     */
    abstract function resolveFile(File $f);

    /**
     * Return the parent pathname string to be used when the parent-directory
     * argument in one of the two-argument File constructors is the empty
     * pathname.
     */
    abstract function getDefaultParent();

    /**
     * Post-process the given URI path string if necessary.  This is used on
     * win32, e.g., to transform "/c:/foo" into "c:/foo".  The path string
     * still has slash separators; code in the File class will translate them
     * after this method returns.
     */
    abstract function fromURIPath($path);

    /* -- Path operations -- */

    /**
     * Tell whether or not the given abstract pathname is absolute.
     */
    abstract function isAbsolute(File $f);

    /** 
     * canonicalize filename by checking on disk 
     * @return mixed Canonical path or false if the file doesn't exist.
     */
    function canonicalize($strPath) {
        return @realpath($strPath);        
    }

    /* -- Attribute accessors -- */

    /**
     * Return the simple boolean attributes for the file or directory denoted
     * by the given abstract pathname, or zero if it does not exist or some
     * other I/O error occurs.
     */
    function getBooleanAttributes($f) {
        throw new Exception("SYSTEM ERROR method getBooleanAttributes() not implemented by fs driver");
    }

    /**
     * Check whether the file or directory denoted by the given abstract
     * pathname may be accessed by this process.  If the second argument is
     * false, then a check for read access is made; if the second
     * argument is true, then a check for write (not read-write)
     * access is made.  Return false if access is denied or an I/O error
     * occurs.
     */
    function checkAccess(File $f, $write = false) {
        // we clear stat cache, its expensive to look up from scratch,
        // but we need to be sure
        @clearstatcache();


        // Shouldn't this be $f->GetAbsolutePath() ?
        // And why doesn't GetAbsolutePath() work?

        $strPath = (string) $f->getPath();

        // FIXME
        // if file object does denote a file that yet not existst
        // path rights are checked
        if (!@file_exists($strPath) && !is_dir($strPath)) {
            $strPath = $f->getParent();
            if ($strPath === null || !is_dir($strPath)) {
                $strPath = Phing::getProperty("user.dir");
            }
            //$strPath = dirname($strPath);
        }

        if (!$write) {
            return (boolean) @is_readable($strPath);
        } else {
            return (boolean) @is_writable($strPath);
        }
    }

    /**
     * Return the time at which the file or directory denoted by the given
     * abstract pathname was last modified, or zero if it does not exist or
     * some other I/O error occurs.
     */
    function getLastModifiedTime(File $f) {
        
        if (!$f->exists()) {
            return 0;
        }

        @clearstatcache();
        $strPath = (string) $f->getPath();
        $mtime = @filemtime($strPath);
        if (false === $mtime) {
            // FAILED. Log and return err.
            $msg = "FileSystem::Filemtime() FAILED. Cannot can not get modified time of $strPath. $php_errormsg";
            throw new Exception($msg);
        } else {
            return (int) $mtime;
        }
    }

    /**
     * Return the length in bytes of the file denoted by the given abstract
     * pathname, or zero if it does not exist, is a directory, or some other
     * I/O error occurs.
     */
    function getLength(File $f) {
        $strPath = (string) $f->getAbsolutePath();
        $fs = filesize((string) $strPath);
        if ($fs !== false) {
            return $fs;
        } else {
            $msg = "FileSystem::Read() FAILED. Cannot get filesize of $strPath. $php_errormsg";
            throw new Exception($msg);
        }
    }

    /* -- File operations -- */

    /**
     * Create a new empty file with the given pathname.  Return
     * true if the file was created and false if a
     * file or directory with the given pathname already exists.  Throw an
     * IOException if an I/O error occurs.
     *
     * @param       string      Path of the file to be created.
     *     
     * @throws      IOException
     */
    function createNewFile($strPathname) {
        if (@file_exists($strPathname))
            return false;
            
        // Create new file
        $fp = @fopen($strPathname, "w");
        if ($fp === false) {
            throw new IOException("The file \"$strPathname\" could not be created");            
        }
        @fclose($fp);        
        return true;
    }

    /**
     * Delete the file or directory denoted by the given abstract pathname,
     * returning true if and only if the operation succeeds.
     */
    function delete(File $f) {
        if ($f->isDirectory()) {
            return $this->rmdir($f->getPath());
        } else {
            return $this->unlink($f->getPath());
        }
    }

    /**
     * Arrange for the file or directory denoted by the given abstract
     * pathname to be deleted when Phing::shutdown is called, returning
    * true if and only if the operation succeeds.
     */
    function deleteOnExit($f) {
        throw new Exception("deleteOnExit() not implemented by local fs driver");
    }

    /**
     * List the elements of the directory denoted by the given abstract
     * pathname.  Return an array of strings naming the elements of the
     * directory if successful; otherwise, return <code>null</code>.
     */
    function listDir(File $f) {
        $strPath = (string) $f->getAbsolutePath();
        $d = @dir($strPath);
        if (!$d) {
            return null;
        }
        $list = array();
        while($entry = $d->read()) {
            if ($entry != "." && $entry != "..") {
                array_push($list, $entry);
            }
        }
        $d->close();
        unset($d);
        return $list;
    }

    /**
     * Create a new directory denoted by the given abstract pathname,
     * returning true if and only if the operation succeeds.
     */
    function createDirectory(&$f) {
        return @mkdir($f->getAbsolutePath(),0755);
    }

    /**
     * Rename the file or directory denoted by the first abstract pathname to
     * the second abstract pathname, returning true if and only if
     * the operation succeeds.
     *
     * @param File $f1 abstract source file
     * @param File $f2 abstract destination file
     * @return void    
     * @throws Exception if rename cannot be performed
     */
    function rename(File $f1, File $f2) {        
        // get the canonical paths of the file to rename
        $src = $f1->getAbsolutePath();
        $dest = $f2->getAbsolutePath();
        if (false === @rename($src, $dest)) {
            $msg = "Rename FAILED. Cannot rename $src to $dest. $php_errormsg";
            throw new Exception($msg);
        }
    }

    /**
     * Set the last-modified time of the file or directory denoted by the
     * given abstract pathname returning true if and only if the
     * operation succeeds.
     * @return void
     * @throws Exception
     */
    function setLastModifiedTime(File $f, $time) {        
        $path = $f->getPath();
        $success = @touch($path, $time);
        if (!$success) {
            throw new Exception("Could not create directory due to: $php_errormsg");
        }
    }

    /**
     * Mark the file or directory denoted by the given abstract pathname as
     * read-only, returning <code>true</code> if and only if the operation
     * succeeds.
     */
    function setReadOnly($f) {
        throw new Exception("setReadonle() not implemented by local fs driver");
    }

    /* -- Filesystem interface -- */

    /**
     * List the available filesystem roots, return array of File objects
     */
    function listRoots() {
        throw new Exception("SYSTEM ERROR [listRoots() not implemented by local fs driver]");
    }

    /* -- Basic infrastructure -- */

    /**
     * Compare two abstract pathnames lexicographically.
     */
    function compare($f1, $f2) {
        throw new Exception("SYSTEM ERROR [compare() not implemented by local fs driver]");
    }

    /**
     * Copy a file.
     *
     * @param File $src Source path and name file to copy.
     * @param File $dest Destination path and name of new file.
     *
     * @return void     
     * @throws Exception if file cannot be copied.
     */
    function copy(File $src, File $dest) {
        global $php_errormsg;
        $srcPath  = $src->getAbsolutePath();
        $destPath = $dest->getAbsolutePath();

        if (false === @copy($srcPath, $destPath)) { // Copy FAILED. Log and return err.
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::copy() FAILED. Cannot copy $srcPath to $destPath. $php_errormsg";
            throw new Exception($msg);
        }
        
        try {
            $dest->setMode($src->getMode());
        } catch(Exception $exc) {
            // [MA] does chmod returns an error on systems that do not support it ?
            // eat it up for now.
        }
    }

    /**
     * Change the permissions on a file or directory.
     *
     * @param    pathname    String. Path and name of file or directory.
     * @param    mode        Int. The mode (permissions) of the file or
     *                        directory. If using octal add leading 0. eg. 0777.
     *                        Mode is affected by the umask system setting.
     *
     * @return void     
     * @throws Exception if operation failed.
     */
    function chmod($pathname, $mode) {    
        $str_mode = decoct($mode); // Show octal in messages.    
        if (false === @chmod($pathname, $mode)) {// FAILED.
            $msg = "FileSystem::chmod() FAILED. Cannot chmod $pathname. Mode $str_mode. $php_errormsg";
            throw new Exception($msg);
        }
    }

    /**
     * Locks a file and throws an Exception if this is not possible.
     * @return void
     * @throws Exception
     */
    function lock(File $f) {
        $filename = $f->getPath();
        $fp = @fopen($filename, "w");
        $result = @flock($fp, LOCK_EX);
        @fclose($fp);
        if (!$result) {
            throw new Exception("Could not lock file '$filename'");
        }
    }

    /**
     * Unlocks a file and throws an IO Error if this is not possible.
     *
     * @throws Exception
     * @return void
     */
    function unlock(File $f) {
        $filename = $f->getPath();
        $fp = @fopen($filename, "w");
        $result = @flock($fp, LOCK_UN);
        fclose($fp);
        if (!$result) {
            throw new Exception("Could not unlock file '$filename'");
        }
    }

    /**
     * Delete a file.
     *
     * @param    file    String. Path and/or name of file to delete.
     *
     * @return void
     * @throws Exception - if an error is encountered.
     */
    function unlink($file) {
        global $php_errormsg;
        if (false === @unlink($file)) {
            $msg = "FileSystem::unlink() FAILED. Cannot unlink '$file'. $php_errormsg";
            throw new Exception($msg);
        }
    }

    /**
     * Symbolically link a file to another name.
     * 
     * Currently symlink is not implemented on Windows. Don't use if the application is to be portable.
     *
     * @param string $target Path and/or name of file to link.
     * @param string $link Path and/or name of link to be created.
     * @return void
     */
    function symlink($target, $link) {
    
        // If Windows OS then symlink() will report it is not supported in
        // the build. Use this error instead of checking for Windows as the OS.

        if (false === @symlink($target, $link)) {
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::Symlink() FAILED. Cannot symlink '$target' to '$link'. $php_errormsg";
            throw new Exception($msg);
        }

    }

    /**
     * Set the modification and access time on a file to the present time.
     *
     * @param string $file Path and/or name of file to touch.
     * @param int $time 
     * @return void
     */
    function touch($file, $time = null) {
        global $php_errormsg;
        
        if (null === $time) {
            $error = @touch($file);
        } else {
            $error = @touch($file, $time);
        }

        if (false === $error) { // FAILED.
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::touch() FAILED. Cannot touch '$file'. $php_errormsg";            
            throw new Exception($msg);            
        }
    }

    /**
     * Delete an empty directory OR a directory and all of its contents.
     *
     * @param    dir    String. Path and/or name of directory to delete.
     * @param    children    Boolean.    False: don't delete directory contents.
     *                                    True: delete directory contents.
     *
     * @return void
     */
    function rmdir($dir, $children = false) {
        global $php_errormsg;
        
        // If children=FALSE only delete dir if empty.
        if (false === $children) {
        
            if (false === @rmdir($dir)) { // FAILED.
                // Add error from php to end of log message. $php_errormsg.
                $msg = "FileSystem::rmdir() FAILED. Cannot rmdir $dir. $php_errormsg";
                throw new Exception($msg);
            }
            
        } else { // delete contents and dir.

            $handle = @opendir($dir);

            if (false === $handle) { // Error.

                $msg = "FileSystem::rmdir() FAILED. Cannot opendir() $dir. $php_errormsg";                
                throw new Exception($msg);

            } else { // Read from handle.

                // Don't error on readdir().
                while (false !== ($entry = @readdir($handle))) {

                    if ($entry != '.' && $entry != '..') {

                        // Only add / if it isn't already the last char.
                        // This ONLY serves the purpose of making the Logger
                        // output look nice:)

                        if (strpos(strrev($dir), "/") == 0) {// there is a /
                            $next_entry = $dir . $entry;
                        } else { // no /
                            $next_entry = $dir."/".$entry;
                        }

                        // NOTE: As of php 4.1.1 is_dir doesn't return FALSE it
                        // returns 0. So use == not ===.

                        // Don't error on is_dir()
                        if (false == @is_dir($next_entry)) { // Is file.
                            
                            try {
                                self::unlink($next_entry); // Delete.
                            } catch (Exception $e) {                            
                                $msg = "FileSystem::Rmdir() FAILED. Cannot FileSystem::Unlink() $next_entry. ". $e->getMessage();
                                throw new Exception($msg);
                            }

                        } else { // Is directory.
                            
                            try {
                                self::rmdir($next_entry, true); // Delete
                            } catch (Exception $e) {
                                $msg = "FileSystem::rmdir() FAILED. Cannot FileSystem::rmdir() $next_entry. ". $e->getMessage();
                                throw new Exception($msg);
                            }

                        } // end is_dir else
                    } // end .. if
                } // end while
            } // end handle if

            // Don't error on closedir()
            @closedir($handle);
            
            if (false === @rmdir($dir)) { // FAILED.
                // Add error from php to end of log message. $php_errormsg.
                $msg = "FileSystem::rmdir() FAILED. Cannot rmdir $dir. $php_errormsg";
                throw new Exception($msg);
            }
            
        }
                
    }

    /**
     * Set the umask for file and directory creation.
     *
     * @param    mode    Int. Permissions ususally in ocatal. Use leading 0 for
     *                    octal. Number between 0 and 0777.
     *
     * @return void
     * @throws Exception if there is an error performing operation.     
     */
    function umask($mode) {
        global $php_errormsg;
        
        // CONSIDERME:
        // Throw a warning if mode is 0. PHP converts illegal octal numbers to
        // 0 so 0 might not be what the user intended.
                        
        $str_mode = decoct($mode); // Show octal in messages.

        if (false === @umask($mode)) { // FAILED.
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::Umask() FAILED. Value $mode. $php_errormsg";
            throw new Exception($msg);
        }
    }

    /**
     * Compare the modified time of two files.
     *
     * @param    file1    String. Path and name of file1.
     * @param    file2    String. Path and name of file2.
     *
     * @return    Int.     1 if file1 is newer.
     *                 -1 if file2 is newer.
     *                  0 if files have the same time.
     *                  Err object on failure.
     *     
     * @throws Exception - if cannot get modified time of either file.
     */
    function compareMTimes($file1, $file2) {

        $mtime1 = filemtime($file1);
        $mtime2 = filemtime($file2);

        if ($mtime1 === false) { // FAILED. Log and return err.        
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::compareMTimes() FAILED. Cannot can not get modified time of $file1.";
            throw new Exception($msg);            
        } elseif ($mtime2 === false) { // FAILED. Log and return err.
            // Add error from php to end of log message. $php_errormsg.
            $msg = "FileSystem::compareMTimes() FAILED. Cannot can not get modified time of $file2.";
            throw new Exception($msg);
        } else { // Worked. Log and return compare.                
            // Compare mtimes.
            if ($mtime1 == $mtime2) {
                return 0;
            } else {
                return ($mtime1 < $mtime2) ? -1 : 1;
            } // end compare
        }
    }
        
}
