* @copyright Copyright (c) 2002-2005 * @link http://templation.websaviour.com/ Templation Website * @license http://opensource.org/licenses/gpl-license.php GNU Public License This file is part of Templation. Templation 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. Templation 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 Templation; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * Templation is a robust HTML construction and maintenance tool * based around the idea of cascading repeated elements throughout * a website without resorting to a rigid structure that could get * in the way of designers and developers. Templation consolidates * repeated page elements through the use of simple templating and * inclusion system that is controlled using standard HTML meta tags. * * $Id: Templation.class.php,v 1.5 2005/09/05 20:43:25 dasil003 Exp $ */ require_once "Templation_Widget.class.php"; // Values for mode_vector of the parseTemplate function. define('TEMPLATION_PARSE_DATA', 1); define('TEMPLATION_PARSE_WIDGETS', 2); define('TEMPLATION_PARSE_INCLUDES', 4); // Define error codes starting with critical errors: define("TEMPLATION_OK", 1); define("TEMPLATION_ERROR", -1); define("TEMPLATION_TEMPLATE_NOT_FOUND", -2); define("TEMPLATION_TEMPLATE_UNOPENABLE", -3); define("TEMPLATION_BAD_SETTING", -4); define("TEMPLATION_WIDGET_ERROR", -5); // Warnings usually indicate error conditions which result in incorrect output: define("TEMPLATION_WARNING", -100); define("TEMPLATION_INCLUDE_NOT_FOUND", -101); define("TEMPLATION_INCLUDE_UNOPENABLE", -102); define("TEMPLATION_WIDGET_NOT_FOUND", -103); define("TEMPLATION_WIDGET_UNOPENABLE", -104); define("TEMPLATION_INVALID_PATH", -105); define("TEMPLATION_INVALID_FILE_OBJECT", -106); define("TEMPLATION_WIDGET_WARNING", -107); define("TEMPLATION_FOLDER_UNOPENABLE", -108); define("TEMPLATION_ACTION_NOT_FOUND", -109); // Notices are anomolous conditions which may be benign. define("TEMPLATION_NOTICE", -1000); define("TEMPLATION_NOT_ENOUGH_INCLUDES", -1001); define("TEMPLATION_WIDGET_NOTICE", -1002); class Templation { /** If a critical error occurs, this should be set to true. * @access private * @var bool */ var $invalid = false; /** This is the Templation data repository, it is passed to the * constructor or auto-detected. * @var string */ var $tpl_dir = ''; /*** GLOBAL CONFIG ONLY SETTINGS ***/ var $tpl_dir_local = ''; //An alternate data repository (not including cache), $DOCUMENT_ROOT/_data/ by default. //This would be a site-specific setting except it needs to be set first in order to FIND the //site-specific configuration. var $hosts = array(); //Array of site specific configurations hashed on the host variable. This is more limited //than standard site-specific configuration file, but is useful if you want to make all your //site definitions in one file. /*** SITE SPECIFIC CONFIG SETTINGS ***/ var $root = ''; //HTTP Server Root, may be set in settings.php or derived automagically. var $host = ''; //Hostname (or IP), may be set arbitrarily in config. There should be //one unique host for each unique site (even if multiple domains point to it), //otherwise multiple caches of the same site will be created. var $src_name = 'SCRIPT_NAME'; //The $_SERVER variable that gives the source name relative to document root. var $src_filename = ''; //If set will override src_name and provide a full path to the actual source page. var $caching_on = false; //Caching minimizes processing, but sometimes needs to be turned off for debugging. var $includes_dir = 'includes'; //Name of directory to search for meta file var $meta_name = 'meta.php'; //Filename to parse for meta tags. var $template_mode = 'std'; //The symbol over which alternate versions of the same template //may branch. Source file is not affected, but templates and filters can be. var $widgets = array(); //Associative array of Templation_Widget objects hashed on widget_path to avoid //duplicate widget instances, thus allowing for widgets to contain static values. var $pages = array(); //Array of Templation_File objects. See instantiation in class constructor. /*** ARRAYS OF SEARCH PATHS ***/ var $widget_dirs = array(); var $template_dirs = array(); var $action_dirs = array(); /*** AUXILIARY VARIABLES not used by Templation directly (recommended for widget use) ***/ var $index_filenames = array('index.php','index.html'); //Useful information when it comes to link recognition. /** * Initialize Templation settings and a Templation_File for the requested (or specified) page. * * @param string The location of the Templation data repository. * @param string The path (relative to document root) to the desired file. **/ function Templation($tpl_dir = '', $file_path = '') { /*** SET UP GLOBAL SETTINGS ***/ if($tpl_dir == '') { $p = strrpos(__FILE__,'/'); $tpl_dir = substr(__FILE__,0,$p).'/data'; } $this->tpl_dir = $tpl_dir; $this->cache_dir = $tpl_dir.'/cache'; //There is only one cache, but everything else has multiples. $this->widget_dirs = array($tpl_dir.'/widgets'); $this->template_dirs = array($tpl_dir.'/templates'); $this->action_dirs = array($tpl_dir.'/actions'); //Default site settings which may be overridden. $this->host = $_SERVER['HTTP_HOST']; $this->root = $_SERVER['DOCUMENT_ROOT']; if(is_dir($this->root.'/_data')) $this->tpl_dir_local = $this->root.'/_data'; if(is_file($this->tpl_dir.'/settings.php')) include_once($this->tpl_dir.'/settings.php'); /*** SET UP SITE SPECIFICS ***/ if($this->tpl_dir_local && is_dir($this->tpl_dir_local.'/actions')) array_push($this->action_dirs, $this->tpl_dir_local.'/actions'); if($this->tpl_dir_local && is_dir($this->tpl_dir_local.'/templates')) array_push($this->template_dirs, $this->tpl_dir_local.'/templates'); if($this->tpl_dir_local && is_dir($this->tpl_dir_local.'/widgets')) array_push($this->widget_dirs, $this->tpl_dir_local.'/widgets'); if($this->tpl_dir_local && is_file($this->tpl_dir_local.'/settings.php')) include_once($this->tpl_dir_local.'/settings.php'); /*** DEFAULT PAGE ***/ if($file_path == '') { if($this->src_filename) $file_path = $_SERVER[$this->src_filename]; else $file_path = $this->root.$_SERVER[$this->src_name]; $file_path_autodetected = true; } else { $file_path_autodetected = false; } /*** ERROR CHECKING ***/ if(!is_dir($this->tpl_dir)) { throw new Exception("Initialization error: The specified data repository '{$this->tpl_dir}' is not a directory.", TEMPLATION_BAD_SETTING); $this->invalid = true; return; } if(!is_dir($this->root)) { throw new Exception("Initialization error: The specified site root '{$this->root}' is not a directory.", TEMPLATION_BAD_SETTING); $this->invalid = true; return; } elseif(!is_file($file_path)) { throw new Exception("Initialization error: The specified file '{$file_path}'".($file_path_autodetected ? " (auto-detected from URL)" : '')." is not a file.", TEMPLATION_BAD_SETTING); $this->invalid = true; return; } /*** INITIAL PAGE SETUP, won't be built until output() is called on it ***/ $this->pages[] = new Templation_File($this,$file_path); } function output($page_num = 0) { if ( isset($this->pages[$page_num]) && Templation_File::isTemplationFile($this->pages[$page_num]) ) { return $this->pages[$page_num]->outputFile(); } else { throw new Exception("output(): Requested page doesn't exist", TEMPLATION_INVALID_FILE_OBJECT); return false; } } /** * Takes a reference to a template string and parses values directly into it according to what the * mode_vector specifies (currently data, includes, and widgets). The src_page is an optional parameter * which is used to provide a starting point for the recursive inclusion capabilities. As the main parsing * engine of Templation, this function is set up so that it can be used independently of the automatic * page building functionality. If you leave out the source page, the function will include files * from the base include directory found in the Templation directory itself. Specifying an include search * path could be useful, but the core of this class is its cascading parameters and not the template engine itself. * Please note that src_page should be passed by reference so that addDependency() works as expected. * **/ function parseTemplate(&$template, $data, &$src_page, $mode_vector = 7) { if (Templation_File::isTemplationFile($src_page)) { $using_src_page = true; $inc_dir = $src_page->tpl->root.'/'.$src_page->rel_dir; //Initial includes directory for recursive search. $userinfo['Source Page'] = $src_page->full_path; //For error reporting. } else { $using_src_page = false; $inc_dir = $this->tpl_dir.'/'.$this->includes_dir; //Default includes come from Templation directory. $userinfo['Source Page'] = "unspecified"; //For error reporting. } //Process custom tags preg_match_all('/<%%[^%]+%%>/i',$template,$matches); foreach ($matches[0] as $tag) { $tag_value = preg_replace('/<%%([^%]+)%%>/','\1',$tag); $values = preg_split('/\s+/',$tag_value); //Tags with a single symbol will be replaced directly with values from the data array. if (count($values) == 1 && ($mode_vector & TEMPLATION_PARSE_DATA)) { if (!isset($data[$values[0]])) $data[$values[0]] = ''; $template = str_replace($tag,$data[$values[0]],$template); //<&&inc filename&&> is shorthand for the tpl_include.php widget. It did not start this way //but it made sense to make the widget conversion once the widget API was available. } else if ($values[0] == 'inc' && ($mode_vector & TEMPLATION_PARSE_INCLUDES)) { $widget = 'tpl_include.php'; $argv = array_slice($values,1); $widget_out = $this->_processWidget($widget,$argv,$data,$src_page); $template = str_replace($tag,$widget_out,$template); //<&&tpl filename&&> is shorthand for the tpl_template.php widget. } else if ($values[0] == 'tpl' && ($mode_vector & TEMPLATION_PARSE_WIDGETS)) { $widget = 'tpl_template.php'; $argv = array_slice($values,1); $widget_out = $this->_processWidget($widget,$argv,$data,$src_page); $template = str_replace($tag,$widget_out,$template); //Here we process widgets. Widgets are basically code blocks that execute in-line (right here) as opposed to //includes which are simply output into the cached copy (ie. code does not execute until page is finally served). //Because widgets operate within Templation, they are all stored in a single directory. } else if ($mode_vector & TEMPLATION_PARSE_WIDGETS) { $widget = $values[0]; $argv = array_slice($values,1); $widget_out = $this->_processWidget($widget,$argv,$data,$src_page); $template = str_replace($tag,$widget_out,$template); //This implies the tag in question is not a type currently being parsed, strip the tag. } else { $template = str_replace($tag,'',$template); } } } //end parseTemplate() /** * This function checks for existence of a widget and instantiates it if it hasn't been * already ($this->widgets is hashed on the widget filename). Then it returns the widget's * output. **/ function _processWidget($widget, $argv, $data, &$src_page) { foreach($this->widget_dirs as $path) { if(is_file($path."/".$widget)) $widget_path = $path."/".$widget; } if (!isset($widget_path)) { throw new Exception("_processWidget(): Widget '$widget' does not exist", TEMPLATION_WIDGET_NOT_FOUND); } else { if(!isset($this->widgets[$widget])) { $this->widgets[$widget] = new Templation_Widget($widget_path, $argv, $data, $src_page); $src_page->addDependency($widget_path); } else { $this->widgets[$widget]->_reInit($argv, $data, $src_page); } return $this->widgets[$widget]->output(); } return false; } /*** STATIC FUNCTIONS CAN BE INSTALLED WITH INSTANTIATION ***/ function errorMessage($value) { static $errorMessages; if (!isset($errorMessages)) { $errorMessages = array( TEMPLATION_OK => 'no error', TEMPLATION_ERROR => 'error: unknown error', TEMPLATION_TEMPLATE_NOT_FOUND => 'error: template not found', TEMPLATION_TEMPLATE_UNOPENABLE => 'error: template unopenable', TEMPLATION_BAD_SETTING => 'error: bad setting', TEMPLATION_WARNING => 'warning: generic', TEMPLATION_INCLUDE_NOT_FOUND => 'warning: include not found', TEMPLATION_INCLUDE_UNOPENABLE => 'warning: include unopenable', TEMPLATION_WIDGET_NOT_FOUND => 'warning: widget not found', TEMPLATION_WIDGET_UNOPENABLE => 'warning: widget unopenable', TEMPLATION_INVALID_PATH => 'warning: invalid path', TEMPLATION_INVALID_FILE_OBJECT => 'warning: invalid file object', TEMPLATION_WIDGET_ERROR => 'warning: widget error', TEMPLATION_FOLDER_UNOPENABLE => 'warning: folder unopenable', TEMPLATION_NOTICE => 'notice: generic' ); } if (Templation::isError($value)) { $value = $value->getCode(); } return isset($errorMessages[$value]) ? $errorMessages[$value] : $errorMessages[TEMPLATION_ERROR]; } /** * This function accepts a comma-delimited list of meta values to be converted to an array. * Commas may be escaped, and backslashes may be escaped (to handle the case of a backslash * occurring just before a comma). Other backslashes will be output literally. **/ function metaToArray($string, $trim = true) { if(is_array($string)) return $string; if(is_bool($string) || is_int($string)) return array($string); $out = array(''); $escaped = false; $j = 0; for($i=0; $iroot, leading slash. var $rel_dir = ''; //Path to directory that source file resides in. var $full_path = ''; //Full path for use in file functions. var $dirs = array(); //All directories between root and current file. Note index is one off from deep_meta. var $deep_meta = array(array());//$deep_meta[0] = base_dir meta, [n] = file meta. var $file_meta = array(); var $data = array(); //The flattened version of $deep_meta. var $cache_path = ''; //Full path of cache file (branches on tpl->host and tpl->template_mode) var $dependencies_path = ''; //Full path of dependency list cache. var $dependencies = array(); //Filenames of files used. var $parse_me; //Indication whether page was parsed or if output is just raw file contents (depends on parse_me). var $output = ''; //Final page output to be included, must be accessed via getOutput() or may be empty. /** * Constructor **/ function Templation_File(&$tpl, $filepath) { //First check that file exists and is within root $this->tpl =& $tpl; $this->rel_path = substr($filepath,strlen($this->tpl->root)); $this->rel_dir = substr_replace($this->rel_path, '', strrpos($this->rel_path,'/') + 1); $this->full_path = $filepath; $this->cache_path = $this->tpl->cache_dir.'/'.$this->tpl->host.'/'.$this->tpl->template_mode.$this->rel_path; $this->dependencies_path = substr_replace($this->cache_path,'_dependencies_',strrpos($this->cache_path,'/')+1,0); if(strlen($this->rel_dir) > 1) $this->dirs = explode('/',trim($this->rel_dir,'/')); //If not at root level. $this->gatherMeta(); //flatten data foreach ($this->deep_meta as $tempData) { while (list($key,$val) = each($tempData)) { $this->data[$key] = $val; } } // If parse by default is true and parse_me is not false // OR if parse_me is true then parse, otherwise don't parse. if (isset($this->data['parse_me']) && $this->data['parse_me']) { $this->parse_me = true; } else { $this->parse_me = false; } } /** * Tha heavy lifting. * Uses cached copy if it exists and is current, otherwise builds anew. **/ function buildPage() { if ( !$this->cacheValid() ) { //If parse_me is not true or there is an error reading the template, then just output file. if ( !$this->parse_me || (!$this->_readTemplate($this->output, $this->data['template'])) ) { $fp = fopen($this->full_path, "r"); $contents = fread($fp, filesize($this->full_path)); fclose ($fp); $this->output = preg_replace('/include(_once)?\([^)]+\);?/','',$contents,1); $this->parsed = false; } else { //This implies success of _readTemplate() and we can then parse it. $this->tpl->parseTemplate($this->output, $this->data, $this); $this->_runActions(); $this->parsed = true; } $this->validateCachePath(); $oldumask = umask(0); $fp = fopen($this->cache_path,"w"); fwrite($fp, $this->output); fclose($fp); $d = implode("\n",$this->dependencies); $fp = fopen($this->dependencies_path,"w"); fwrite($fp, $d); fclose($fp); umask($oldumask); } } /** * Output processed page. **/ function outputFile() { if ( empty($this->output) ) { $this->buildPage(); if ( empty($this->output) ) $this->output = 'Empty Page'; } return $this->cache_path; } /** * Recursively gather array from base_dir to rel_path from $includes_dir/$meta_filename. **/ function gatherMeta() { //initialize values for loop $temp_dir = $this->tpl->root; $d = $this->dirs; if (!isset($this->dirs[0]) || $this->dirs[0] !== '') $d = array_merge(array(''),$d); //Blank entry simplified loop setup. //gather meta information from appropriate sources //Adding dependencies should be done here. Simply adding all $fnames would not work because //if a file is deleted, there is no record that it ever existed. On the other hand, if we only //add $fnames that exist, then if new meta files are added in the hierarchy, there will be no dependency //recorded for that file and hence it will be ignored by a strict dependency modified-date check. //Some special check will be necessary. //First get meta data from data/includes: $fname = $this->tpl->tpl_dir.'/'.$this->tpl->includes_dir.'/'.$this->tpl->meta_name; if(is_file($fname)) { $this->dependencies[] = $fname; $this->deep_meta[0] = get_meta_tags($fname); //Beware get_meta_tag bugs. array_walk($this->deep_meta[0],array($this,'metaValueParser')); } else { $this->dependencies[] = $fname." 0"; $this->deep_meta[0] = array(); } //Second get meta data from all the website directories. $i = 1; foreach($d as $thang) { if ($thang != '') $temp_dir .= '/'.$thang; $fname = $temp_dir.'/'.$this->tpl->includes_dir.'/'.$this->tpl->meta_name; if (is_file($fname)) { $this->dependencies[] = $fname; $this->deep_meta[$i] = get_meta_tags($fname); //Beware get_meta_tag bugs. array_walk($this->deep_meta[$i], array($this,'metaValueParser')); } else { $this->dependencies[] = $fname." 0"; $this->deep_meta[$i] = array(); } $i++; } //Third get meta from the actual file. $this->deep_meta[$i] = get_meta_tags($this->full_path); array_walk($this->deep_meta[$i], array($this,'metaValueParser')); // Read in standard pagebuilding elements. // leading_php - Code that needs to send headers must occur before output, so it can be // inserted in the first PHP block before the element. // head - Standard element contents. // body - Standard element contents. // frameset - Standard element instead of . // // Beware the regexp method of pulling these elements. In particular, the sequence // full_path, "r"); $content = fread($fp, filesize($this->full_path)); fclose($fp); //Note that the php must be the first thing in the document, also, this is a naive pattern matcher. //If you have a string containing the PHP closing tag then the final output will be mysteriously buggy. //Note that there are some added slashes to prevent BBEdit syntax highlighting issues related to closing PHP tags. preg_match('/^(<\?php.*?\?>)/is', $content, $matches); if (isset($matches[1])) { //Strip out the first include() as that is the one that is used to invoke the driver program. $raw_php = $matches[1]; //The length of this is used below for finding where the body starts in absense of body tags. $this->deep_meta[$i]['leading_php'] = preg_replace('/include(_once)?\([^;\n]+\);?/','',$raw_php,1); if(preg_match('/^<\?php\s*\?>$/s',$this->deep_meta[$i]['leading_php'])) $this->deep_meta[$i]['leading_php'] = ''; } else { $this->deep_meta[$i]['leading_php'] = ''; } if (preg_match("/]*>(.*)<\/body>/is", $content, $matches)) { $this->deep_meta[$i]['body'] = $matches[1]; } else { //If no body tag than the entire thing (- leading_php) is the body. $this->deep_meta[$i]['body'] = substr($content, strlen($raw_php)); } if (preg_match("/]*>(.*)<\/head>/is", $content, $matches)) { $this->deep_meta[$i]['head'] = $matches[1]; } if (preg_match("/]*>(.*)<\/frameset>/is", $content, $matches)) { $this->deep_meta[$i]['frameset'] = $matches[1]; } $this->file_meta = $this->deep_meta[$i]; } /** * This function is a helper used by array_walk() in gather_meta() to convert the string 'false' and 'true' to * boolean values as well as integer values (no spaces!) to actual PHP ints. In beta 2, comma-delimited lists * were parsed automatically, but that complicated matters so both scalar and array values needed to be accounted * for. That functionality has been added to Templation_Widget::metaToArray(). **/ function metaValueParser(&$string, $index) { if($string=="true") $string = true; else if($string=="false") $string = false; else if(preg_match('/^\d+$/',$string)) $string = intval($string); else $string = strtr($string, array_flip(get_html_translation_table(HTML_SPECIALCHARS))); //print "$string!
\n"; return $string; } /** * This function must be called with the full pathname of every single page that is included or read * or influences the construction of a page in any way otherwise the caching mechanism will not know * when a page needs to be rebuilt. **/ function addDependency($dependency) { $this->dependencies[$dependency] = $dependency; } /** * This function returns true if the cache file exists and was modified more recently * than the file itself or any of its dependencies. Otherwise false. **/ function cacheValid() { if ( (!is_file($this->cache_path)) || (filemtime($this->full_path) > ($cache_timestamp = filemtime($this->cache_path)) || !$this->tpl->caching_on) ) return false; //Need to check for existence of file if ( $fp = fopen($this->dependencies_path, "r") ) { $c = fread( $fp, filesize($this->dependencies_path) ); fclose($fp); $dependencies = preg_split("/(\r\n)|(\r)|(\n)/",$c,-1,PREG_SPLIT_NO_EMPTY); foreach ($dependencies as $string) { unset($matches); if ( preg_match("/([^\s]+)\s*(\d*)/",$string,$matches) == 1 ) { //0 indicates file did not exist at the time. if ($matches[2] == '0') { if ( is_file($matches[1]) ) return false; } else { if ( $cache_timestamp < filemtime($matches[1]) ) return false; } } } return true; } else { //error opening file. } return false; } /** * If $this->cache_path is not valid, this function will create directories as needed to validate * the path. If the last piece of the path doesn't contain a period this function * assumes the path does NOT end with a file name. **/ function validateCachePath() { $relative_path = str_replace($this->tpl->cache_dir,'',$this->cache_path); $path_array = explode('/',$relative_path); // Drop the filename part of the path if it is there if(strchr($path_array[sizeof($path_array)-1], ".")) $path_array[sizeof($path_array)-1] = ""; $path = $this->tpl->cache_dir; foreach($path_array as $p) { if($p != "") $path .= "/$p"; } if(is_dir($path)) return; $i = 0; $path = $this->tpl->cache_dir; $newpath = false; $oldumask = umask(0); while(isset($path_array[$i])) { if($path_array[$i] != "") { $path .= "/" . $path_array[$i]; if($newpath || !is_dir($path)) { $newpath = true; // from here on out the path isn't going to exist if(!mkdir($path, 0777)) trigger_error("'$path' is unwritable by server.", E_USER_ERROR); } } $i++; } umask($oldumask); } /** * Statically checks to see if an object is of this class. **/ function isTemplationFile($obj) { return is_a($obj,'Templation_File'); //Requires PHP 4.2+ } /** * Opens and reads a template file from the standard templates directory using * the template_mode subdirectory if necessary. * **/ function _readTemplate(&$template, $filename) { $userinfo['Source Page'] = $this->full_path; //For error reporting purposes. foreach($this->tpl->template_dirs as $path) { if(is_file($path.'/'.$this->tpl->template_mode.'/'.$filename)) $template_filename = $path.'/'.$this->tpl->template_mode.'/'.$filename; else if(is_file($path.'/'.$filename)) $template_filename = $path.'/'.$filename; } if (!isset($template_filename)) { throw new Exception("_readTemplate(): Unsuccessful finding '$filename'", TEMPLATION_TEMPLATE_NOT_FOUND); } else if ( ($fp = fopen($template_filename, 'r')) === false) { throw new Exception("_readTemplate(): Unsuccessful opening '$filename'", TEMPLATION_TEMPLATE_UNOPENABLE); } else { $this->addDependency($template_filename); $template = fread($fp, filesize($template_filename)); return true; } return false; } //end _readTemplate() /** * This function runs all the actions specified (if they exist). **/ function _runActions() { if (isset($this->data['actions'])) { $actions = Templation::metaToArray($this->data['actions']); foreach($actions as $a) { $action_path = ''; foreach($this->tpl->action_dirs as $path) { if(is_file($path.'/'.$a)) $action_path = $path.'/'.$a; } if(!$action_path) throw new Exception("_runActions(): Action '$a' not found.", TEMPLATION_ACTION_NOT_FOUND); else include($action_path); } } } //end _runActions() } ?>