Location: PHPKode > projects > Mocovie web framework > mocovi/library/filesystems/XMLFS.php
<?php
if(version_compare(LIBXML_DOTTED_VERSION, '2.6.32', '<')) die(__FILE__.': libxml >= v2.6.32 is needed! You have libxml v'.LIBXML_DOTTED_VERSION);
/**
 *  Copyright (C) 2010  Kai Dorschner
 *
 *  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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * @author Kai Dorschner <the-hide@address.com>
 * @copyright Copyright 2010, Kai Dorschner
 * @license http://www.gnu.org/licenses/gpl.html GPLv3
 * @package mocovi
 * @subpackage filesystems
 */

/**
 * Require Filesystem.
 */
class_exists('FS')					or require $GLOBALS['filesystemsLibrary'].'FS.php';
class_exists('ClientDetermination')	or require $GLOBALS['library'].'ClientDetermination.php';

/**
 * XML Filesystem
 *
 * This filesystem consists of XML and contains all files and each model of a file.
 *
 * @package mocovi
 * @subpackage filesystems
 */
class XMLFS extends FS
{
	/**
	 * Reference of the Document Object Model set in the constructor
	 *
	 * With this reference you are able to create, move, update or delete
	 * XML-elements of the page-model
	 *
	 * @var object Contains a DOM reference
	 * @see function __constructor()
	 */
	protected $dom;

	/**
	 * XPath object referencing the {@see $dom} in the {@see __constructor()}
	 *
	 * This is needed to select nodes by Xpath queries (XQuery)
	 *
	 * @var object Xpath Object containing the {@see $dom}
	 * @see function __constructor()
	 */
	public $xpath;

	/**
	 * Reference-element Namespace
	 *
	 * Reference-elements are replaced as soon as the setReferences();
	 * method is called. This method searches all nodes by this referenceNamespace
	 * variable. If the Namespace changes in model-XML, you have to change this
	 * variable too.
	 *
	 * @var string Contains the cross-reference namespace
	 */
	public $referenceNamespace = 'x-schema:refSchema.xml';

	public $includeNamespace = 'http://www.w3.org/2001/XInclude';

	public $masterNamespace = 'http://mocovi.de/schema/masterfs1.0';

	/**
	 * Contains the namespace of XML
	 *
	 * @var string Contains the XML namespace
	 */
	public $xmlNamespace = 'http://www.w3.org/XML/1998/namespace';

	/**
	 * The node name of a reference Tag
	 *
	 * @var string The name of the node
	 */
	protected $referenceTagName = 'reference';

	protected $tip = ' Tip: Define a 404 file in the filesystem to get a soft 404 error.';

	const INCLUDE_INVISIBLES = true;

	/**
	 * Set {@see $dom} object and creating {@see $xpath} object containing {@see $dom}.
	 *
	 * @return void
	 */
	public function __construct($xmlpath)
	{
		if(is_file($xmlpath))
		{
			$this->source = $xmlpath;
			$this->dom = new DomDocument();
			$this->dom->formatOutput = false;
			$this->dom->preserveWhiteSpace = false;
			$this->dom->load($xmlpath);
			$this->dom->XInclude(); // Execute XIncludes
			$this->xpath = new DomXPath($this->dom);
			$this->xpath->registerNameSpace('xref', $this->referenceNamespace);
			$this->xpath->registerNameSpace('xml', $this->xmlNamespace);
			$this->xpath->registerNameSpace('mocovi', Mocovi::$namespace);
			$this->xpath->registerNameSpace('master', $this->masterNamespace);
		}
		else
			throw new MvcException('Couldn\'t load filesystem source "'.$xmlpath.'"');
	}

