<?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;
}
}