<?php
/**
 * $Id: ManagerEngine.php 430 2008-10-10 12:50:41Z spocke $
 *
 * @package MCManager
 * @author Moxiecode
 * @copyright Copyright © 2007, Moxiecode Systems AB, All rights reserved.
 */

// Get base path
$basepath = dirname(__FILE__) . "/";

// Import core classes
require_once($basepath . "Utils/Logger.class.php");
require_once($basepath . "ManagerPlugin.php");
require_once($basepath . "Utils/ResultSet.php");
require_once($basepath . "Utils/LanguagePack.php");
require_once($basepath . "FileSystems/BaseFile.php");
require_once($basepath . "FileSystems/FileStream.php");
require_once($basepath . "FileSystems/FileFilter.php");
require_once($basepath . "FileSystems/FileTreeHandler.php");

/**
 * This class handles the core logic of the MCManager it's responsible for event handeling, configuration management,
 * language packs and plugins.
 *
 * @package MCManager
 */
class Moxiecode_ManagerEngine {
	/**#@+
	 * @access private
	 */

	var $_config;
	var $_plugins;
	var $_prefixes;
	var $_fileSystems;
	var $_rootPaths;
	var $_language;
	var $_type;
	var $_logger;
	var $_langPackPath;

	/**#@+
	 * @access public
	 */

	/**
	 * Main constructor.
	 *
	 * @param String $type Language pack type prefix. Used inorder to load the correct language pack like im or fm.
	 */
	function Moxiecode_ManagerEngine($type) {
		$this->_plugins = array();
		$this->_prefixes = array();
		$this->_rootPaths = array();
		$this->_config = array();
		$this->_type = $type;
	}

	/**
	 * Sets the name/value array of config options. This method will also force some relative config option paths to absolute.
	 *
	 * @param Array $config Name/value array of config options.
	 * @param bool $setup_values True/false if the paths etc of the config should be set to default values or not.
	 */
	function setConfig($config, $setup_values = true) {
		$this->_rootPaths = array();

		if ($setup_values) {
			// Auto add rootpaths and force them absolute in config
			$newRoots = array();
			$roots = explode(';', $config['filesystem.rootpath']);
			foreach ($roots as $root) {
				$rootParts = explode('=', $root);

				// Unnamed root
				if (count($rootParts) == 1) {
					$rootParts[0] = $this->removeTrailingSlash($this->toAbsPath($rootParts[0]));
					$this->addRootPath($rootParts[0]);
				}

				// Named root
				if (count($rootParts) == 2) {
					$rootParts[1] = $this->removeTrailingSlash($this->toAbsPath($rootParts[1]));
					$this->addRootPath($rootParts[1]);
				}

				$newRoots[] = implode('=', $rootParts);
			}

			$config['filesystem.rootpath'] = implode(';', $newRoots);

			// Force absolute path
			if ($config['filesystem.path'] == "")
				$config['filesystem.path'] = $this->_rootPaths[0];
			else
				$config['filesystem.path'] = $this->removeTrailingSlash($this->toAbsPath($config['filesystem.path']));

			// Setup absolute wwwroot
			if (isset($config['preview.wwwroot']) && $config['preview.wwwroot'])
				$config['preview.wwwroot'] = $this->toUnixPath($this->toAbsPath($config['preview.wwwroot']));
			else
				$config['preview.wwwroot'] = $this->getSiteRoot();

			// Setup preview.urlprefix
			if ($config["preview.urlprefix"]) {
				if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on")
					$config["preview.urlprefix"] = str_replace("{proto}", "https", $config["preview.urlprefix"]);
				else
					$config["preview.urlprefix"] = str_replace("{proto}", "http", $config["preview.urlprefix"]);

				$config["preview.urlprefix"] = str_replace("{host}", $_SERVER['HTTP_HOST'], $config["preview.urlprefix"]);
				$config["preview.urlprefix"] = str_replace("{port}", $_SERVER['SERVER_PORT'], $config["preview.urlprefix"]);
			}
		}

		$this->_config =& $config;
	}

	function getType() {
		return $this->_type;
	}

	function setLangPackPath($path) {
		$this->_langPackPath = $path;
	}

	function getLangPackPath() {
		return $this->_langPackPath;
	}

