<?php
// +----------------------------------------------------------------------+
// | popoon                                                               |
// +----------------------------------------------------------------------+
// | Copyright (c) 2001-2006 Bitflux GmbH                                 |
// +----------------------------------------------------------------------+
// | Licensed under the Apache License, Version 2.0 (the "License");      |
// | you may not use this file except in compliance with the License.     |
// | You may obtain a copy of the License at                              |
// | http://www.apache.org/licenses/LICENSE-2.0                           |
// | Unless required by applicable law or agreed to in writing, software  |
// | distributed under the License is distributed on an "AS IS" BASIS,    |
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
// | implied. See the License for the specific language governing         |
// | permissions and limitations under the License.                       |
// +----------------------------------------------------------------------+
// | Author: Christian Stocker <chregu@bitflux.ch>                        |
// +----------------------------------------------------------------------+
//
// $Id: sitemap.php 10871 2008-10-15 14:47:15Z bitflux $


/**
 * Class for doing the sitemap parsing stuff
 *
 * @author   Christian Stocker <chregu@bitflux.ch>
 * @version  $Id: sitemap.php 10871 2008-10-15 14:47:15Z bitflux $
 * @package  popoon
 */

class popoon_sitemap {

    private $maps = array();
    public $file = null;
    public $uri = "";
    public $xml = null;
    public $rootFile = null;

    /**
     * Contains the header, which will be output just before serializing
     *
     * @var array
     */
    public $header = array();

    /**
     * HTTP response code which is sent with the first header
     *
     * @var array
     */
    private $responseCode = NULL;

    /**
     * The contenttype
     *
     * should be set within components (resp. serializers) with
     * $this->sitemap->setContentType($type);
     *
     * @var string contenttype
     */

    private $contentType = "text/html";
    /**
     * The Directory, where the cached sitemaps should be saved
     *
     * @var string
     */
    public $cacheDir = "./tmp/";

    /**
     * Contains instance of ComponentCache or false
     *
     * @var mixed
     * @see enableCaching(), disableCaching()
     */
    private $componentCache = false;

    public $outputCache = false;
    /**
     * The directory, where the xslt documents for
     * sitemap 2 php translation are.
     *
     * @var string
     */
    private $sm2php_xsl_dir = "sitemap/";

    /**
     * The XSL File for transforming sitemap.xsl to a cached php file
     *
     * You shouldn't have to change it.
     *
     * @var string
     */
    private $sm2php_xsl = "sitemap2php.xsl";

    private $sm2phpincludes_xsl = "sitemap2phpincludes.xsl";

    public $options;

    /**
     * Construtor
     *
     *  Almost everything happens here. The cached sitemap is generated here, if it
     *   doesn't exist or if it's older
     *  Then this sitemap is included and the code in it is run
     *
     *  @param string $sitemapFile the location of sitemap.xml, can be relativ
     *  @param string $uri the uri of the call, optional (takes _SERVER["REQUEST_URI"] then)
     *  @param array $options options, to be defined
     *  @access public
     *  @return bool
     */
    function __construct($sitemapFile, $uri = null, popoon_classes_config $options = NULL, $maps = NULL) {
        if (! $this->rootFile) {
            $this->rootFile = $sitemapFile;
        }
        //replace class-properties by values in the options-array()
        if ($maps) {
            $this->maps = $maps;
        }
        $this->options = $options;

        //FIXME use new config object class...
        if (! isset($options['sm2php_xsl_dir']) && isset($options['sm2php_xsl'])) {
            $options['sm2php_xsl_dir'] = dirname($options['sm2php_xsl']);
            $options['sm2php_xsl'] = basename($options['sm2php_xsl']);
        }

        foreach ($options as $key => $value) {
            if (isset($this->$key)) {
                $this->$key = $value;
            }
        }
        $this->file = $sitemapFile;

        if ($uri === null) {
            $this->uri = $_SERVER["REQUEST_URI"];
        } else {
            $this->uri = $uri;
        }

        //generate paths and ids
        $sitemapRealPath = realpath($sitemapFile);
        if (! $sitemapRealPath) {
            return popoon::raiseError("Sitemap $sitemapFile does not exist", POPOON_ERROR_FATAL);
        }
        $sitemapId = $this->generateSitemapID($sitemapRealPath);
        /*		if (substr($this->cacheDir,0,1) != "/") {
            $this->cacheDir = BX_PROJECT_DIR."/".$this->cacheDir;
        }*/
        $sitemapCachedFile = $this->cacheDir . $sitemapId;
        //check if sitemapCache does exists and if it's older than the sitemap.xml
        if ((! (file_exists($sitemapCachedFile) && filemtime($sitemapCachedFile) >= filemtime($sitemapRealPath)))) {
            //if it is, make new sitemapCached file
            $err = $this->sitemap2php($sitemapRealPath, $sitemapCachedFile);
        }

        $pipelineHit = $this->runSitemap($sitemapCachedFile);
        return $pipelineHit;

    }

