<?php
/**
* PhritzTpl.class.php
*
* Base class for the front-end template object
* Variables are assigned via the assign() or append() methods
* Users can also assign RSS feeds with the assign/append_rss() methods
* Prefilters, postfilters, resouce functions can all be added with the appropriate
* methods
*
* LICENSE: This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
* @package Phritz
* @link http://phritz.fritz-works.com
* @author Michael Collado <hide@address.com>
* @copyright 2006
* @version 0.0.1a
**/
require_once PHRITZ_ROOT.'/kernel/PhritzView.class.php';
Phritz::import('phritz.XMLBuilder');
Phritz::import('phritz.XSL_Wrapper');
//Error code definitions here
define('XSL_NOT_INITED', "XSLT API has not been installed");
define('XML_NOT_INITED', "DOM API has not been installed");
define('CALLBACK_FUNC_ERROR', 'An error occurred loading a plugin function');
define('FUNC_NOT_EXIST', 'The specified function can not be called');
define('TPL_UNAVAILABLE', 'The requested template file could not be found');
define('PHP_VERSION_ERROR', 'PHP version 4.3 or higher is required to use PHRITZ_TPL. Please update your version of php');
define('XSL_COMPILE_FAILED', 'The XSL processor was unable to process the file ');
define('XSL_IMPORT_FAILED', 'The XSL processor was unable to load the template file<br />Please check the template file for correct synatx');
define('DIR_SEP', DIRECTORY_SEPARATOR);
class PhritzTpl extends PhritzObject implements PhritzViewInterface {
private $xsl_wrapper;
private $tree;
private $params;
private $changed;
private $resources = array();
private $resource_functions = array();
private $output_xml = true;
private $secure_dir = array(PHRITZ_ROOT,'prefilters','postfilters','templates');
private $prefilter_dir = 'view/prefilters';
private $postfilter_dir = 'view/postfilters';
private $tpl_dir = 'templates';
private $base_url;
private $allow_caching = false;
private $cache_dir = 'view/cache';
private $cache_id = '';
private $cache_time;
private $cache_life = 3600;
function __construct() {
if (class_exists('XSLTProcessor')) {
$this->xsl_wrapper =& new XSL_Wrapper();
} else {
throw new PhritzDependancyException('XSLT API is required to use this template system', 'XSL');
}
$this->setParam('SERVER', $_SERVER);
$this->setParam('ENV', $_ENV);
$this->registerDefault();
$this->cache_time = time();
$this->changed = false;
return true;
}
/**
* allow_xml_output()
*
* Use to determine whether the tpl object will output strict xml or processed via xslt
*
* @param Boolean $bool true to output xml, false to output html
* @return void
*/
public function allow_xml_output($bool = true) {
$this->output_xml = $bool;
}
/**
* setThemePath()
*
* change the default template directory
*
* @param string $dir name of the directory to use
* @return void
*/
public function setThemePath($dir) {
if (is_dir(PHRITZ_ROOT.DIR_SEP.$dir)) {
$this->tpl_dir = $dir;
} else {
return false;
}
}
/**
* getThemePath()
*
* Returns the current theme path
*/
public function getThemePath() {
return $this->tpl_dir;
}
/**
* setCacheID()
*
* set the cache prefix for all cache files to be stored or called during this
* session
*/
public function setCacheID($id) {
$this->cache_id = $id;
}
/**
* assign()
*
* assign values to template variables. assignments can be multidimensional arrays or simple
* key, value parameters. See the demo for examples.
*
* @param mixed $name string name or associative array with tpl assignments
* @param mixed $value string value or associative array with tpl assignments
*/
public function assign($name, $value=null) {
if (is_array($name)) { //$name is array, ignore $value
foreach ($name AS $var=>$val) {
$this->vars[$var] = $val;
}
} else {
$this->vars[$name] = $value;
}
$this->changed = true;
}
/**
* append()
*
* appends values to tpl variables that are already set.
*
* <code>
* $forum = array('NAME'=>'MyForum', 'ID'=>3);
* $append = array('FORUMS'=>array('FORUM'=>$forum));
* $tpl->append($append);
* </code>
* <b>OR</b>
* <code>
* foreach ($forums AS $forum) {
* $array = array('NAME'=>$forum->Name, 'ID'=>$forum->ID);
* $tpl->append('FORUMS', array('FORUM'=>$array));
* }
* </code>
* will result in a dom tree that looks like:
* <forums>
* <forum>
* <name>Forum's name</name>
* <id>Forum ID</id>
* </forum>
* </forums>
*/
public function append($name, $value = null, $merge = false) {
if (is_array($name)) { //name can be array of NAME=>VAR
foreach ($name AS $key=>$var) {
if (!@is_array($this->vars[$key])) {
settype($this->vars[$key], 'array');
}
if ($merge && is_array($var)) {
foreach ($var AS $n_key=>$n_var) {
$this->vars[$key][$n_key] = $n_var;
}
} else {
$this->vars[$key][] = $var;
}
}
} else {
if (!isset($this->vars[$name])) {
$this->vars[$name] = array();
}
if (!is_array($this->vars[$name])) {
settype($this->vars[$name], 'array');
}
if ($merge && is_array($value)) {
foreach ($value AS $var=>$val) {
$this->vars[$name][$var] = $val;
}
} else {
$this->vars[$name][] = $value;
}
}
$this->changed = true;
}
/**
* assign_rss()
*
* Assign RSS data from a given feed to the template XML tree.
* As long as Sockets are enabled you can pass any url to the method and
* the feed will be opened, the xml declaration tag will be stripped and the
* feed will be appended starting and ending with the <rss> tag
* Exceptionally useful since RSS feeds are already in XML format, you
* can parse the RSS feed in the same template files you use for anything else.
*
* @access public
* @param String $name The name of the template variable
* @param String $page The page where the RSS feed can be located
* @return Boolean
*/
public function assign_rss($name, $page) {
$info = file_get_contents($page);
if (!$info) {
return false;
} else {
preg_match("|<rss .+</rss>|isx", $info, $feed);
$this->assign($name, $feed[0]);
return true;
}
}
/**
* append_rss()
*
* Same as assign_rss() except that it appends the RSS feed
* to an already existing template variable.
*
* @access public
* @param String $name The name of the template variable
* @param String $page The page where the RSS feed can be located
* @return Boolean
*/
public function append_rss($name, $page) {
if (is_array($page)) {
$element = $page[0];
$url = $page[1];
} else {
$element = 'feed';
$url = $page;
}
$info = file_get_contents($url);
if (!$info) {
return false;
} else {
preg_match("|<rss .+</rss>|isx", $info, $feed);
$this->append($name, array($element=>$feed[0]));
return true;
}
}
/**
* clear_assign()
*
* deletes a portion of the dom tree.
* deletes all of the child nodes of the $key element
*
* @param string $key The name of the tpl var to be deleted.
* @return void
*/
public function clear_assign($key) {
if (is_array($key)) {
foreach ($key AS $var) {
unset($this->vars[$var]);
}
} else {
unset($this->vars[$key]);
}
$this->changed = true;
}
public function setParam($name, $value) {
if (is_array($value)) {
foreach ($value AS $k=>$val) {
$varname = $name.'.'.$k;
if (is_array($value[$k])) {
$this->setParam($varname, $value[$k]);
} else {
$this->xsl_wrapper->setParameter($varname, $val);
}
}
} else {
$this->xsl_wrapper->setParameter($name, $value);
}
}
/**
* register_php_function()
*
* Assigns a php function that can be used when parsing xslt stylesheets
*
* @param string $function The name of the function to use
* @return bool true on success, false on failure (php4 always returns falses)
*/
public function register_php_function($function) {
return $this->xsl_wrapper->registerFunction($function);
}
/**
* unregister_php_function()
*
* Unregister a registered PHP function with the XSLT processor
* Again, only available for PHP5
*
* @access public
* @param String $func_name The name of the function to be unregistered
* @return void
*/
public function unregister_php_function($func_name) {
$this->xsl_wrapper->unregisterFunction($func_name);
}
/**
* register_prefilter()
*
* Registers a prefilter to be used on the template files before they
* are processed by the XSLT processor
* Usage:
* <code>
* $tpl->register_prefilter('function_name', array([$param1, [$param2, [$param3...]]]));
* </code>
* OR
* <code>
* $tpl->register_prefilter(Array($obj, 'method_name'), array([$param1, [$param2, [$param3]]]));
* </code>
*
* @access public
* @param Mixed $function The name of the function or an array($object, "Function_name")
* @param Array $params Any parameters that should be passed to the function when called
* @returns void
*/
public function register_prefilter($function, $params = null) {
$name = (is_array($function)) ? $function[1]: $function;
$this->resource_functions['pre'][$name]['function'] = $function;
$this->resource_functions['pre'][$name]['params'] = $params;
}
/**
* unregister_prefilter()
*
* Unregisters a registered prefilter
*
* @access public
* @param String $func_name The name of the registered function
* @return void
*/
public function unregister_prefilter($func_name) {
unset($this->resource_functions['pre'][$func_name]);
}
/**
* register_postfilter()
*
* Register a filter function to be used after the XSLT stylesheet has been processed
* Usage instructions are identical to register_prefilter()
* @see PhritzTPL::register_prefilter()
* @access public
* @param Mixed $function String name of function or array(Object $object, String "Function_name")
* @param Array $params Any parameters to be passed to the function when called
* @return void
*/
public function register_postfilter($function, $params=null) {
$name = is_array($function) ? $function[1] : $function;
$this->resource_functions['post'][$name]['function'] = $function;
$this->resource_function['post'][$name]['params'] = $params;
}
/**
* register_resource()
*
* Register a resource function to be used to retrieve templates that are not located in a file
* (i.e. retrieve a template stored in a database)
*
* @access public
* @param String $type A name given to the resource type (i.e. db for database)
* @param Mixed $function Either the function name or Array(Object $obj, String $Method);
* @param Array $params Any parameters that should be passed to the function when called
* @return void
*/
public function register_resource($type, $function, $params = null) {
$this->resources[] = $type;
$this->resource_functions[$type]['function'] = $function;
$this->resource_functions[$type]['params'] = $params;
}
/**
* unregister_resource()
*
* Unregister a registered resource function
*
* @param String $name The resource name given in register_resource()
* @return void
*/
public function unregister_resource($name) {
if (($ind = array_search($name, $this->resources)) !== false) {
unset($this->resources[$ind]);
}
foreach($this->resource_functions[$name] AS $i=>$function) {
unset($this->resource_functions[$name][$i]);
}
unset($this->resource_functions[$name]);
}
/**
* display()
*
* If a template filename is supplied, this will either display the
* current XML string with an xml-stylesheet processing instruction
* for the browser to process (if xml_output is enabled)
* or the server-side XSLT processor will process the template
* file and display the output.
*
* If no template file is provided, the raw XML string will be
* displayed. This can be useful for AJAX applications where a raw XML
* can be passed to the javascript functions on the client-side
* for processing.
*
* @access public
* @param String The name of the template file to be processed
* @param String Any cache id applied to the given template
* @return void
*/
public function display($tpl = null, $cache_id = null) {
header('Pragma: no-cache');
header('Cache-conrol: no-store');
if ($this->output_xml) {
header('Content-type: text/xml; charset=UTF-8');
if (!empty($tpl)) {
$stylesheet = PHRITZ_URL.'/'.$this->tpl_dir.'/'.$tpl;
} else {
$stylesheet = '';
}
if (!$this->changed) {
echo $this->xsl_wrapper->display($this->tree, $stylesheet);
} else {
echo $this->xsl_wrapper->display(XMLBuilder::compile($this->vars), $stylesheet);
}
} else {
$output = $this->fetch($tpl, $cache_id, true);
echo $output;
}
}
/**
* fetch()
*
* This is the method used to transform the template XML string to a named XSLT resource
* XSLT resources can be passed in one of three ways:
* 1) The name of the XSL template file located in the specified template directory
* 2) The name of a registered resource function (such as a database accessor) in the format
* RESOURCE_NAME:FILE_NAME
* 3) As an XSL string with XSL_STRING: preceding the actual string (i.e. XSL_STRING:<?xml ...)
*
* @returns string The transformed template
*/
public function fetch($tpl_resource, $cache_id = null, $force_compile = false, $display = false) {
if ($cache_id === null) {
$cache_id = $this->cache_id;
}
//to fetch a tpl file from a registered resource, use RESOURCE_NAME:TPL_FILE_NAME
if (preg_match("/(\w+):([\w\d\.\-_]+||<\?xml.+)/", $tpl_resource, $match)) {
$ext_resource = true;
$cache_file = $cache_id.$match[2];
} else {
$ext_resource = false;
$cache_file = $cache_id.$tpl_resource;
}
//if caching on this template
if ($force_compile === false && $this->allow_caching) {
if ($file = $this->getCachedResource($cache_file)) {
return $file;
}
}
if ($ext_resource) {
if ($match[1] == "STRING") { //Enable users to pass an xsl string as resource argument
$tpl_source = $match[2];
} else {
$tpl_source = $this->getFromResoure($match[1], $match[2]);
}
} else {
if (file_exists($tpl_resource)) {
$tpl_source = $this->getFileContents($tpl_resource);
} else {
$tpl_source = $this->getFileContents(PHRITZ_ROOT.DIR_SEP.$this->tpl_dir.DIR_SEP.$tpl_resource);
}
}
$tpl_source = $this->runPreFilters($tpl_source);
if (!$this->changed) {
$compiled = $this->xsl_wrapper->process($tpl_source, $this->tree, $tpl_resource);
} else {
$compiled = $this->xsl_wrapper->process($tpl_source, XMLBuilder::compile($this->vars), $tpl_resource);
}
$compiled = $this->runPostFilters($compiled);
if ($force_compile === false && $this->allow_caching) {
$comp_file = PHRITZ_ROOT.DIR_SEP.$this->cache_dir.DIR_SEP.$cache_file;
$handle = fopen($comp_file, 'w');
if ($handle !== false) {
fwrite($handle, $compiled);
fclose($handle);
}
}
return $compiled;
}
/**
* debug()
*
* Returns the current XML string
*
* @access public
* @return String
*/
public function debug() {
return XMLBuilder::compile($this->vars);
}
/**
* __getFileContents()
*
* Opens a template file and reads the contents into a string
* which will be read and processed by the XSLT processor
*
* @access private
* @returns String
*/
private function getFileContents($filename) {
$source = '';
if (file_exists($filename)) {
$source = file_get_contents($filename);
} else {
throw new PhritzFileException("Could not load template file.", $filename);
}
return $source;
}
/**
* __getFromResource()
*
* Retrieves the template string from a registered resource
* which will be read and processed by the XSLT processor
*
* @access private
* @returns String
*/
private function getFromResource($resource, $file) {
$f_name = $this->resource_functions[$resource]['function'];
$params = (!empty($this->resource_functions[$resource]['params'])) ?
$this->resource_functions[$resource]['params'] : null;
if (!empty($params)) {
if (!is_array($params)) { //make sure $params is an array
settype($params, 'array');
}
//The first two paramaters of any resource function are the Template object & the resource name
array_unshift($params, $this, $file);
} else {
$params = array($this, $file);
}
if (!is_callable($f_name)) {
$this->loadPlugin($f_name);
}
//callback functions should return false if they can not retrieve tpl info
if (($test = call_user_func_array($f_name, $params)) !== false) {
return $test;
} else {
throw new PhritzParseException("Cannot retrieve source template from resource", CALLBACK_ERROR);
}
}
/**
* __getCachedResource()
*
* Opens a cache file for a given template and returns
* the previously processed string
*
* @access private
* @returns String
*/
private function getCachedResource($tpl_name) {
$file_name = PHRITZ_ROOT.DIR_SEP.$this->cache_dir.DIR_SEP.$tpl_name;
if (file_exists($file_name)) {
$age = $this->cache_time - filemtime($file_name);
if ($age < $this->cache_life) {
return file_get_contents($file_name);
} else {
unlink($file_name);
return false;
}
} else {
return false;
}
}
/**
* __runPreFilters()
*
* Processes the XSLT template string using any registered prefilters
*
* @access private
* @param String $text The raw XSLT stylesheet text
* @return String
*/
private function runPreFilters($text) {
if (isset($this->resource_functions['pre']) && !empty($this->resource_functions['pre'])) {
foreach($this->resource_functions['pre'] AS $id=>$array) {
$params = (!empty($array['params'])) ? $this->resource_functions['pre'][$id]['params'] : null;
$name = $this->resource_functions['pre'][$id]['function'];
if (!empty($params)) {
if (!is_array($params)) {
settype($params, 'array');
} //first two parameters of pre filters should be template object and the tpl_source string.
array_unshift($params, $this, $text);
} else {
$params = array($this, $text);
}
if (!is_callable($name)) {
try {
$this->loadPlugin($name);
} catch(PhritzFileException $e) {
trigger_error($e->getMessage(), E_USER_NOTICE);
continue;
}
}
$filtered = call_user_func_array($name, $params);
if ($filtered !== false) { //prefilter functions return false on failure.
$text = $filtered;
}
}
}
return $text;
}
/**
* __runPostFilters()
*
* Process the processed template file using any registered postfilters
*
* @access private
* @param String $text The processed XSLT stylesheet text
* @return String
*/
private function runPostFilters($text) {
if (isset($this->resource_functions['post']) && !empty($this->resource_functions['post'])) {
foreach($this->resource_functions['post'] AS $id=>$array) {
$params = (!empty($array['params'])) ? $this->resource_functions['pre'][$id]['params'] : null;
$name = $this->resource_functions['post'][$id]['function'];
if (!empty($params)) {
if (!is_array($params)) {
settype($params, 'array');
}
//first two parameters of postfilters should be template object and the tpl_source string.
array_unshift($params, $this, $text);
} else {
$params = array($this, $text);
}
if (!is_callable($name)) {
try {
$this->loadPlugin($name);
} catch(PhritzFileException $e) {
trigger_error($e->getMessage(), E_USER_NOTICE);
continue;
}
}
$filtered = call_user_func_array($name, $params);
if ($filtered !== false) { //postfilter functions return false on failure.
$text = $filtered;
}
}
}
return $text;
}
/**
* __loadPlugin()
*
* Searches the plugin directory and any registered secure directories for either a file named
* xsl_plugin.$FUNCTION_NAME.php or, if a class/method pair were passed, it will search for a file
* named $CLASS_NAME.class.php
*
* @access private
* @param Mixed $filter String filtername or Array(String Classname, String Method)
* @return void
*/
private function loadPlugin($filter) {
if (is_array($filter)) {
if (is_string($filter[0])) {
foreach($this->secure_dir AS $dirname) {
if (file_exists(PHRITZ_ROOT.DIR_SEP.$dirname.DIR_SEP.$filter[0].'.class.php')) {
include_once(PHRITZ_ROOT.DIR_SEP.$dirname.DIR_SEP.$filter[0].'.class.php');
return;
}
}
}
throw new PhritzFileException("Could not load callback file", $filter[0]);
} else {
foreach ($this->secure_dir AS $dirname) {
$fname = PHRITZ_ROOT.DIR_SEP.$dirname.'xsl_plugin'.$filter.'.php';
if (file_exists($fname)) {
include_once($fname);
return;
}
}
throw new PhritzFileException("Could not load callback file", $filter);
}
}
/**
* __registerDefault()
*
* Traverses the default plugin directory and registers functions
*
* @access private
* @return void
*/
private function registerDefault() {
$dir = opendir($this->prefilter_dir);
while (($file = readdir($dir)) !== false) {
if (preg_match("/xsl_plugin.([\w\d_\-]+).php/i", $file, $filter)) {
require_once(PHRITZ_ROOT.DIR_SEP.$this->prefilter_dir.DIR_SEP.$file);
$this->register_prefilter($filter[1]);
}
}
$dir = opendir(PHRITZ_ROOT.DIR_SEP.$this->postfilter_dir);
while (($file = readdir(PHRITZ_ROOT.DIR_SEP.$dir)) !== false) {
if (preg_match("/xsl_plugin.([\w\d_\-]+).php/", $file, $filter)) {
include_once PHRITZ_ROOT.DIR_SEP.$this->postfilter_dir.DIR_SEP.$file;
$this->register_postfilter($filter[1]);
}
}
}
}
?>