	/**
	 * Returns a LanguagePack instance of the current language pack.
	 *
	 * @return LanguagePack Instance of the current language pack.
	 */
	function &getLangPack() {
		if (!$this->_language) {
			$config = $this->getConfig();
			$this->_language = new Moxiecode_LanguagePack();
			$this->_language->load($this->toAbsPath("language/" . $this->_langPackPath . "/" . $config['general.language'] . ".xml"));
		}

		return $this->_language;
	}

	/**
	 * Returns a lang item by group and key.
	 *
	 * @param string $group Language group to look in.
	 * @param string $item Item to get inside group.
	 * @param Array $replace Name/Value array of variables to replace in language pack.
	 * @return string Language pack string loaded from XML file.
	 */
	function getLangItem($group, $item, $replace = array()) {
		$pack =& $this->getLangPack();

		$string = $pack->get($group, $item);

		foreach ($replace as $key => $val)
			$string = str_replace("{". $key ."}", $val, $string);

		return $string;
	}

	/**
	 * Returns an array of plugin paths to be loaded/required/includes.
	 *
	 * @return Array Array of file paths to load.
	 */
	function getPluginPaths() {
		global $basepath;

		$config = $this->getConfig();
		$plugins = array();

		if (isset($config["general.plugins"]) && $config["general.plugins"])
			$plugins = preg_split("/,/i", $config["general.plugins"]);

		$out = array();
		foreach($plugins as $plugin) {
			if (!isset($this->_plugins[strtolower($plugin)])) {
				// Check for single file plugins
				if (file_exists($basepath . "../plugins/" . $plugin . ".php"))
					$out[] = "plugins/" . $plugin . ".php";
				else
					$out[] = "plugins/" . $plugin . "/". $plugin . ".php";
			}
		}

		// Check the string first
		if ($config["authenticator"] == "")
			$config["authenticator"] = "BaseAuthenticator";

		$authenticators = array();
		// Check string for delimiter, preg_split returns php warning if delimiter is not found!
		if (strpos($config["authenticator"], "+") || strpos($config["authenticator"], "|"))
			$authenticators = preg_split("/\+|\|/i", $config["authenticator"], -1, PREG_SPLIT_NO_EMPTY);

		if (count($authenticators) != 0) {
			foreach ($authenticators as $auth) {
				// Check for single file plugins
				if (!isset($this->_plugins[strtolower($auth)])) {
					if (file_exists($basepath . "../plugins/" . $auth . ".php"))
						$out[] = "plugins/" . $auth . ".php";
					else
						$out[] = "plugins/" . $auth . "/". $auth . ".php";
				}
			}
		} else {
			if (!isset($this->_plugins[strtolower($config["authenticator"])])) {
				if (file_exists($basepath . "Authenticators/" . $config["authenticator"] . ".php"))
					$out[] = "classes/Authenticators/" . $config["authenticator"] . ".php";
				else if (file_exists($basepath . "../plugins/" . $config["authenticator"] . ".php"))
					$out[] = "plugins/" . $config["authenticator"] . ".php";
				else
					$out[] = "plugins/" . $config["authenticator"] . "/" . $config["authenticator"] . ".php";
			}
		}

		// Check so that they all exists
		foreach ($out as $path) {
			if (!file_exists($basepath . "../" . $path))
				trigger_error("Plugin could not be found: " . $path, FATAL);
		}

		return $out;
	}

	/**
	 * Adds a path to the list of root paths.
	 *
	 * @param String $path Path to add must be a absolute path.
	 */
	function addRootPath($path) {
		$this->_rootPaths[] = $path;
	}

	/**
	 * Returns an array of root paths.
	 *
	 * @return Array Root paths.
	 */
	function getRootPaths() {
		return $this->_rootPaths;
	}

	/**
	 * Returns a plugin by id/name.
	 *
	 * @param String $name Plugin id/name.
	 * @return MCManagerPlugin instance object.
	 */
	function getPlugin($name) {
		return isset($this->_plugins[$name]) ? $this->_plugins[$name] : null;
	}

	/**
	 * Returns a true/false check for plugin.
	 *
	 * @param String $name Plugin id/name.
	 * @return Bool true/false
	 */
	function hasPlugin($name) {
		return isset($this->_plugins[$name]);
	}

	/**
	 * Returns a name/value array of plugins.
	 *
	 * @return Array name/value array of MCManagerPlugin instances.
	 */
	function getPlugins() {
		return $this->_plugins;
	}

