<?php
/*
 * $RCSfile: GalleryPlatform.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 * @version $Revision: 1.77 $ $Date: 2005/08/26 21:35:37 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/**
 * A framework for doing platform specific tasks.
 *
 * This is an abstract class that implements many basic tasks that are
 * different from platform to platform.
 *
 * @package GalleryCore
 * @abstract
 * @access public
 */
class GalleryPlatform {

    /**
     * Copy a file
     *
     * @param string the source file
     * @param string the destination file
     * @return bool TRUE if the copy succeeded, FALSE otherwise.
     * @access public
     */
    function copy($source, $dest) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("copy($source, $dest)");
	}

	if (!isset($this->_umask)) {
	    $this->_calculateUmaskAndFilePerms();
	}
	$umask = umask($this->_umask);
	if (is_uploaded_file($source)) {
	    $results = move_uploaded_file($source, $dest);
	} else {
	    $results = copy($source, $dest);
	}
	umask($umask);

	return $results;
    }

    /**
     * Symlink a file
     *
     * @param string the source file
     * @param string the destination file
     * @return bool TRUE if the copy succeeded, FALSE otherwise.
     * @access public
     */
    function symlink($source, $dest) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("symlink($source, $dest)");
	}

	if (!isset($this->_umask)) {
	    $this->_calculateUmaskAndFilePerms();
	}
	$umask = umask($this->_umask);
	$results = symlink($source, $dest);
	umask($umask);

	return $results;
    }



    /**
     * Move an uploaded file to a new location and return the new location.
     *
     * If the second filename is not provided, a new file is created in the
     * Gallery temporary directory.
     *
     * @return string the new file name, if the move was successful
     * @access public
     */
    function move_uploaded_file($filename, $newFilename=null) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("move_uploaded_file($filename, $newFilename)");
	}

	if (empty($newFilename)) {
	    $tmpDir = $gallery->getConfig('data.gallery.tmp');
	    $newFilename = tempnam($tmpDir, 'MUF_');
	    if ($newFilename == null) {
		return null;
	    }
	    if ($gallery->getDebug()) {
		$gallery->debug("chose new file name: $newFilename");
	    }
	}

	if (move_uploaded_file($filename, $newFilename)) {
	    return $newFilename;
	} else {
	    return null;
	}
    }

    /**
     * Create a file with a unique file name
     * @param string target dir
     * @param string file prefix
     */
    function tempnam($dir, $prefix) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("tempnam($dir, $prefix)");
	}

	return tempnam($dir, $prefix);
    }

    /**
     * Does the given file exist?
     *
     * @param string full filesystem path to a file
     * @return bool TRUE if the file exists, FALSE otherwise
     * @access public
     */
    function file_exists($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("file_exists($filename)");
	}

	return is_uploaded_file($filename) || @file_exists($filename);
    }

    /**
     * Is the given path a symbolic link?
     *
     * @param string a filesystem path
     * @return bool TRUE if the file is a link, FALSE otherwise
     * @access public
     */
    function is_link($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_link($filename)");
	}

	return is_link($filename);
    }

    /**
     * Is the given path a directory?
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is a directory, FALSE otherwise
     * @access public
     */
    function is_dir($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_dir($filename)");
	}

	return is_dir($filename);
    }

    /**
     * Is the given path a normal file?
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is a file, FALSE otherwise
     * @access public
     */
    function is_file($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_file($filename)");
	}

	return @file_exists($filename) && @is_file($filename);
    }

    /**
     * Is the given path a writeable file
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is writeable, FALSE otherwise
     * @access public
     */
    function is_writeable($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_writeable($filename)");
	}

	return is_writeable($filename);
    }

    /**
     * Is the given path a readable file
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is readable, FALSE otherwise
     * @access public
     */
    function is_readable($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_readable($filename)");
	}

	return is_readable($filename);
    }

    /**
     * Is the given path an uploaded file
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is an uploaded file, FALSE otherwise
     * @access public
     */
    function is_uploaded_file($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_uploaded_file($filename)");
	}

	return is_uploaded_file($filename);
    }

    /**
     * Is the given path an executable file?
     *
     * @param string a filesystem path
     * @return bool TRUE if the path is an executable file, FALSE otherwise
     * @access public
     */
    function is_executable($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("is_executable($filename)");
	}

	return is_executable($filename);
    }

    /**
     * How large is the given file?
     *
     * @param string full filesystem path to a file
     * @return int the size in bytes
     * @access public
     */
    function filesize($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("filesize($filename)");
	}

	return filesize($filename);
    }

    /**
     * Clear the stat cache
     *
     * @access public
     */
    function clearstatcache() {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug('clearstatcache()');
	}

	return clearstatcache();
    }

    /**
     * Return a file as an array
     *
     * @param string a file path or URL
     * @param int if this is set to 1, search the include path also
     * @return array of lines
     * @access public
     */
    function file($filename, $use_include_path=false) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("file($filename, $use_include_path)");
	}

	return file($filename, $use_include_path);
    }

    /**
     * Output a file
     *
     * @param string a file path or URL
     * @param int if this is set to 1, search the include path also
     * @return int the number of bytes read from the file
     * @access public
     */
    function readfile($filename, $use_include_path=false) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("readfile($filename, $use_include_path)");
	}
	return readfile($filename, $use_include_path);
    }

    /**
     * Reads the entire contents of the specified file into a string.
     * file_get_contents PHP function is used if available.
     * 
     * @param string file path
     * @return string file contents or boolean false on failure
     */
    function file_get_contents($path) {
	if (function_exists('file_get_contents')) {
	    return file_get_contents($path);
	}
	if (!($fd = $this->fopen($path, 'rb'))) {
	    return false;
	}
	$fileSize = $this->filesize($path);
	$contents = $fileSize == 0 ? '' : $this->fread($fd, $fileSize);
	$this->fclose($fd);
	return $contents;
    }

    /**
     * Open a file or URL
     *
     * @param string a file path or URL
     * @param string a file mode
     * @param int if this is set to 1, search the include path also
     * @return resource a file descriptor
     * @access public
     */
    function fopen($filename, $mode, $use_include_path=0) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fopen($filename, $mode, $use_include_path)");
	}

	return fopen($filename, $mode, $use_include_path);
    }

    /**
     * flock -- Portable advisory file locking
     *
     * @param resource a file handle
     * @param operation (LOCK_SH, LOCK_EX, LOCK_UN) [ | LOCK_NB]
     * @param bool set to true if the operation would have blocked
     * @return true or false
     * @access public
     */
    function flock($handle, $operation, &$wouldblock) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("flock($handle, $operation, $wouldblock)");
	}

	return flock($handle, $operation, $wouldblock);
    }

    /**
     * Perform an atomic write to a file.  This guarantees that the data written is not corrupted
     * (but it does not prevent another process from immediately replacing the file with its own
     * version).
     *
     * @param filename
     * @param string data to be written
     * @param boolean binary mode?
     * @return boolean success or failure
     * @access public
     */
    function atomicWrite($filename, $data) {
	$tempFile = tempnam(dirname($filename), basename($filename));
	$fd = fopen($tempFile, 'wb');
	$success = false;
	if ($fd) {
	    if (!isset($this->_filePerms)) {
		$this->_calculateUmaskAndFilePerms();
	    }
	    chmod($tempFile, $this->_filePerms);
	    $bytesWritten = fwrite($fd, $data);
	    if ($bytesWritten == strlen($data)) {
		$success = true;
	    }
	    fclose($fd);
	}

	if ($success) {
	    return $this->rename($tempFile, $filename);
	} else {
	    @unlink($tempFile);
	    return false;
	}
    }

    /**
     * Open a file or URL
     *
     * @param string a file path
     * @return resource a directory descriptor
     * @access public
     */
    function opendir($path) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("opendir($path)");
	}

	return opendir($path);
    }

    /**
     * Return the next file resource from a directory
     *
     * @param string a directory resource
     * @access public
     */
    function readdir($resource) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("readdir($resource)");
	}

	return readdir($resource);
    }

    /**
     * Close a directory resource
     *
     * @param string a directory resource
     * @access public
     */
    function closedir($resource) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("closedir($resource)");
	}

	return closedir($resource);
    }

    /**
     * Rename a file/dir
     *
     * As a side bonus, create a backup of the original file.
     *
     * @param string original file/dir name
     * @param string new file/dir name
     * @return bool true on success, false on failure
     * @access public
     */
    function rename($oldname, $newname) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("rename($oldname, $newname)");
	}

	return rename($oldname, $newname);
    }

    /**
     * Get information about a file
     *
     * @param string file/dir name
     * @return array the statistics of the file
     * @access public
     */
    function stat($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("stat($filename)");
	}

	return stat($filename);
    }

    /**
     * Get size information about an image
     *
     * @param string the image file name
     * @return array with 4 elements. Index 0 contains the
     * width of the image in pixels. Index 1 contains the height. Index 2 is a
     * flag indicating the type of the image: 1 = GIF, 2 = JPG, 3 = PNG, 4 =
     * SWF, 5 = PSD, 6 = BMP, 7 = TIFF(intel byte order), 8 = TIFF(motorola
     * byte order), 9 = JPC, 10 = JP2, 11 = JPX, 12 = JB2, 13 = SWC, 14 =
     * IFF. These values correspond to the IMAGETYPE constants that were added
     * in PHP 4.3. Index 3 is a text string with the correct height="yyy"
     * width="xxx" string that can be used directly in an IMG tag.
     */
    function getimagesize($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("getimagesize($filename)");
	}

	/*
	 * getimagesize() returns an E_WARNING if the file is unreadable or not an image.
	 * We'd rather that it just returned false without the warning.
	 */
	return @getimagesize($filename);
    }

    /**
     * Delete a file
     *
     * @param string file name
     * @return bool true on success, false on failure
     * @access public
     */
    function unlink($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("unlink($filename)");
	}

	return unlink($filename);
    }

    /**
     * Delete a directory
     *
     * @param string directory name
     * @return bool true on success, false on failure
     * @access public
     */
    function rmdir($filename) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("rmdir($filename)");
	}

	return rmdir($filename);
    }

    /**
     * Delete a directory, and all its contents
     *
     * @param string directory name
     * @return bool true on success, false on failure
     * @access public
     */
    function recursiveRmdir($dirname) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("recursiveRmdir($dirname)");
	}

	if ($dirname{strlen($dirname)-1} != $this->getDirectorySeparator()) {
	    $dirname .= $this->getDirectorySeparator();
	}
	
	if (!($fd = $this->opendir($dirname))) {
	    return false;
	}

	while (($filename = $this->readdir($fd)) !== false) {
	    if (!strcmp($filename, '.') || !strcmp($filename, '..')) {
		continue;
	    }
	    $path = $dirname . $filename;

	    if ($this->is_dir($path)) {
		$ret = $this->recursiveRmdir($path);
	    } else {
		$ret = $this->unlink($path);
	    }

	    if ($ret == false) {
		return false;
	    }
	}
	closedir($fd);

	return $this->rmdir($dirname);
    }

    /**
     * Create a new directory
     *
     * @param string a filesystem path
     * @param string permissions of the newly created directory
     * @return bool true on success, false on failure
     * @access public
     */
    function mkdir($path, $stringPerms='755') {
	global $gallery;

	/* Convert string permission representation to octal */
	$octalPerms = octdec($stringPerms);
	if ($gallery->getDebug()) {
	    $gallery->debug(sprintf("mkdir(%s, %o)", $path, $octalPerms));
	}

	$umask = umask(0);
	$results = mkdir($path, $octalPerms);
	umask($umask);
	return $results;
    }

    /**
     * Return true if the path component specified is composed of legal
     * characters.
     *
     * @param string the path component (must not contain path separators)
     * @return true if yes
     */
    function isLegalPathComponent($component) {
	$legalChars = $this->getLegalPathCharacters();
	if (strspn($component, $legalChars) == strlen($component)) {
	    return true;
	} else {
	    return false;
	}
    }

    /**
     * Legal characters on all systems: A-Z a-z 0-9 _ . -
     *
     * Specific platform implementations can override this.
     *
     * @return string a string composed of all legal path characters
     */
    function getLegalPathCharacters() {
	return "ABCDEFGHIJKLMNOPQRSTUVWXYZ" .
	    "abcdefghijklmnopqrstuvwxyz" .
	    "0123456789" .
	    "# _.-";

	/*
	 * Keep the hyphen at the end of this string, else ereg()
	 * functions will complain if you use this in a bracket expression.
	 */
    }

    /**
     * Remove any illegal characters from the path component.
     */
    function legalizePathComponent($component) {
	global $gallery;
	$legal = $this->getLegalPathCharacters();

	/* Scrub out all the illegal characters */
	$component = ereg_replace("[^$legal]", "_", $component);

	/* Break it into filebase and extension */
	list ($fileBase, $extension) = GalleryUtilities::getFileNameComponents($component);

	/*
	 * Convert all dots to underscores in the fileBase.  This prevents
	 * malicious users from uploading files like 'foo.php.jpg' which will
	 * be treated like a JPEG by Gallery, but could be treated like a .php
	 * file by Apache opening a security hole.
	 */
	$fileBase = str_replace('.', '_', $fileBase);

	/*
	 * If we don't know exactly what type this file is, we have to assume
	 * that it's something malicious.  In that case, it might be a server
	 * side script of some kind and we don't want that to wind up in the
	 * albums directory in a pure state because it may open up a security
	 * hole on systems that have placed the gallery data directory inside
	 * the document root.  So mangle the extension to make sure that the
	 * webserver doesn't execute it.
	 */
	list ($ret, $tmp) = GalleryCoreApi::convertExtensionToMime($extension);
	if ($ret->isError() && $gallery->getDebug()) {
	    $gallery->debug('Error: convertExtensionToMime in ' .
	    'GalleryPlatform::legalizePathComponent');
	}
	if (($ret->isError() || $tmp == 'application/unknown') && !empty($extension)) {
	    $fileBase = $fileBase . '_' . $extension;
	    $extension = null;
	}

	/* Rebuild the baseName according to our transforms above */
	$baseName = $fileBase;
	if (!empty($extension)) {
	    $baseName .= '.' . $extension;
	}

	return $baseName;
    }

    /**
     * Execute a command and record the results and status.
     *
     * @param array(array('cmd', 'arg'), ...)
     * @param whether or not we should return error output
     * @return array(bool TRUE if the command succeeded, FALSE otherwise,
     *               string standard output from the command
     *               string error output from the command)
     * @access public
     */
    function exec($cmdArray) {
	/* This must be implemented in a platform specific way */
	assert(false);
    }

    /**
     * Return the filesystem specific directory separator
     *
     * @return string directory separator
     */
    function getDirectorySeparator() {
	return DIRECTORY_SEPARATOR;
    }

    /**
     * Return true if the path provided is not allowed by the current open_basedir configuration.
     *
     * @return true if the path is restricted
     */
    function isRestrictedByOpenBaseDir($path) {
	/*
	 * This must be implemented in a platform specific way due to the fact
	 * that different PHP platforms use different separators in the basedir
	 * path, and since case sensitivity of path elements is not relevant on
	 * all platforms.
	 */
	assert(false);
    }

    /**
     * Check if path is allowed by open_basedir, given platform path separator & case sensitivity.
     * @access protected
     */
    function _isRestrictedByOpenBaseDir($path, $separator, $caseSensitive) {
	global $gallery;
	$slash = $this->getDirectorySeparator();
	$phpVm = $gallery->getPhpVm();
	$openBasedir = $phpVm->ini_get('open_basedir');
	if (empty($openBasedir)) {
	    return false;
	}

	if (($realpath = $this->realpath($path)) === false) {
	    /*
	     * PHP's open_basedir will actually take an invalid path, resolve relative
	     * paths, parse out .. and . and then check against the dir list..
	     * Here we do an ok job of doing the same, though it isn't perfect.
	     */
	    $s = '\\\/';  /* do this by hand because preg_quote() isn't reliable */
	    if (!preg_match("{^([a-z]+:)?[$s]}i", $path)) {
		$path = $this->getcwd() . $slash . $path;
	    }
	    for ($realpath = $path, $lastpath = ''; $realpath != $lastpath;) {
		$realpath = preg_replace("#[$s]\.([$s]|\$)#", $slash, $lastpath = $realpath);
	    }

	    for ($lastpath = ''; $realpath != $lastpath;) {
		$realpath = preg_replace("#[$s][^$s]+[$s]\.\.([$s]|\$)#",
					 $slash, $lastpath = $realpath);
	    }
	}

	$function = $caseSensitive ? 'strncmp' : 'strncasecmp';
	foreach (explode($separator, $openBasedir) as $baseDir) {
	    if (($baseDirMatch = $this->realpath($baseDir)) === false) {
		$baseDirMatch = $baseDir;
	    } else if ($baseDir{strlen($baseDir)-1} == $slash) {
		/* Realpath will remove a trailing slash.. add it back to avoid prefix match */
		$baseDirMatch .= $slash;
	    }
	    /* Add slash on path so /dir is accepted if /dir/ is a valid basedir */
	    if (!$function($baseDirMatch, $realpath . $slash, strlen($baseDirMatch))) {
		return false;
	    }
	}

	return true;
    }

    /**
     * Initiates a socket connection to the resource specified by target.
     * @see http://php.net/fsockopen
     *
     * @param string the hostname
     * @param int the port
     * @param int the error number (out)
     * @param string the error string (out)
     * @param int the timeout
     * @return resource a file descriptor
     */
    function fsockopen($target, $port, &$errno, &$errstr, $timeout) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fsockopen($target, $port, $errno, $errstr, $timeout)");
	}

	return fsockopen($target, $port, $errno, $errstr, $timeout);
    }

    /**
     * Write the contents of string to the file stream pointed to by handle.
     *
     * @param resource the handle
     * @param string the buffer
     * @param int how many bytes to write (optional)
     * @return int the number of bytes written
     */
    function fwrite($handle, $string, $length=null) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fwrite($handle, ..., $length)");
	}

	if (isset($length)) {
	    return fwrite($handle, $string, $length);
	} else {
	    return fwrite($handle, $string);
	}
    }

    /**
     * Closes an open file pointer
     *
     * @param resource the handle
     * @return boolean true on success, false on failure
     */
    function fclose($handle) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fclose($handle)");
	}

	return fclose($handle);
    }

    /**
     * Flush an open file pointer
     *
     * @param resource the handle
     * @return boolean true on success, false on failure
     */
    function fflush($handle) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fflush($handle)");
	}

	return fflush($handle);
    }

    /**
     * Tests for end-of-file on a file pointer
     *
     * @param resource the handle
     * @return boolean true if the file pointer is at EOF
     */
    function feof($handle) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("feof($handle)");
	}

	return feof($handle);
    }

    /**
     * Reads data from an open file handle
     *
     * @param resource the handle
     * @param int the length
     * @return string the bytes read
     */
    function fread($handle, $length) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fread($handle, $length)");
	}

	return fread($handle, $length);
    }

    /**
     * Seeks on a file pointer
     *
     * @param resource the handle
     * @param int offset
     * @param int whence
     * @return int 0 upon success, otherwise -1
     */
    function fseek($handle, $offset, $whence = SEEK_SET) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fseek($handle, $offset, $whence)");
	}

	return fseek($handle, $offset, $whence);
    }

    /**
     * Truncates a file to a given length
     *
     * @param resource the handle
     * @param int the size
     * @return boolean success?
     */
    function ftruncate($handle, $size) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("ftruncate($handle, $size)");
	}

	$ret = ftruncate($handle, $size);
	if ($ret === 1) {
	    /*
	     * Prior to PHP 4.3.3, ftruncate() returns an integer value of 1
	     * on success, instead of boolean TRUE. We correct this here.
	     */
	    return true;
	}
	return $ret;
    }

    /**
     * Gets line from file pointer
     *
     * @param resource the file handle
     * @param int the optional max line length
     * @return the string or FALSE on eof
     */
    function fgets($handle, $length=0) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("fgets($handle, $length)");
	}

	return fgets($handle, $length);
    }

    /**
     * Expand all symbolic links and resolve references to '/./', '/../' and extra '/' characters
     * in the input path and return the canonicalized absolute pathname.   The resulting path will
     * have no symbolic link, '/./' or '/../' components. [cribbed from http://php.net/realpath]
     *
     * @return FALSE on failure, e.g. if the file does not exists.
     */
    function realpath($file) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("realpath($file)");
	}

	return realpath($file);
    }

    /**
     * touch -- Sets access and modification time of file
     *
     * Attempts to set the access and modification time of the file named by filename to the value
     * given by time. If the option time is not given, uses the present time. This is equivalent
     * to what utime (sometimes referred to as utimes) does. If the third option atime is present,
     * the access time of the given filename is set to the value of atime. Note that the access
     * time is always modified, regardless of the number of parameters.
     *
     * @param string the file path
     * @param int (optional) the modification time
     * @param int (optional) the access time
     */
    function touch($file, $time=null, $atime=null) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("touch($file, $time, $atime)");
	}

	if (isset($atime)) {
	    touch($file, $time, $atime);
	} else if (isset($time)) {
	    touch($file, $time);
	} else {
	    touch($file);
	}
    }

    /**
     * mail -- Send an email
     *
     * @param string to address(es) (comma separated)
     * @param string subject
     * @param string body
     * @param string (optional) additional headers (\r\n separated)
     * @return boolean true on success
     */
    function mail($to, $subject, $body, $headers=null) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("mail($to, $subject, $body, $headers)");
	}

	$config = array();
	list ($ret, $params) = GalleryCoreApi::fetchAllPluginParameters('module', 'core');
	if ($ret->isSuccess()) {
	    foreach ($params as $key => $value) {
		if (strncmp($key, 'smtp.', 5) == 0) {
		    $config[$key] = $value;
		}
	    }
	}

	if (!empty($config['smtp.host'])) {
	    GalleryCoreApi::relativeRequireOnce('lib/smtp/smtp.php');
	    $ret = smtpmail($config, $to, $subject, $body, $headers);
	    if ($ret->isError() && $gallery->getDebug()) {
		$gallery->debug("smtpmail error: " . $ret->getAsText());
	    }
	    return $ret->isSuccess();
	} else if (isset($headers)) {
	    return mail($to, $subject, $body, $headers);
	} else {
	    return mail($to, $subject, $body);
	}
    }

    /**
     * Split path into component elements.  Include root path for absolute paths.
     * Examples: /tmp -> array('/', 'tmp')     rela/tive/path -> array('rela', 'tive', 'path')
     *           C:\Test\File.txt -> array('C:\', 'Test', 'File.txt')
     * @param string path
     * @return array (path elements) first item is "root" if path is absolute
     */
    function splitPath($path) {
	/* This must be implemented in a platform specific way */
	assert(false);
    }


    /**
     * Return a boolean specifying whether or not this platform can perform
     * a symbolic link (symlink) command
     *
     * @return boolean true if the platform supports symlinks
     *
     * @access public
     */
    function isSymlinkSupported() {
	/* This must be implemented in a platform specific way */
	assert(false);
    }

    /**
     * Return the string of characters which represent the line ending on
     * this platform
     *
     * @return string Line ending
     *
     * @access public
     */
    function getLineEnding() {
	/* This must be implemented in a platform specific way */
	assert(false);
    }


    /**
     * chdir -- change working directory
     *
     * @param string directory
     * @return boolean true on success
     */
    function chdir($path) {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("chdir($path)");
	}
	return chdir($path);
    }

    /**
     * getcwd -- gets the current working directory
     *
     * @return the current working directory, or FALSE on failure.
     */
    function getcwd() {
	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug("getcwd()");
	}
	return getcwd();
    }

    /**
     * Set file permissions according to preferences
     * @param string file path
     * @return boolean true on success
     */
    function setFilePermissions($file) {
	if (!isset($this->_filePerms)) {
	    $this->_calculateUmaskAndFilePerms();
	}
	return chmod($file, $this->_filePerms);
    }

    /**
     * Get umask preference
     * @access private
     */
    function _calculateUmaskAndFilePerms() {
	if (!isset($this->_umask) || !isset($this->_filePerms)) {
	    $this->_filePerms = 0644;
	    list ($ret, $filePerms) =
		GalleryCoreApi::getPluginParameter('module', 'core', 'permissions.file');
	    if ($ret->isSuccess() && !empty($filePerms)) {
		$this->_filePerms = octdec($filePerms);
	    }
	    $this->_umask = 0777 - $this->_filePerms;
	}

	return $this->_umask;
    }

    /**
     * Format a local time/date according to locale settings.
     * Converts any text output from strftime tokens to UTF-8.
     * @param string format
     * @param (optional) int timestamp
     * @return string
     */
    function strftime($format, $timestamp=null) {
	$i = 0;
	$newFormat = '';
	$textPieces = array('');
	/* Separate text and tokens so we can convert only token output to UTF-8 */
	foreach (preg_split('{(%.)}', $format, -1, PREG_SPLIT_DELIM_CAPTURE) as $piece) {
	    if (++$i % 2) {
		$textPieces[] = $piece;
		$newFormat .= '&%%&s';
	    } else {
		$newFormat .= $piece;
	    }
	}
	/* Call strftime and convert to UTF-8; escape % characters before sprintf */
	$textPieces[0] = str_replace(array('%', '&%%&'), array('%%', '%'),
	    GalleryCoreApi::convertToUtf8(
		isset($timestamp) ? strftime($newFormat, $timestamp) : strftime($newFormat)));
	return call_user_func_array('sprintf', $textPieces);
    }

    /**
     * Clear any cached saved state in this platform.
     */
    function resetPlatform() {
	unset($this->_umask);
	unset($this->_filePerms);
    }
}
?>