	/**
	 * Returns a list of "mounted" filesystems.
	 *
	 * @access public
	 * @static
	 * @return Array List of filesystems.
	 */
	public static function getAvailableFilesystems()
	{
		if($handle = @opendir(self::getPool()))
		{
			$filesystems = array();
			while($element = readdir($handle))
				if(is_file(self::getPool().$element))
					$filesystems[] = $element;
			closedir();
			return $filesystems;
		}
		throw new MvcException('Filesystem pool "'.self::getPool().'" not found.');
	}
	public static function getPool()
	{
		return $GLOBALS['filesystems'];
	}

	/**
	 * Returns all references in the filesystem as an object type of XmlNodeList.
	 *
	 * @access protected
	 * @return XmlNodeList Containts all XML references.
	 */
	protected function getReferences(DOMNode $contextNode = null)
	{
		if(!$contextNode) $contextNode = $this->dom->documentElement;
		return $this->xpath->query('//xref:'.$this->referenceTagName, $contextNode);
	}


	/**
	 * Parses the model-DOM for internal node-references and replaces them.
	 *
	 * The reference nodes are searched by the NameSpace URI defined in the
	 * {@see $referenceNamespace} property. The ref-attribute selects the internal reference
	 * node, clones and replaces itself with it.
	 *
	 * @access private
	 * @return void
	 * @see $dom
	 */
	protected function setReferences(DOMNode $contextNode = null)
	{
		if(!$contextNode) $contextNode = $this->dom->documentElement;
		foreach($this->getReferences($contextNode) as $reference)
		{
			if($element = $this->dom->getElementById($reference->getAttribute('ref')))
			{
				$clone = $element->cloneNode(true);
				$clone->removeAttributeNS($this->xmlNamespace, 'id');
			}
			else
			{
				// @todo check if this works properly
				$clone = ControlFactory::createError($this->source, 'Reference "'.$reference->getAttribute('ref').'" is not present (line '.$reference->getLineNo().')')->load($contextNode)->run();
				// $clone = $this->dom->createElement('error');
				// $clone->setAttribute('file', $this->source);
				// $clone->setAttribute('message', 'Reference "'.$reference->getAttribute('ref').'" is not present (line '.$reference->getLineNo().')');
			}
			$reference->parentNode->insertBefore($clone, $reference);
		}
		$this->deleteReferences($contextNode);
	}

	/**
	 * Cleaning up the model-DOM.
	 *
	 * Deletes all references.
	 *
	 * @access private
	 * @param DomDocument $dom Contains the model-DOM.
	 * @return void
	 */
	protected function deleteReferences(DomNode $contextNode = null)
	{
		if(!$contextNode) $contextNode = $this->dom->documentElement;
		while(($references = $this->getReferences($contextNode)) && $references->length > 0)
		{
			$element = $references->item(0);
			$element->parentNode->removeChild($element);
		}
	}

	protected function setMaster(DomNode $node = null)
	{
		if($node && $masterReference = $this->xpath->query('master:use', $node)->item(0))
		{
			if($master = $this->xpath->query('//master:model[@name="'.$masterReference->getAttribute('name').'"]')->item(0))
			{
				$masterReference->parentNode->insertBefore($master->childNodes->item(0), $masterReference);
				foreach($this->xpath->query('master:*', $masterReference) as $modifier)
				{
					$selected = $this->xpath->query($modifier->getAttribute('select'), $node);
					switch($modifier->localName)
					{
						case 'insertBefore':
							foreach($selected as $child)
								while($item = $modifier->childNodes->item(0))
									$child->parentNode->insertBefore($item, $child);
						break;
						case 'append':
							foreach($selected as $child)
								while($item = $modifier->childNodes->item(0))
									$child->appendChild($item);
						break;
						case 'remove':
							foreach($selected as $child)
								$child->parentNode->removeChild($child);
						break;
						case 'setAttribute':
							if($modifier->getAttribute('name'))
								foreach($selected as $child)
									$child->setAttribute($modifier->getAttribute('name'), $modifier->getAttribute('value'));
						break;
						
						case 'removeAttribute':
							if($modifier->getAttribute('name'))
								foreach($selected as $child)
									$child->removeAttribute($modifier->getAttribute('name'));
						break;
					}
				}
			}
			$masterReference->parentNode->removeChild($masterReference);
		}
	}