	/**
	 * Registers a plugin by id/name.
	 *
	 * @param $name Id/name to register plugin by.
	 * @param $plugin Plugin instance to register/add to list.
	 *
	 * @return SmallMCEPlugin The added plugin instance.
	 */
	function &registerPlugin($name, &$plugin, $prefix = false) {
		$name = strtolower($name);
		$this->_plugins[$name] =& $plugin;

		if ($prefix != false)
			$this->_prefixes[$name] = $prefix;

		return $plugin;
	}

	/**
	 * Returns the MCManager config as name/value array.
	 *
	 * @return Array MCManager config as name/value array.
	 */
	function &getConfig() {
		return $this->_config;
	}

	/**
	 * Returns the a config item by name.
	 *
	 * @param string $key Config item key to retrive.
	 * @param string $def Default value to return.
	 * @return mixed config item by name.
	 */
	function getConfigItem($key, $def = false) {
		return isset($this->_config[$key]) ? $this->_config[$key] : $def;
	}

	/**
	 * Returns a merged JS config. It will only export configured items controlled by the allow_export suffix.
	 *
	 * @return Array Name/Value array of JS config options.
	 */
	function &getJSConfig($config = false, $prefixes = '*') {
		$encrypted = array("filesystem.path", "filesystem.rootpath", "filesystem.directory_templates");
		$jsConfig = array();
		$prefixes = explode(",", $prefixes);

		if (!$config)
			$config = $this->getConfig();

		foreach ($config as $name => $value) {
			$pos = strpos($name, ".allow_export");

			if ($pos > 0) {
				$names = explode(",", $value);
				$prefix = substr($name, 0, $pos);

				if (in_array("*", $prefixes) || in_array($prefix, $prefixes)) {
					foreach ($names as $key) {
						$key = $prefix . "." . $key ;

						// Encrypt some paths
						if (in_array($key, $encrypted))
							$jsConfig[$key] = "" . $this->encryptPath($config[$key]);
						else
							$jsConfig[$key] = "" . is_bool($config[$key]) ? ($config[$key] ? "true" : "false") : $config[$key];
					}
				}
			}
		}

		return $jsConfig;
	}

	/**
	 * Encrypts the specified path so that it doesn't contain full paths on the client side of the application.
	 *
	 * @param string $path Path to encrypt.
	 * @return string Encrypted short path.
	 */
	function encryptPath($path) {
		$config = $this->getConfig();

		if (checkBool($config['general.encrypt_paths'])) {
			$count = 0;

			foreach ($this->_rootPaths as $rootPath) {
				// Needs encryption?
				if ($rootPath != "/")
					$path = str_replace($rootPath, "{" . $count++ . "}", $path);
			}
		}

		return utf8_encode($path);
	}

	/**
	 * Decrypts the specified path from a non absolute path to a absolute path.
	 *
	 * @param string $path Path to decrypt.
	 * @return string Decrypted absolute path.
	 */
	function decryptPath($path) {
		if (!$path)
			return "";

		$count = 0;
		$path = $this->toUnixPath($path);

		// Is relative path
		if (!(strpos($path, '/') === 0 || strpos($path, ':') !== false || strpos($path, '{') !== false))
			$path = realpath(dirname(__FILE__) . '/../' . $path);

		foreach ($this->_rootPaths as $rootPath)
			$path = str_replace("{" . $count++ . "}", $rootPath, $path);

		$path = str_replace("{default}", $this->_config["filesystem.path"], $path);

		return $path;
	}