    /**
     * Runs the cached Sitemap
     *
     * @param string $sitemapCachedFile location of the cached file
     * @return mixed true on success, pear error on error
     */
    function runSitemap($sitemapCachedFile) {
        //include the sitemap file
        $pipelineHit = false;
        include ($sitemapCachedFile);
        $this->pipelineHit = $pipelineHit;
        return $pipelineHit;
    }

    public function getOptions($clone = false) {
        return clone $this->options;
    }

    /**
     * Sets a header, which is output just before the serializer
     *
     * @param string $name name of the header
     * @param string $value value of the header
     */
    function setHeader($name, $value = null) {
        if ($value) {
            $this->header[$name] = $value;
        } else {
            if (isset($this->header[$name])) {
                unset($this->header[$name]);
            }
        }
    }

    /**
     * Sets multiple headers which are merged into the existing ones.
     *
     * @param array $value headers to be set
     */
    function setHeaders($headers) {
        $this->header = array_merge($this->header, $headers);
    }

    function setUserData($name, $value) {
        $this->header['_' . $name] = $value;
    }

    function getUserData($name) {
        return $this->header['_' . $name];
    }

    function setHeaderIfNotExists($name, $value) {
        if (! isset($this->header[$name])) {
            $this->setHeader($name, $value);
        }
    }

    /**
     * Sets a HTTP response code, which is sent with the first header
     *
     * @param int $value response code value
     */
    function setResponseCode($value) {
        $this->responseCode = $value;
    }

    /**
     * Sets a header, and directly outputs it
     *
     * Very useful in DomStart of serializers, as the printHeader()
     *  function is called before...
     *
     * @param string $name name of the header
     * @param string $value value of the header
     */
    function setHeaderAndPrint($name, $value) {
        $this->setHeader($name, $value);
        header("$name: $value");
    }

    /**
     * Sets the last modified time
     *
     * @param int $time unixtime last modified
     */
    function setLastModified($time) {
        if ($time > 0) {
            $this->setHeader("Last-Modified", gmdate("r", $time));
        }
    }

    /**
     * Sets the content type of the document
     *
     * @param string $type content type
     */
    function setContentType($type) {
        $this->contentType = $type;
    }