	/**
	 * Returns model-node selected by XQuery.
	 *
	 * It is able to catch a redirection inside file and handle it.
	 * This means a new header will be generated which will guide a user
	 * to the referenced location.
	 *
	 * @access public
	 * @return DomDocument The model node
	 * @param string $path Contains the XQuery
	 * @param boolean $setReferences Should the XRef-references be resoluted or not; Default: true;
	 */
	public function getModel($path = '/', $setReferences = true)
	{
		if($this->redirect && $redirect = $this->getRedirect($path))
		{
			if($redirect == $GLOBALS['page'])
				throw new MvcException('Redirect loop detected: "'.$redirect.'"');
			$GLOBALS['header']->status(301);
			if(isset($_GET['redirect']) && $_GET['redirect'] == 'no')		// User can stop the automated redirection
				$model = $this->createRedirectModel($redirect)->childNodes;	// If stopped a generated page with information about the redirect will be generated
			else
			{
				$GLOBALS['header']->location(Mocovi::buildPath($redirect, ''));
				$GLOBALS['header']->sendHeader();
				exit();
			}
		}
		elseif($item = $this->xpath->query($this->preparePath($path).'/mocovi:model')->item(0))
			$model = $item->childNodes;
		$this->setMaster($item);
		if(isset($model) && $model instanceof DOMNodeList)
		{
			if($setReferences)
				foreach($model as $node)
					$this->setReferences($node);
			return $model;
		}
		else
		{
			$GLOBALS['header']->status(404);
			if($this->isFile('/404'))
				return $this->getModel('/404');
			else
				throw new MvcException('Cannot read model from requested file "'.$path.'". It is empty or does not exist.'.$this->tip);
		}
	}

	public function getFile($path = '/')
	{
		$node = $this->xpath->query(preg_replace('/(.*)\/$/', '\\1', $this->preparePath($path)))->item(0);
		if($node instanceof DOMNode)
			return $node;
		elseif($this->isFile('/404'))
			return $this->getFile('/404');
		else
			throw new MvcException('Cannot read file "'.$path.'". It is empty or does not exist.'.$this->tip);
	}

	/**
	 * Returns keyword-node.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return DomDocument The model node
	 */
	public function getKeywords($path = '/')
	{
		return $this->xpath->query($this->preparePath($path).'mocovi:keywords/*'); // If you put a slash at the beginning it will result in a deep search for keywords. Maybe it's an option...
	}

	/**
	 * Returns a list of a virtual directory.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return array List of all elements in the directory
	 */
	public function getList($path = '/', $showInvisible = FALSE)
	{
		$array = array();
		foreach($this->xpath->query($this->preparePath($path).'mocovi:file[@name'.($showInvisible ? '' : ' and not(@invisible = 1)').']') as $element)
			$array[] = $element;
		return $array;
	}

	public function countChildren($path = '/', $showInvisible = FALSE)
	{
		return $this->xpath->query($this->preparePath($path).'/mocovi:file[@name'.($showInvisible ? '' : ' and not(@invisible = 1)').']')->length;
	}

	public function hasChildren($path = '/', $showInvisible = FALSE)
	{
		return (count($this->getList($path, $showInvisible)) > 0);
	}

	/**
	 * Returns the theme of a model.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return string Name of current theme in the model
	 */
	public function getTheme($path = '/')
	{
		return $this->getValueFromNode($path, 'mocovi:model/@theme');
	}

	/**
	 * Returns the alias name of a virtual file.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return string Alias name of the file
	 */
	public function getAlias($path = '/')
	{
		if(!($alias = $this->getValueFromNode($path, '@nameAliasToken')))
			$alias = $this->getValueFromNode($path, '@nameAlias');
		return $alias;
	}

	/**
	 * Returns the filename of a virtual file.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return string Name of the file
	 */
	public function getName($path = '/')
	{
		return $this->getValueFromNode($path, '@name');
	}