	/**
	 * isAuthenticated checks against the configuration and sends events to auth plugins.
	 * This method will also call the onBeforeInit, onInit and onAfterInit methods if the user was authenticated.
	 *
	 * @return bool Returns true if authenticated, false if not.
	 */
	function isAuthenticated() {
		$config = $this->getConfig();

		$authenticators = strtolower($config["authenticator"]);

		// Check the string first
		if ($authenticators != "" && $authenticators != "BaseAuthenticator") {
			if (strpos($authenticators, "|") && strpos($authenticators, "+"))
				trigger_error("You can not use both + and | at the same time for adding authenticators.", FATAL);

			$pass = false;

			// Check for AND authenticators
			if (strpos($authenticators, "+")) {
				$authArray = preg_split("/\+/i", $authenticators, -1, PREG_SPLIT_NO_EMPTY);

				if (!$authArray)
					trigger_error("No Authenticator could be used.", FATAL);

				// Verify that all authenticators exists
				foreach($authArray as $auth) {
					if (!$this->hasPlugin($auth))
						trigger_error("Authenticator \"". htmlentities($auth) ."\" was not found.", FATAL);
				}

				// Default to true
				$pass = true;

				// Send AND event
				foreach ($authArray as $auth) {
					$plugin = $this->getPlugin($auth);
					if ($pass && !$plugin->onAuthenticate($this))
						$pass = false;
				}

			// Check for OR authentocator string
			} else if (strpos($authenticators, "|")) {
				$authArray = preg_split("/\|/i", $authenticators, -1, PREG_SPLIT_NO_EMPTY);

				if (!$authArray)
					trigger_error("No Authenticator could be used.", FATAL);

				// Verify that all authenticators exists
				foreach ($authArray as $auth) {
					if (!$this->hasPlugin($auth))
						trigger_error("Authenticator \"". htmlentities($auth) ."\" was not found.", FATAL);
				}

				// Default to false

				$pass = false;
				// Send OR event
				foreach ($authArray as $auth) {
					$plugin = $this->getPlugin($auth);
					if ($plugin->onAuthenticate($this))
						$pass = true;
				}

			} else {
				$plugin = $this->getPlugin($authenticators);

				if ($plugin->onAuthenticate($this))
					$pass = true;
			}
		} else
			$pass = true;

		// Is authenticated, call onInit
		if ($pass)
			$this->dispatchEvent("onInit");

		// Set config again to update rootpaths etc
		$this->setConfig($this->_config);

		return $pass;
	}

	/**
	 * Dispatches a event to all registered plugins. This method will loop through all plugins and call the specific event if this
	 * event method returns false the chain will be terminated.
	 *
	 * @param String $event Event name to be dispatched for example onAjaxCommand.
	 * @param Array $args Optional array with arguments.
	 * @return Bool Returns true of a plugin returned true, false if not.
	 */
	function dispatchEvent($event, $args=false) {
		// Setup event arguments
		$keys = array_keys($this->_plugins);

		for ($i=0; $i<count($keys); $i++) {
			$plugin =& $this->_plugins[$keys[$i]];

			// Valid prefix
			if (isset($this->_prefixes[$keys[$i]])) {
				if ($this->_type != $this->_prefixes[$keys[$i]])
					continue;
			}

			switch ($event) {
				case "onAuthenticate":
					if (!$plugin->onAuthenticate($this))
						return false;

					break;

				case "onInit":
					if (!$plugin->onInit($this))
						return false;

					break;

				case "onPreInit":
					if (!$plugin->onPreInit($this, $args[0]))
						return false;

					break;

				case "onLogin":
					if (!$plugin->onLogin($this))
						return false;

					break;

				case "onLogout":
					if (!$plugin->onLogout($this))
						return false;

					break;

				case "onBeforeFileAction":
					if (!isset($args[2]))
						$args[2] = null;

					if (!$plugin->onBeforeFileAction($this, $args[0], $args[1], $args[2]))
						return false;

					break;

				case "onFileAction":
					if (!isset($args[2]))
						$args[2] = null;

					if (!$plugin->onFileAction($this, $args[0], $args[1], $args[2]))
						return false;

					break;

				case "onBeforeRPC":
					if (!$plugin->onBeforeRPC($this, $args[0], $args[1]))
						return false;

					break;

				case "onBeforeStream":
					if (!$plugin->onBeforeStream($this, $args[0], $args[1]))
						return false;

					break;

				case "onStream":
					if (!$plugin->onStream($this, $args[0], $args[1]))
						return false;

					break;

				case "onAfterStream":
					if (!$plugin->onAfterStream($this, $args[0], $args[1]))
						return false;

					break;

				case "onBeforeUpload":
					if (!$plugin->onBeforeUpload($this, $args[0], $args[1]))
						return false;

					break;

				case "onAfterUpload":
					if (!$plugin->onAfterUpload($this, $args[0], $args[1]))
						return false;

					break;

				case "onCustomInfo":
					if (!$plugin->onCustomInfo($this, $args[0], $args[1], $args[2]))
						return false;

					break;

				case "onListFiles":
					if (!$plugin->onListFiles($this, $args[0], $args[1]))
						return false;

					break;

				case "onInsertFile":
					if (!$plugin->onInsertFile($this, $args[0]))
						return false;

					break;
			}
		}

		return true;
	}