    /**
     * If noCache is set, disables all http caching
     * headers according to http://dclp-faq.de/q/q-http-caching.html
     */
    function setCacheHeaders($noCache, $expireTime = 10) {

        if ($noCache || $expireTime === 0) {
            $date = gmdate("D, d M Y H:i:s", time() - 10);
            $this->setHeader("Expires", $date . " GMT");
            $this->setHeaderIfNotExists("Last-Modified", $date . " GMT");
            $this->setHeader("Pragma", "no-cache");
            $this->setHeader("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
        } else {
            if (! $expireTime) {
                $expireTime = 10;
            }
            //My Apache 2 sends max-age=10800, which is insanely high.. change that
            // here to 10 seconds (at least, we have something then, even if not that high)


            if (! empty($_SERVER['HTTP_USER_AGENT']) && ! empty($this->header['Last-Modified']) && strpos($_SERVER['HTTP_USER_AGENT'], "bot") > 0) {
                $maxTime = 24 * 14 * 3600;
                $t = strtotime($this->header['Last-Modified']);
                $expireTime = time() - $t;
                if ($expireTime > $maxTime) {
                    $expireTime = $maxTime;
                }
            }

            $this->setHeaderIfNotExists("Expires", gmdate("r", time() + $expireTime));
            $this->setHeaderIfNotExists("Cache-Control", "public, max-age=$expireTime");
            $this->setHeader("Pragma");

        }

    }

    /**
     * Prints all the header in $this->header
     *
     * this function is called from components/serializer.php in the constructor
     */

    function printHeader() {
        if (! headers_sent()) {
            $this->setHeader("Content-Type", $this->contentType);

            // flag for an already sent response header
            $responseCodeSent = FALSE;

            foreach ($this->header as $name => $value) {
                if (substr($name, 0, 1) != "_") {

                    // only send response code with first header
                    if ($responseCodeSent) {
                        header("$name: $value");
                    } else {
                        header("$name: $value", TRUE, $this->responseCode);
                        $responseCodeSent = TRUE;
                    }
                }
            }
        }

    }

    /**
     * generates a unique ID out of a string (path+filename)
     *
     * This method is used for generating a unique ID for every sitemap
     *  which has to be compiled. It just takes the realpath as input,
     *  and returns a unique ID for it.
     * In this case it replaces every DIRECTORY_SEPERATOR with _ for better
     *  debugging. Theoretically one could use md5 as well.
     *
     * @param string $realpath Any string, but in this class normally a fullpath+filename
     * @access public
     * @return string ID
     */
    function generateSitemapID($realpath) {
        return str_replace(array(DIRECTORY_SEPARATOR, ":"), "_", $realpath);
    }

    /**
     * Generates the cached sitemap
     *
     * This function generates a php file out of a sitemap.xml with the help
     *  an xsl file (poponn/sitemap/sitemap2xsl.php)
     *
     * Libxslt and Sablotron is supported right now.
     *
     * @param string $sitemapRealPath the absolute location of sitemap.xml
     * @param string $sitemapCachedFile the absolute location of the cached sitemap (php file)
     * @access public
     * @return bool
     */
    function sitemap2php($sitemapRealPath, $sitemapCachedFile) {
        //file_exists and is writable should be the normal case...
        if (! is_writable($sitemapCachedFile)) {
            if (! realpath(dirname($sitemapCachedFile))) {
                if (! mkdir(dirname($sitemapCachedFile) . "/")) {
                    return popoon::raiseError("The cache directory " . dirname($sitemapCachedFile) . " does not exist", POPOON_ERROR_FATAL);
                }
            }
            if (! is_writable(dirname($sitemapCachedFile))) {
                return popoon::raiseError("The cache directory  " . realpath(dirname($sitemapCachedFile)) . " is not writable", POPOON_ERROR_FATAL, __FILE__, __LINE__);
            } else
                if ((file_exists($sitemapCachedFile) && ! is_writable($sitemapCachedFile))) {
                    return popoon::raiseError("The cache file " . realpath($sitemapCachedFile) . " is not writable", POPOON_ERROR_FATAL);
                }
        }

        //check if we have domxml/xslt


        $xslDom = new DomDocument();
        $xslDom->resolveExternals = true;
        $xslDom->substituteEntities = true;
        $xslDom->load($this->sm2php_xsl_dir . "/" . $this->sm2php_xsl);
        if (! class_exists("XsltProcessor")) {
            return popoon::raiseError("Popoon doesn't run without XSLT support in PHP.", POPOON_ERROR_FATAL, __FILE__, __LINE__);
        }
        $xsl = new XsltProcessor();
        $xsl->importStylesheet($xslDom);
        $xsl->registerPhpFunctions();
        $xslincludesDom = new DomDocument();
        $xslincludesDom->load($this->sm2php_xsl_dir . "/" . $this->sm2phpincludes_xsl);
        $xslincludes = new XsltProcessor();
        $xslincludes->importStylesheet($xslincludesDom);
        $sm = new DomDocument();
        $sm->resolveExternals = true;
        $sm->substituteEntities = true;
        if (! $sm->load($sitemapRealPath)) {
            if (! file_exists($sitemapRealPath)) {
                throw new PopoonFileNotFoundException($sitemapRealPath);
            } else {
                throw new PopoonXMLParseErrorException("Could not load $sitemapRealPath");
            }
        }
        $xsl->setParameter("", "popoonDir", dirname(__FILE__));

        $result = $xslincludes->transformToDoc($sm);
        $result = $xsl->transformToUri($result, $sitemapCachedFile);

        return True;
    }

    function convertXML($object, &$xml) {
        if ($object->XmlFormat == "DomDocument") {
            self::var2XMLObject($xml);
        } elseif ($object->XmlFormat == "XmlString") {
            self::var2XMLString($xml);
        }
        return True;
    }

    /**
     * Converts it's parameter into a DomDocument object
     *
     * @param  mixed  xmldoc        can either be a XML String or a DomDocument object
     * @return bool
     * @access private
     */
    static public function var2XMLObject(&$xmldoc) {
        if (is_string($xmldoc)) {

            $xmldom = new DomDocument();

            $xmldom->loadXML($xmldoc);
            $xmldoc = $xmldom;

        }
        if (strtolower(get_class($xmldoc)) != "domdocument") {
            return popoon::raiseError('First parameter to var2XMLObject() is neither a XML String nor a XML DomDocument object. It is: ' . var_export($xmldoc, true), POPOON_ERROR_FATAL);
        }
        return True;
    }

    function redirectTo($uri) {
        header("Location: " . popoon_sitemap::translateScheme($uri));
        exit();
    }

    /**
     * Converts it's parameter into a XML String
     *
     * @param  mixed  xmldoc        can either be a XML String or a DomDocument object
     * @return bool
     * @access private
     */
    static public function var2XMLString(&$xmldoc) {
        if (strtolower(get_class($xmldoc)) == "domdocument") {
            $xmldoc = $xmldoc->saveXML();
        }
        if (! is_string($xmldoc)) {
            return popoon::raiseError('First parameter to var2XMLString() is neither a XML String nor a XML DomDocument object. It is: ' . var_export($xmldoc, true), POPOON_ERROR_FATAL);
        } else {
            return True;
        }
    }

    /* in the array doNotTranslate we can give some values, which should not be translated, as for example http...*/
    function translateScheme($value, $doNotTranslate = array(), $onSitemapGeneration = false) {
        // don't do anything, if we don't have any scheme stuff in the $value;
        // strpos should be rather fast, i assume.


        if (is_object($value) || is_array($value) || strpos($value, ":/") === false && strpos($value, "{") === false) {

            return $value;
        }

        $scheme = popoon_sitemap::getSchemeParts($value);

        //checks if value  ends with } and starts with { with no { after the first position
        // then we don't need this fairly complicated preg from below and can substitute also arrays and alike


        if (! $onSitemapGeneration && substr($scheme["value"], - 1, 1) == "}" && strrpos($scheme["value"], "{") === 0) {
            $scheme["value"] = substr($scheme["value"], 1, - 1);

            $scheme["value"] = @$this->maps[substr_count($scheme["value"], '../')][str_replace("../", "", $scheme["value"])];
        } else
            if ($onSitemapGeneration) {

                $scheme["value"] = preg_replace("#\{([\./]*([^}]+))\}#e", "popoon_sitemap::translateSchemeSubPartsOnSitemapGeneration('$1','$2')", $scheme["value"]);
            } else {
                $scheme["value"] = preg_replace("#\{([\./]*([^}]+))\}#e", "\$this->translateSchemeSubParts('$1','$2')", $scheme["value"]);
            }
        if (in_array($scheme["scheme"], $doNotTranslate)) {
            return $value;
        } else
            if ($scheme["scheme"] != "default") {
                if (! @include_once ("popoon/components/schemes/" . $scheme["scheme"] . ".php")) {
                    return $value;
                }
                if ($onSitemapGeneration) {
                    if (function_exists("scheme_" . $scheme["scheme"] . "_onSitemapGeneration")) {
                        return call_user_func("scheme_" . $scheme["scheme"] . "_onSitemapGeneration", $scheme["value"]);
                    } else {
                        return $value;
                    }
                } else {
                    return call_user_func("scheme_" . $scheme["scheme"], $scheme["value"], $this);
                }
            } else {
                return $scheme["value"];
            }
    }

    function translateSchemeSubParts($value, $value2) {
        if (strpos($value, ":/") === false) {
            return $this->maps[substr_count($value, '../')][$value2];
        } else {
            return popoon_sitemap::translateScheme($value);
        }
    }

    static function translateSchemeSubPartsOnSitemapGeneration($value, $value2) {
        if (strpos($value, ":/") === false) {
            return '{' . $value . '}';
        } else {
            $newVal = popoon_sitemap::translateScheme($value, array(), true);
            if ($newVal == $value) {
                return '{' . $value . '}';
            } else {
                return $newVal;
            }

        }
    }

    static function getSchemeParts($value) {
        $scheme = array();
        if (preg_match("#^'(.*)'$#", $value, $match)) {
            $scheme["scheme"] = "default";
            $scheme["value"] = $match[1];
        } elseif (preg_match("#^([_a-zA-Z0-9]+)://(.*)#", $value, $match)) {
            $scheme["scheme"] = $match[1];
            $scheme["value"] = $match[2];
        } else {
            $scheme["scheme"] = "default";
            $scheme["value"] = $value;
        }
        return $scheme;
    }

    function addMap($map) {
        array_unshift($this->maps, $map);
    }

    function removeMap() {
        array_shift($this->maps);
    }

    function setGlobalOptions($name, $data) {
        $GLOBALS["_POPOON_globalContainer"]->options[$name] = $data;
    }

    function setGlobalOptionsAll($data) {
        $GLOBALS["_POPOON_globalContainer"]->options = $data;
    }

    function getGlobalOptions($name) {
        return $GLOBALS["_POPOON_globalContainer"]->options[$name];
    }

    function getGlobalOptionsAll() {
        if (isset($GLOBALS["_POPOON_globalContainer"]->options)) {
            return $GLOBALS["_POPOON_globalContainer"]->options;
        } else {
            return null;
        }
    }

    /**
     * Mounts a second sitemap
     *
     * Not a very elegant solution, should be rewritten some day
     *
     */
    private function _mount($attribs) {
        $file = popoon_sitemap::translateScheme($attribs["src"]);
        $old_uri = $this->uri;

        if (isset($attribs["uri-prefix"])) {
            $prefix = popoon_sitemap::translateScheme($attribs["uri-prefix"]);
            if ($prefix) {
                $this->uri = preg_replace("#^/*$prefix/*#", "", $this->uri);
            }

        }
        // I hope, this doesn't have too many sideeffects
        $pipelineHit = $this->__construct($file, $this->uri, $this->options, $this->maps);

        $this->uri = $old_uri;
        return $pipelineHit;
    }

    private function _scheme($attribs) {
        if (! isset($GLOBALS["_POPOON_globalContainer"])) {
            $GLOBALS["_POPOON_globalContainer"] = new stdClass();
        }
        if (! isset($GLOBALS["_POPOON_globalContainer"]->schemes)) {

            $GLOBALS["_POPOON_globalContainer"]->schemes = array();
        }
        $GLOBALS["_POPOON_globalContainer"]->schemes[$attribs["name"]] = array();
        if (isset($attribs["subname"])) {
            $GLOBALS["_POPOON_globalContainer"]->schemes[$attribs["name"]][$attribs["subname"]] = array();
            foreach ($attribs as $value => $key) {
                $GLOBALS["_POPOON_globalContainer"]->schemes[$attribs["name"]][$attribs["subname"]][$value] = popoon_sitemap::translateScheme($key);
            }
        } else {
            foreach ($attribs as $value => $key) {
                $GLOBALS["_POPOON_globalContainer"]->schemes[$attribs["name"]][$value] = popoon_sitemap::translateScheme($key);
            }
        }

    }

    /**
     * Starts component caching
     *
     * @param  array  Attributes of current pipeline
     * @see    disableCaching(), $componentCache
     */
    function enableCaching($pipelineAttribs) {
        include ('popoon/components/cache.php');
        $this->componentCache = new ComponentCache($pipelineAttribs, $this);
    }

    /**
     * Disables component caching
     *
     * Reset $this->componentCache
     *
     * @see    enableCaching(), $componentCache
     */
    function disableCaching() {
        $this->componentCache = false;
    }

    function disableOutputCaching() {
        $this->options->disableOutputCaching();
    }

}

function sitemap_formatValues($value) {
    $value = str_replace("'", "\'", $value);
    //replace constant() with content
    preg_match_all("#constant\(([^\)]+)\)#", $value, $matches);
    $c = count($matches[0]);
    if ($c > 0) {
        for ($i = 0; $i < $c; $i ++) {
            if (defined($matches[1][$i])) {
                $value = str_replace($matches[0][$i], "'." . $matches[1][$i] . ".'", $value);
            }
        }
    }
    //check if there are any schemes... else we can return here
    if (strpos($value, ":/") === false && strpos($value, "{") === false) {
        return sitemap_fixValue($value);
    }
    // translate translatabe scheme
    $value = popoon_sitemap::translateScheme($value, array(), true);

    return sitemap_fixValue($value);
}

function sitemap_fixValue($value) {
    $value = "'" . $value . "'";
    return preg_replace(array("#\.''$#", "#^''\.#", "#\.''\.#"), "", $value);
}