	/**
	 * Returns wether the the file is redirected and the redirect position.
	 *
	 * @access public
	 * @param string $path Contains the XQuery
	 * @return string Redirected file location.
	 */
	public function getRedirect($path = '/')
	{
		return preg_replace('/\.\//', $this->getParentPath($GLOBALS['page']).'/', $this->getValueFromNode($path, '@redirect'));
	}

	public function isRedirect($path = '/')
	{
		return (strlen($this->getRedirect($path)) > 0);
	}

	/**
	 * Returns the user defined priority of a file.
	 *
	 * @param string $path Contains the XQuery
	 * @return string Priority value (from 0.1 to 1.0) of current file
	 */
	public function getPriority($path = '/')
	{
		return $this->getValueFromNode($path, '@priority');
	}

	public function getParentPath($path = '/')
	{
		$parentPage = explode('/', $path);
		array_pop($parentPage);
		return implode('/', $parentPage);
	}

	/**
	 * Returns the date of last modification of a file.
	 *
	 * @param string $path Contains the XQuery
	 * @return string Returns the value of lastModified attribute
	 */
	public function getLastModified($path = '/')
	{
		$time = $this->getValueFromNode($path, '@lastModified');
		if(empty($time))
			$time = $this->getValueFromNode($path, '@created');
		if($time == '*')
			return date('c', filemtime($this->source)); // Get last modification from filesystem.
		else return $time;
	}

	/**
	 * Returns the name of the author of a file.
	 *
	 * @param string $path Contains the XQuery
	 * @return string Returns the name of the author
	 */
	public function getAuthor($path = '/')
	{
		return $this->getValueFromNode($path, '@author');
	}

	/**
	 * Returns a value from a path in the {@see $dom}.
	 *
	 * @param string $path Contains the XQuery
	 * @param string $sub
	 * @param boolean $lazy
	 * @return string Returns the name of the author
	 */
	public function getValueFromNode($node, $sub, $lazy = false)
	{
		if($x = $this->xpath->query($this->preparePath($node).($lazy ? '/':'').$sub)->item(0))
			return $x->nodeValue;
	}

	public function contextQuery(DomNode $node, $query)
	{
		return $this->xpath->query($query, $node);
	}

	/**
	 * Returns the language if a file.
	 *
	 * @param string $path Contains the XQuery
	 * @return string Returns the language
	 */
	public function getPageTranslation($path = '/')
	{
		$dom = new DomDocument();
		if($node = $this->xpath->query($this->preparePath($path).'mocovi:content')->item(0))
			$dom->appendChild($dom->importNode($node, true));
		return $dom;
	}

	/**
	 * Returns all defined translation XML files based in the header of the filesystem.
	 *
	 * @access public
	 * @return array All translation files.
	 */
	public function getFilesystemTranslations()
	{
		$array = array();
		if($nodes = $this->xpath->query('/mocovi:xmlfs/mocovi:translations/mocovi:file'))
			foreach($nodes as $node)
				if(is_readable($node->nodeValue))
				{
					$dom = new DomDocument();
					$dom->load($node->nodeValue);
					$array[] = $dom;
				}
		return $array;
	}

	/**
	 * Returns the default language set in the filesystem.
	 *
	 * @access public
	 * @return string The default language.
	 */
	public function getDefaultLanguage()
	{
		if($node = $this->xpath->query('/mocovi:xmlfs/@language')->item(0))
			return $node->nodeValue;
		return self::DEFAULT_LANGUAGE;
	}

	/**
	 * Returns the default theme set in the filesystem.
	 *
	 * @access public
	 * @return string The default theme.
	 */
	public function getDefaultTheme()
	{
		if($node = $this->xpath->query('/mocovi:xmlfs/@theme')->item(0))
			return $node->nodeValue;
		return self::DEFAULT_THEME;
	}