	/**
	 * Executes a event in all registered plugins if a plugin returns a object or array the execution chain will be
	 * terminated.
	 *
	 * @param String $event Event name to be dispatched for example onAjaxCommand.
	 * @param Array $args Optional array with arguments.
	 * @return Bool Returns true of a plugin returned true, false if not.
	 */
	function executeEvent($event, $args=false) {
		// Setup event arguments
		$keys = array_keys($this->_plugins);

		for ($i=0; $i<count($keys); $i++) {
			$plugin =& $this->_plugins[$keys[$i]];

			// Valid prefix
			if (isset($this->_prefixes[$keys[$i]])) {
				if ($this->_type != $this->_prefixes[$keys[$i]])
					continue;
			}

			switch ($event) {
				case "onRPC":
					$result =& $plugin->onRPC($this, $args[0], $args[1]);

					if (!is_null($result))
						return $result;

					break;

				case "onUpload":
					$result =& $plugin->onUpload($this, $args[0], $args[1]);

					if (!is_null($result))
						return $result;

					break;
			}
		}

		return null;
	}

	function getInvalidFileMsg() {
		return $this->_invalidFileMsg;
	}

	/**
	 * Returns the wwwroot if it fails it will trigger a fatal error.
	 *
	 * @return String wwwroot or null string if it was impossible to get.
	 */
	function getSiteRoot() {
		// Check config
		if (isset($this->_config['preview.wwwroot']) && $this->_config['preview.wwwroot'])
			return $this->toUnixPath(realpath($this->_config['preview.wwwroot']));

		// Try script file
		if (isset($_SERVER["SCRIPT_NAME"]) && isset($_SERVER["SCRIPT_FILENAME"])) {
			$path = str_replace($this->toUnixPath($_SERVER["SCRIPT_NAME"]), "", $this->toUnixPath($_SERVER["SCRIPT_FILENAME"]));

			if (is_dir($path))
				return $this->toUnixPath(realpath($path));
		}

		// If all else fails, try this.
		if (isset($_SERVER["SCRIPT_NAME"]) && isset($_SERVER["PATH_TRANSLATED"])) {
			$path = str_replace($this->toUnixPath($_SERVER["SCRIPT_NAME"]), "", str_replace("//", "/", $this->toUnixPath($_SERVER["PATH_TRANSLATED"])));

			if (is_dir($path))
				return $this->toUnixPath(realpath($path));
		}

		// Check document root
		if (isset($_SERVER['DOCUMENT_ROOT']))
			return $this->toUnixPath(realpath($_SERVER['DOCUMENT_ROOT']));

		trigger_error("Could not resolve WWWROOT path, please set an absolute path in preview.wwwroot config option. Check the Wiki documentation for details.", FATAL);

		return null;
	}

	/**
	 * Returns a absolute file system path of a absolute URI path for example /mydir/myfile.htm
	 * will be resolved to /www/mywwwroot/mydir/myfile.htm.
	 *
	 * @param String $uri Absolute URI path for example /mydir/myfile.htm
	 * @return String Absolute file system path or empty string on failure.
	 */
	function resolveURI($uri) {
		return realpath($this->getSiteRoot() . $uri);
	}

	/**
	 * Returns a site absolute path from a absolute file system path for example /www/mywwwroot/mydir/myfile.htm
	 * will be converted to /mydir/myfile.htm.
	 *
	 * @param String $abs_path Absolute path for example /mydir/myfile.htm
	 * @return String Site absolute path (URI) or empty string on failure.
	 */
	function convertPathToURI($abs_path) {
		$log =& $this->getLogger();
		$root = $this->getSiteRoot();

		if (!$root) {
			trigger_error("Could not resolve WWWROOT path, please set an absolute path in preview.wwwroot config option.", FATAL);
			die();
		}

		if ($root == "/") {
			if ($log && $log->isDebugEnabled())
				$log->info("ConvertPathToURI: SiteRoot=" . $root . ", Path: " . $abs_path . " -> URI: " . $abs_path);

			return $abs_path;
		}

		$uri = substr($abs_path, strlen($root));

		if ($log && $log->isDebugEnabled())
			$log->info("ConvertPathToURI: SiteRoot=" . $root . ", Path: " . $abs_path . " -> URI: " . $uri);

		return $uri;
	}

	/**
	 * Converts a URI such as /somedir/somefile.gif to /system/path/somedir/somefile.gif
	 *
	 * @param String $uri URI to convert to path.
	 * @return String Path out of URI.
	 */
	function convertURIToPath($uri) {
		$log =& $this->getLogger();

		if ($log && $log->isDebugEnabled())
			$log->info("ConvertURIToPath: SiteRoot=" . $this->getSiteRoot() . ", URI: " . $uri . " -> Path: " . $this->removeTrailingSlash($this->getSiteRoot()) . $uri);

		return $this->removeTrailingSlash($this->getSiteRoot()) . $uri;
	}

