*/ /** The file method allow to manage files like PHP with a working chroot on all * plateforms, and a right management compatible with database * Don't follow links ! * * To allow an external authorization test in plus of the filesystem check, you * must extends the class and overload checkExternalPathRO and * checkExternalPathRW. */ class file { /** The virtual current working directory */ private $cwd = "."; /** The real directory used as root in virtual chroot */ private $baseDir = "/"; /** The lock stack */ private $locks = array (); /** Activate the debug and define the minimum priority to save */ public $debug = 0; /** Change the current working directory * @return bool true if the directory is changed * @throws If directory not exists, or the directory is not executable */ public function chdir ($directory) { $this->debug (2, "chdir ($directory)"); $tmpdirectory = $this->realpath ($directory); $this->checkPathRO ($tmpdirectory); if ($this->baseDir === "/") $this->cwd = $tmpdirectory; else $this->cwd = substr ($tmpdirectory, strlen ($this->baseDir)); $this->debug (1, "chdir $directory -> $this->cwd"); return true; } /** Chroot in the provided directory * @param $directory string The directory to chroot * @return true if the chroot is done, false if there is a failure * @throws If directory not exists, or the directory is not executable */ public function chroot ($directory) { // Use the checkPathRO (using the $this->baseDir) to not allow to go away of // the chroot. $this->debug (2, "chroot ($directory)"); $directory = $this->realpath ($directory); $this->checkPathRO ($directory); $this->baseDir = preg_replace ("#//+#", "/", $directory); $this->cwd = "/"; $this->debug (1, "chroot $directory -> $this->baseDir"); return true; } /** Checks whether a file or directory exists * @param string $filename The file or directory to verify * @return bool true if the file exists, false otherwise * @throws If parent directory not exists, or is not executable */ public function file_exists ($filename) { $this->debug (2, "file_exists ($filename)"); $filename = $this->realpath ($filename); try { $this->checkPathRO (dirname ($filename)); } catch (\Exception $e) { if ($e->getCode () !== 404) throw new \Exception ($e->getMessage (), $e->getCode ()); } if (file_exists ($filename) && ! is_link ($filename)) return true; return false; } /** Get the file contents * @param string $filename Name of the file to read * @return string Content of the file * @throws If parent directory not exists, is not readable, the file is not * exists or is not readable */ public function file_get_contents ($filename) { $this->debug (2, "file_get_contents ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRO (dirname ($filename)); if (! is_file ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not a file"), $filename), 500); if (! is_readable ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not readable"), $filename), 500); $contents = file_get_contents ($filename); $this->debug (1, "file_get_contents ($filename) => ".strlen ($contents). " bytes"); return $contents; } /** Write a string to a file * @param string $filename Path to the file where to write the data * @param The data to write * @return the length of the data stored * @throws If parent directory not exists, is not writeable, or the file * exists and is not writeable */ public function file_put_contents ($filename, $data) { $this->debug (2, "file_put_contents ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); if (file_exists ($filename) && ! is_writeable ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not writeable"), $filename), 500); if (file_exists ($filename) && ! is_file ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not a file"), $filename), 500); $contents = file_put_contents ($filename, $data); $this->debug (1, "file_put_contents ($filename, \$data) => ". "$contents bytes"); return $contents; } /** Return the current working directory * @return string the current working directory */ public function getcwd () { $this->debug (1, "getcwd $this->cwd"); return $this->cwd; } /** Tells whether the given filename is a directory * @param string $filename The filename to test * @return bool true if the $filename is a directory and exists, false * otherwise * @throws If parent directory not exists, or is not executable */ public function is_dir ($filename) { $this->debug (2, "is_dir ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRO (dirname ($filename)); if (file_exists ($filename) && is_dir ($filename)) return true; return false; } /** Tells whether the given filename is a valid file * @param string $filename The filename to test * @return bool true if the $filename is a file and exists, false otherwise * @throws If parent directory not exists, or is not executable */ public function is_file ($filename) { $this->debug (2, "is_file ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRO (dirname ($filename)); if (file_exists ($filename) && is_file ($filename)) return true; return false; } /** Lock a file exclusively * @param $filename The file to lock * @return bool true if the lock is acquired, false otherwise * @throws If parent directory not exists, or is not writeable */ public function lockEX ($filename) { $this->debug (2, "lockEX ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); $this->locks[$filename] = fopen ($filename, "rt"); return flock ($this->locks[$filename], LOCK_EX); } /** Lock a file shared (allow multiple read) * @param $filename The file to lock * @return bool true if the lock is acquired, false otherwise * @throws If parent directory not exists, or is not writeable */ public function lockSH ($filename) { $this->debug (2, "lockSH ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); $this->locks[$filename] = fopen ($filename, "rt"); return flock ($this->locks[$filename], LOCK_SH); } /** Unlock a file previously locked * @param $filename The file to lock * @return bool true if the lock is acquired, false otherwise * @throws If parent directory not exists, or is not writeable */ public function lockUN ($filename) { $this->debug (2, "lockUN ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); $res = true; if (isset ($this->locks[$filename])) { $res = flock ($this->locks[$filename], LOCK_UN); fclose ($this->locks[$filename]); } return $res; } /** Create a new directory * @param $pathname The directory to create * @param $mode The mode to create (0777 by default) * @param $recursive (false by default) * @return bool true if the directory is correctely created, false if the * directory already exists * @throws If parent directory not exists, is not writeable */ public function mkdir ($pathname, $mode = 0777, $recursive = false) { $this->debug (2, "mkdir ($pathname, $mode, $recursive)"); $pathname = $this->realpath ($pathname); if ($recursive) { $parents = explode ("/", $pathname); array_pop ($parents); $parent = ""; foreach ($parents as $p) { $parent = $parent.$p."/"; if (! file_exists ($parent)) { if (is_writeable (dirname ($parent))) break; throw new \Exception (sprintf ("Last Directory '%s' is readonly", dirname ($parent)), 500); } } if ($parent === dirname ($pathname) && ! is_writeable (dirname ($parent))) { throw new \Exception (sprintf ("Parent directory '%s' is readonly", dirname ($parent)), 500); } } else { $this->checkPathRW (dirname ($pathname)); } if (file_exists ($pathname)) throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' already exists"), $pathname), 500); $rc = mkdir ($pathname, $mode, $recursive); $this->debug (1, "mkdir ($pathname, $mode, $recursive) => $rc"); return $rc; } /** Copy a file or a directory * @param string $oldname The file to copy * @param string $newname The new name of the file. It will be * overwrited if it already exists * @return bool */ public function copy ($oldname, $newname) { $this->debug (2, "copy ($oldname, $newname)"); $oldname = $this->realpath ($oldname); $newname = $this->realpath ($newname); $this->checkPathRO (dirname ($oldname)); $this->checkPathRW (dirname ($newname)); if (is_dir ($oldname)) { // Copy directory structure if (! $this->file_exists ($newname)) $this->mkdir ($newname); $files = $this->scandirNotSorted ($oldname); foreach ($files as $file) $this->copy ("$oldname/$file", "$newname/$file"); } else { $rc = copy ($oldname, $newname); } $this->debug (1, "copy ($oldname, $newname) => $rc"); return $rc; } /** Renames a file or directory * @param string $oldname The file or directory to rename * @param string $newname The new name of the file or directory. It will be * overwrited if it already exists * @return bool */ public function rename ($oldname, $newname) { $this->debug (2, "rename ($oldname, $newname)"); $oldname = $this->realpath ($oldname); $newname = $this->realpath ($newname); $this->checkPathRO (dirname ($oldname)); $this->checkPathRW (dirname ($newname)); $rc = rename ($oldname, $newname); $this->debug (1, "rename ($oldname, $newname) => $rc"); return $rc; } /** Return a ini file converted to an array * @param string $filename The filename of the ini file being parsed. * @param bool $process_sections Process the sections * @return array */ public function parse_ini_file ($filename, $process_sections = false) { $this->debug (2, "parse_ini_file ($filename, $process_sections)"); $filename = $this->realpath ($filename); $this->checkPathRO (dirname ($filename)); if (! is_file ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not a file"), $filename), 500); if (! is_readable ($filename)) throw new \Exception (sprintf (dgettext ("domframework", "File '%s' is not readable"), $filename), 500); return parse_ini_file ($filename, $process_sections); } /** Return the canonical absolute path. Do not check if the directory exists, * if there is links. Just calculate the realpath based on the chroot value * @param string $path the path to analyze * @return string the canonical absolute path */ public function realpath ($path) { $oriPath = $path; $this->debug (2, "realpath ($oriPath)"); $path = preg_replace ("#//+#", "/", $path); if (substr ($path, -1) === "/") $path = substr ($path, 0, -1); $parts = explode ("/", $path); $current = $this->cwd; $tmp = explode ("/", $current); foreach ($parts as $part) { if ($part === "") $tmp = array(); elseif ($part === ".") continue; elseif ($part === "..") { array_pop ($tmp); continue; } else array_push ($tmp, $part); } if (reset ($tmp) === ".") { array_shift ($tmp); $path = $current."/".implode ("/", $tmp); } else $path = "/".implode ("/", $tmp); if ($this->baseDir !== "/") $path = $this->baseDir.$path; $path = preg_replace ("#//+#", "/", $path); if ($path !== "/" && substr ($path, -1) === "/") $path = substr ($path, 0, -1); $this->debug (1, "realpath ($oriPath) => $path"); return $path; } /** Remove the provided directory * If the recurse flag is true, remove the content too (files and * directories) * @param string $dirname The directory to remove * @param bool $recursive Remove recursively * @return bool true if all is removed, false otherwise * @throws If parent directory not exists, is not writeable or the current * dir is not writeable */ public function rmdir ($dirname, $recursive=false) { $this->debug (2, "rmdir ($dirname, $recursive)"); $tmpdirname = $this->realpath ($dirname); $this->checkPathRW (dirname ($tmpdirname)); $this->checkPathRW ($tmpdirname); if ($recursive === false) return @rmdir ($tmpdirname); $files = array_diff (scandir ($tmpdirname), array('.','..')); foreach ($files as $file) { (is_dir ("$tmpdirname/$file")) ? $this->rmdir("$dirname/$file") : unlink ("$tmpdirname/$file"); } return rmdir ($tmpdirname); } /** Return the list of files and directories in the directory. * Do not return the . and .. virtual dirs. * The result is sorted * @param string $directory The directory to read * @return array the list of files and dirs * @throws If directory not exists, or is not executable */ public function scandir ($directory) { $this->debug (2, "scandir ($directory)"); $directory = $this->realpath ($directory); $this->checkPathRO ($directory); $res = array_values (array_diff (scandir ($directory), array('..', '.'))); natsort ($res); return $res; } /** Return the list of files and directories in the directory. * Do not return the . and .. virtual dirs. * The result is NOT sorted * @param string $directory The directory to read * @return array the list of files and dirs * @throws If directory not exists, or is not executable */ public function scandirNotSorted ($directory) { $this->debug (2, "scandirNotSorted ($directory)"); $directory = $this->realpath ($directory); $this->checkPathRO ($directory); $res = array_values (array_diff (scandir ($directory, SCANDIR_SORT_NONE), array('..', '.'))); return $res; } /** Create a new file or update the timestamp if the file exists * @param string $filename the filename * @param int $time the timestamp to use (actual timestamp if not defined) * @return bool true or false on failure * @throws If parent directory not exists, is not writeable */ public function touch ($filename, $time = null, $atime = null) { $this->debug (2, "touch ($filename, $time, $atime)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); if ($time === null) $time = time (); if ($atime === null) $atime = time (); $rc = touch ($filename, $time, $atime); $this->debug (1, "touch ($filename, $time, $atime) => $rc"); return $rc; } /** Delete an existing file. * @param string $filename The filename to remove * @return bool true if the file si removed, false otherwise * @throws If parent directory not exists, or is not executable */ public function unlink ($filename) { $this->debug (2, "unlink ($filename)"); $filename = $this->realpath ($filename); $this->checkPathRW (dirname ($filename)); if (! file_exists ($filename) || ! is_writeable ($filename)) return false; return unlink ($filename); } /** Check all the parents of the $directory if they are available, and * executable. The path must exists. * Must use the filesystem path (complete) and not the version in chroot. * The last directoy must be executable and readable (no test for writeable) * @param $path string The directory path to check * @return true if the path is executable for all the parents and for the * last directory * @throws if there is a missing part, or a parent is not executable */ private function checkPathRO ($path) { $this->debug (2, "checkPathRO ($path)"); $path = preg_replace ("#//+#", "/", $path); $parents = explode ("/", $path); array_pop ($parents); $parent = ""; foreach ($parents as $p) { $parent = $parent.$p."/"; if (! file_exists ($parent)) { $this->debug (1, "checkPathRO ($path) => Parent Path '$parent' not found"); throw new \Exception (sprintf (dgettext ("domframework", "Parent Path '%s' not found"), $parent), 404); } if (! is_executable ($parent)) { $this->debug (1, "checkPathRO ($path) => ". "Parent Directory '$parent' not executable"); throw new \Exception (sprintf (dgettext ("domframework", "Parent Directory '%s' not executable"), $parent), 500); } if ($this->checkExternalPathRO ($parent) !== true) { $this->debug (1, "checkPathRO ($path) => ". "Parent Directory '$parent' not accessible by ". "external check read-only"); throw new \Exception (sprintf (dgettext ("domframework", "Parent Directory '%s' not accessible ". "by external check read-only"), $parent), 500); } } if (! file_exists ($path)) { $this->debug (1, "checkPathRO ($path) => Path '$path' not found"); throw new \Exception (sprintf (dgettext ("domframework", "Path '%s' not found"), $path), 404); } if (! is_dir ($path)) { $this->debug (1, "checkPathRO ($path) => ". "Path '$path' is not a directory"); throw new \Exception (sprintf (dgettext ("domframework", "Path '%s' is not a directory"), $path), 500); } if (! is_executable ($path)) { $this->debug (1, "checkPathRO ($path) => ". "Directory '$path' is not executable"); throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' is not executable"), $path), 500); } if (! is_readable ($path)) { $this->debug (1, "checkPathRO ($path) => ". "Directory '$path' is not readable"); throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' is not readable"), $path), 500); } return $this->checkExternalPathRO ($path); } /** Check all the parents of the $directory if they are available, and * executable. The path must exists. * Must use the filesystem path (complete) and not the version in chroot. * The last directoy must be executable and readable and writeable * @param $path string The directory path to check * @return true if the path is executable for all the parents and for the * last directory * @throws if there is a missing part, or a parent is not executable */ private function checkPathRW ($path) { $this->debug (2, "checkPathRW ($path)"); $this->checkPathRO ($path); if (! is_writeable ($path)) { $this->debug (1, "checkPathRW ($path) => ". "Directory '$path' is not writeable"); throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' is not writeable"), $path), 500); } if ($this->checkExternalPathRW ($path) !== true) { $this->debug (1, "checkPathRW ($path) => ". "Directory '$path' not accessible by ". "external check read-write"); throw new \Exception (sprintf (dgettext ("domframework", "Directory '%s' not accessible ". "by external check read-write"), $path), 500); } return true; } /** Save a debug log * @param $prio The message priority. Should be higher than $this->debug to * save the message * @param $message The message to save * @return null; */ private function debug ($prio, $message) { if ($this->debug === false || $this->debug === 0) return; if ($prio <= $this->debug) { echo "[$prio] $message\n"; //file_put_contents ("/tmp/domframework.file.debug", // date ("Y:m:d H:i:s")." [$prio] $message\n", // FILE_APPEND); } } /** External function allowed to be overloaded to test the RO access to a * resource * @param string $path The path to test in the filesystem * @return boolean true if RO access, false if not */ public function checkExternalPathRO ($path) { return true; } /** External function allowed to be overloaded to test the RW access to a * resource * @param string $path The path to test in the filesystem * @return boolean true if RW access, false if not */ public function checkExternalPathRW ($path) { return true; } }