	/**
	 * Returns the default media set in the filesystem.
	 *
	 * @access public
	 * @return string The default media.
	 */
	public function getDefaultMedia()
	{
		if(ClientDetermination::is('mobile') && $this->mediaExists('mobi'))
			return 'mobi';
		if($node = $this->xpath->query('/mocovi:xmlfs/@media')->item(0))
			return $node->nodeValue;
		return self::DEFAULT_MEDIA;
	}

	/**
	 * Returns the whole DOM of the filesystem.
	 *
	 * @access public
	 * @return DomDocument The parsed filesystem.
	 */
	public function getDOM()
	{
		return $this->dom;
	}

	public function getPath(DomElement $node)
	{
		$pre = (isset($node->parentNode) && get_class($node->parentNode) === 'DOMElement' ? $this->getPath($node->parentNode) : '');
		return $pre.($pre ? '/' : '').$node->getAttributeNS(Mocovi::$namespace, 'name');
	}

	/**
	 * Returns wether the query is a file or not.
	 *
	 * @access public
	 * @return boolean Query is file or not.
	 */
	public function isFile($path)
	{
		try
		{
			$return = is_object
			(
				$this->xpath->query
				(
					preg_replace
					( '#\/$#'
					, ''
					, $this->preparePath($path)
					)
				)->item(0)
			);
		}
		catch(Exception $e)
		{
			return false;
		}
		return $return;
	}

	/**
	 * Prepares a path for XQuery.
	 *
	 * @param string $path Contains the normal path
	 * @return string Returns the XQuery
	 */
	public function preparePath($path)
	{
		return '/mocovi:xmlfs'.preg_replace('#([A-z0-9]+)\/?#', 'mocovi:file[@name="\\1"]/', $path);
	}

	public function mediaExists($media)
	{
		$theme = $this->getTheme();
		if(is_file($GLOBALS['userViews'].$theme.'/'.$media.'.xsl'))				// Theme specific template
			return true;
		if(is_file($GLOBALS['userViews'].$media.'.xsl'))							// Domain template
			return true;
		if(is_file($GLOBALS['commonViews'].$theme.'/'.$media.'.xsl'))	// Theme specific global template
			return true;
		if(is_file($GLOBALS['commonViews'].$media.'.xsl'))						// Global template
			return true;
		return false;
	}

	
	/**
	 * Create a "redirection" model XML.
	 *
	 * It only contains the information that this page has been moved permanently.
	 * This method is only called when the user has explicit denied the redirection.
	 * 
	 *
	 * @access protected
	 * @return DomDocument Model XML.
	 * @param String URI to the redirected file.
	 */
	protected function createRedirectModel($redirect)
	{
		$model = $this->dom->createElement('root');
		$model->appendChild($root		= $this->dom->createElement('root'));
		$root->appendChild($content			= $this->dom->createElement('content')); // @todo replace with article
		$content->appendChild($headline		= $this->dom->createElement('headline'));
		foreach(translator::translateToken('Redirection')->childNodes as $value)
			$headline->appendChild($this->dom->importNode($value, true));
		$headline->appendChild(new DOMText(' '));
		$content->appendChild($paragraph	= $this->dom->createElement('paragraph'));
		$paragraph->appendChild($text		= $this->dom->createElement('text'));
		foreach(translator::translateToken('SiteMoved')->childNodes as $value)
			$text->appendChild($this->dom->importNode($value, true));
		$text->appendChild(new DOMText(' '));
		$text->setAttribute('flow'			, 'yes');
		$paragraph->appendChild($anchor		= $this->dom->createElement('anchor'));
		$anchor->setAttribute('href'		, $GLOBALS['basepath'].$GLOBALS['linkprefix'].$redirect.'.'.$GLOBALS['media']); // Funktioniert nur bei einer einzigen Seite auf dem Server (/pages muss dynamisch eingebaut werden)
		foreach(translator::translateToken($this->getAlias($redirect))->childNodes as $value)
			$anchor->appendChild($this->dom->importNode($value, true));
		return $model;
	}
}
Return current item: Mocovie web framework