	/**
	 * Converts an path into a visualy presentatble path. So that special folder names
	 * gets translated and root paths gets replaced with their names.
	 *
	 * @param String $path Path to convert into a visual path.
	 * @return String Visual path based on input path.
	 */
	function toVisualPath($path, $root = false) {
		$fs = "file";

		// Parse out FS
		if (preg_match('/([a-z]+):\/\/(.+)/', $path, $matches)) {
			$fs = $matches[1];
			$path = $matches[2];
		}

		$path = $this->decryptPath($path);

		// Speficied root
		if ($root) {
			$pos = strpos($path, $root);

			if ($pos === 0)
				$path = substr($path, strlen($root));

			if ($path == "")
				$path = "/";

			// Re-attach fs
			if ($fs != "file")
				$path = $fs . "://" . $path;

			return $this->encryptPath($path);
		}

		// Use config roots
		$rootNames = $this->_getRootNames();
		foreach ($rootNames as $rootPath => $name) {
			$pos = strpos($path, $rootPath);

			if ($pos === 0) {
				$path = substr($path, strlen($rootPath));

				if ($name == "/")
					$name = "";

				$path = "/" . $name . $path;
			}
		}

		if (!$path)
			$path = "/";

		// Re-attach fs
		if ($fs != "file")
			$path = $fs . "://" . $path;

		return $this->encryptPath($path);
	}

	/**
	 * Verifies that a path is within the parent path.
	 *
	 * @param String $parent_path Parent path that must contain the path.
	 * @param String $path Path that must contained the parent path.
	 * @return Bool true if it's valid, false if it's not. 
	 */
	function isChildPath($parent_path, $path) {
		return strpos(strtolower($path), strtolower($parent_path)) === 0;
	}

	/**
	 * Checks if a specific tool is enabled or not.
	 *
	 * @param string $tool Tool to check for.
	 * @param Array $config Name/Value config array to check tool against.
	 * @return bool true/false if the tool is enabled or not.
	 */
	function isToolEnabled($tool, $config = false) {
		if (!$config)
			$config = $this->getConfig();

		$ar = explode(',', $config['general.disabled_tools']);
		if (in_array($tool, $ar))
			return false;

		$ar = explode(',', $config['general.tools']);
		if (in_array($tool, $ar))
			return true;

		return false;
	}

	/**
	 * Verifies that the specified path is within valid root paths.
	 *
	 * @param String $path Path to verify.
	 * @return Bool true if the path is valid, false if it's invalid.
	 */
	function verifyPath($path) {
		$fs = "file";
		$valid = false;

		// Parse out FS
		if (preg_match('/([a-z]+):\/\/(.+)/', $path, $matches)) {
			$fs = $matches[1];
			$path = $matches[2];
		}

		// Filesystem wasn't found
		if (!isset($this->_fileSystems[$fs])) {
			trigger_error($this->getLangItem("error", "no_filesystem", array("path" => $path)), FATAL);
			die();
		}

		$path = $this->decryptPath($path);

		// /../ is never valid
		if (indexOf($this->addTrailingSlash($path), "/../") != -1)
			return false;

		if ($fs != 'file')
			return true;

		foreach ($this->_rootPaths as $rootPath) {
			if ($this->isChildPath($rootPath, $path))
				$valid = true;
		}

		return $valid;
	}

	/**
	 * Returns the file system for a path for file if it couldn't be extracted.
	 *
	 * @param string $path Path to get FS from.
	 * @return string Filesystem for path.
	 */
	function getFSFromPath($path) {
		$fs = "file";

		// Parse out FS
		if (preg_match('/([a-z]+):\/\/(.+)/', $path, $matches))
			$fs = $matches[1];

		return $fs;
	}

	/**
	 * Verifies that the specified file is valid agains the filters specified in config.
	 *
	 * @param String $path Path to verify.
	 * @param String $action Action to get config options by.
	 * @param Array $config Name/Value array of config options.
	 * @return int Reason why it was denied.
	 */
	function verifyFile($file, $action = false, $config = false) {
		$config = $config ? $config : $file->getConfig();

		// Verify filesystem config
		$fileFilter = new Moxiecode_BasicFileFilter();
		$fileFilter->setIncludeDirectoryPattern($config['filesystem.include_directory_pattern']);
		$fileFilter->setExcludeDirectoryPattern($config['filesystem.exclude_directory_pattern']);
		$fileFilter->setIncludeFilePattern($config['filesystem.include_file_pattern']);
		$fileFilter->setExcludeFilePattern($config['filesystem.exclude_file_pattern']);
		$fileFilter->setIncludeExtensions($config['filesystem.extensions']);

		$this->_invalidFileMsg = "{#error.invalid_filename}";

		$status = $fileFilter->accept($file);

		if ($status != BASIC_FILEFILTER_ACCEPTED) {
			if ($status == BASIC_FILEFILTER_INVALID_NAME) {
				if ($file->isFile() && isset($config['filesystem.invalid_file_name_msg']))
					$this->_invalidFileMsg = $config['filesystem.invalid_file_name_msg'];
				else if (!$file->isFile() && isset($config['filesystem.invalid_directory_name_msg']))
					$this->_invalidFileMsg = $config['filesystem.invalid_directory_name_msg'];

				if (!$this->_invalidFileMsg)
					$this->_invalidFileMsg = "{#error.invalid_filename}";
			}

			return $status;
		}

		// Verify action specific config
		$fileFilter = new Moxiecode_BasicFileFilter();

		if ($action) {
			if (isset($config[$action . '.include_directory_pattern']))
				$fileFilter->setIncludeDirectoryPattern($config[$action . '.include_directory_pattern']);

			if (isset($config[$action . '.exclude_directory_pattern']))
				$fileFilter->setExcludeDirectoryPattern($config[$action . '.exclude_directory_pattern']);

			if (isset($config[$action . '.include_file_pattern']))
				$fileFilter->setIncludeFilePattern($config[$action . '.include_file_pattern']);

			if (isset($config[$action . '.exclude_file_pattern']))
				$fileFilter->setExcludeFilePattern($config[$action . '.exclude_file_pattern']);

			if (isset($config[$action . '.extensions']))
				$fileFilter->setIncludeExtensions($config[$action . '.extensions']);
		} else
			return BASIC_FILEFILTER_ACCEPTED;

		$status = $fileFilter->accept($file);

		if ($status != BASIC_FILEFILTER_ACCEPTED) {
			if ($status == BASIC_FILEFILTER_INVALID_NAME) {
				$this->_invalidFileMsg = "{#error.invalid_filename}";

				if ($file->isFile()) {
					if (isset($config[$action . '.invalid_file_name_msg']))
						$this->_invalidFileMsg = $config[$action . '.invalid_file_name_msg'];
				} else {
					if (isset($config[$action . '.invalid_directory_name_msg']))
						$this->_invalidFileMsg = $config[$action . '.invalid_directory_name_msg'];
				}
			}

			return $status;
		}

		return BASIC_FILEFILTER_ACCEPTED;
	}

	/**
	 * Returns a file object represtentation of a file path this
	 * will also do security checks agains the list of valid paths
	 * so that file IO can't be done outside the valid paths.
	 *
	 * @param String $path Path to return as File object.
	 * @param String $file_name Optional file name.
	 * @param String $type Optional file type.
	 * @return File File object representation of a file.
	 */
	function &getFile($path, $file_name = "", $type = MC_IS_FILE) {
		$path = utf8_decode($path);
		$file_name = utf8_decode($file_name);
		$fs = 'file';
		$matches = array();
		$oldpath = $path;

		// Parse out FS
		if (preg_match('/([a-z]+):\/\/(.+)/', $path, $matches)) {
			$fs = $matches[1];
			$path = $matches[2];
		}

		// Filesystem wasn't found
		if (!isset($this->_fileSystems[$fs])) {
			trigger_error($this->getLangItem("error", "no_filesystem", array("path" => $path)), FATAL);
			die();
		}

		$path = $this->decryptPath($path);
		$path = $this->removeTrailingSlash($this->toUnixPath($path));

		// Verfiy path if no file was returned
		if ($fs == 'file' && !$this->verifyPath($path)) {
			$log =& $this->getLogger();
			if ($log && $log->isDebugEnabled())
				$log->debug("Could not access path: " . $path);

			trigger_error("{#error.no_access}", FATAL);
			die();
		}

		// Validate file name
		if ($fs == 'file' && $file_name) {
			if (preg_match('/[\\\\\\/:]+/', $file_name, $matches)) {
				$log =& $this->getLogger();
				if ($log && $log->isDebugEnabled())
					$log->debug("Could not access path: " . $path);

				trigger_error("{#error.no_access}", FATAL);
				die();
			}
		}

		// Get file instance
		$file = new $this->_fileSystems[$fs]($this, $path, $file_name, $type);

		// Verfiy path if no file was returned
		if ($fs == 'file' && !$this->verifyPath($file->getAbsolutePath())) {
			$log =& $this->getLogger();
			if ($log && $log->isDebugEnabled())
				$log->debug("Could not access path: " . $path);

			trigger_error("{#error.no_access}", FATAL);
			die();
		}

		return $file;
	}

	/**
	 * Converts a relative path to absolute path.
	 *
	 * @param string $path Path to convert to absolute.
	 * @param string $basepath Optional base path default to ../.
	 */
	function toAbsPath($path, $basepath = false) {
		$path = $this->toUnixPath($path);

		if (!$basepath)
			$basepath = dirname(__FILE__) . "/../";

		// Is absolute unix or windows
		if (substr($path, 0, 1) == '/' || strpos($path, ":") !== false) {
			// Resolve symlinks
			$tmp = realpath($path);

			if ($tmp)
				$path = $tmp;

			return $this->toUnixPath($path);
		}

		$path = $this->toUnixPath($this->addTrailingSlash($this->toUnixPath(realpath($basepath))) . $path);

		// Local FS and exists remove any ../../
		if (strpos($path, "://") === false && file_exists($path))
			$path = $this->toUnixPath(realpath($path));

		return $path;
	}

	/**
	 * Converts a Unix path to OS specific path.
	 *
	 * @param String $path Unix path to convert.
	 */
	function toOSPath($path) {
		return str_replace("/", DIRECTORY_SEPARATOR, $path);
	}

	/**
	 * Converts a OS specific path to Unix path.
	 *
	 * @param String $path OS path to convert to Unix style.
	 */
	function toUnixPath($path) {
		return str_replace(DIRECTORY_SEPARATOR, "/", $path);
	}

	/**
	 * Adds a trailing slash to a path.
	 *
	 * @param String path Path to add trailing slash on.
	 * @return String New path with trailing slash.
	 */
	function addTrailingSlash($path) {
		if (strlen($path) > 0 && $path[strlen($path)-1] != '/')
			$path .= '/';

		return $path;
	}

	/**
	 * Removes the trailing slash from a path.
	 *
	 * @param String path Path to remove trailing slash from.
	 * @return String New path without trailing slash.
	 */
	function removeTrailingSlash($path) {
		// Is root
		if ($path == "/")
			return $path;

		if ($path == "")
			return $path;

		if ($path[strlen($path)-1] == '/')
			$path = substr($path, 0, strlen($path)-1);

		return $path;
	}

	/**
	 * Adds a new file system bu name.
	 *
	 * @param String $protocol File protocol like zip/ftp etc.
	 * @param String $file_system Name of class to create instances by.
	 */
	function registerFileSystem($protocol, $file_system) {
		$this->_fileSystems[$protocol] = $file_system;
	}

	/**
	 * Returns a logger instance.
	 *
	 * @return Logger New logger instance.
	 */
	function &getLogger() {
		if (!$this->_logger) {
			$log = new Moxiecode_Logger();

			$null = null; // PHP why!!! Other languages can return null
			if (!checkBool($this->getConfigItem("log.enabled")))
				return $null;

			// Set logger options
			$log->setLevel($this->getConfigItem("log.level", "fatal"));
			$log->setPath($this->toAbsPath($this->getConfigItem("log.path", "logs")));
			$log->setFileName($this->getConfigItem("log.filename", "{level}.log"));
			$log->setFormat($this->getConfigItem("log.format", "[{time}] [{level}] {message}"));
			$log->setMaxSize($this->getConfigItem("log.max_size", "100k"));
			$log->setMaxFiles($this->getConfigItem("log.max_files", "10"));

			$this->_logger = $log;
		}

		return $this->_logger;
	}

	// * * * * * * * Private methods

	function _getRootNames() {
		$config = $this->getConfig();
		$output = array();

		$roots = explode(';', $config['filesystem.rootpath']);
		foreach ($roots as $root) {
			$rootParts = explode('=', $root);

			if (count($rootParts) > 1)
				$output[$rootParts[1]] = $rootParts[0];
			else {
				$output[$rootParts[0]] = basename($root);

				// If it's root
				if ($output[$rootParts[0]] == "")
					$output[$rootParts[0]] = "/";
			}
		}

		return $output;
	}

	/**#@-*/
}

?>