From d4c2aa240cb09ef33ece91769f2781186edffd2d Mon Sep 17 00:00:00 2001 From: pdontthink Date: Mon, 2 Oct 2006 09:38:05 +0000 Subject: [PATCH] Massive rewrite of Template class to implement template inheritance, along with a fallback template (different than concept of a default which just helps catch void in user prefs). As suggested by Alexandros, a caching mechanism replaces the heaps of file_exists() calls in previous implementation, although this introduces more complexity, so watch for bugs! Inheritance implies changes in stylesheet and javascript file loading, in that now all such files inherited from all ancestors are loaded in case a template falls back to one of those ancestors (or the fallback set). Template config file caching and an easy API to get those settings also implemented. Finally, when a template set falls back to another set with a different template engine for a given page, the engine is swapped out dynamically (although this is less than ideal and should be discouraged when authors are creating template sets). git-svn-id: https://svn.code.sf.net/p/squirrelmail/code/trunk/squirrelmail@11791 7612ce4b-ef26-0410-bec9-ea0150e637f0 --- class/template/Template.class.php | 1031 +++++++++++++++++++++++------ 1 file changed, 817 insertions(+), 214 deletions(-) diff --git a/class/template/Template.class.php b/class/template/Template.class.php index 1e913621..43c83c63 100644 --- a/class/template/Template.class.php +++ b/class/template/Template.class.php @@ -31,6 +31,8 @@ require(SM_PATH . 'functions/template.php'); * * assign() * assign_by_ref() + * clear_all_assign() + * get_template_vars() * append() * append_by_ref() * apply_template() @@ -48,10 +50,11 @@ class Template * @var string * */ - var $template_id = ''; + var $template_set_id = ''; /** - * The template directory to use + * The template set base directory (relative path from + * the main SquirrelMail directory (SM_PATH)) * * @var string * @@ -67,62 +70,73 @@ class Template var $template_engine = ''; /** - * The default template ID + * The fall-back template ID * * @var string * */ - var $default_template_id = ''; + var $fallback_template_set_id = ''; /** - * The default template directory + * The fall-back template directory (relative + * path from the main SquirrelMail directory (SM_PATH)) * * @var string * */ - var $default_template_dir = ''; + var $fallback_template_dir = ''; /** - * The default template engine (please use constants defined in constants.php) + * The fall-back template engine (please use + * constants defined in constants.php) * * @var string * */ - var $default_template_engine = ''; + var $fallback_template_engine = ''; /** - * Javascript files required by the template + * Template file cache. Structured as an array, whose keys + * are all the template file names (with path information relative + * to the template set's base directory, e.g., "css/style.css") + * found in all parent template sets including the ultimate fall-back + * template set. Array values are sub-arrays with the + * following key-value pairs: * - * @var array + * PATH -- file path, relative to SM_PATH + * SET_ID -- the ID of the template set that this file belongs to + * ENGINE -- the engine needed to render this template file * */ - var $required_js_files = array(); + var $template_file_cache = array(); /** - * Alternate stylesheets provided by the template. This is defined in the - * template config file so that we can provide pretty names in the display - * preferences - * - * @var array - **/ - var $alternate_stylesehets = array(); - + * Extra template engine class objects for rendering templates + * that require a different engine than the one for the current + * template set. Keys should be the name of the template engine, + * values are the corresponding class objects. + * + * @var array + * + */ + var $other_template_engine_objects = array(); + /** * Constructor * * Please do not call directly. Use Template::construct_template(). * - * @param string $template_id the template ID + * @param string $template_set_id the template ID * */ - function Template($template_id) { + function Template($template_set_id) { //FIXME: find a way to test that this is ONLY ever called // from the construct_template() method (I doubt it // is worth the trouble to parse the current stack trace) // if (???) // trigger_error('Please do not use default Template() constructor. Instead, use Template::construct_template().', E_USER_ERROR); - $this->set_up_template($template_id); + $this->set_up_template($template_set_id); } @@ -133,14 +147,16 @@ class Template * to get a Template object from the normal/default constructor, * and is necessary in order to control the return value. * - * @param string $template_id the template ID + * @param string $template_set_id the template ID * * @return object The correct Template object for the given template set * + * @static + * */ - function construct_template($template_id) { + function construct_template($template_set_id) { - $template = new Template($template_id); + $template = new Template($template_set_id); return $template->get_template_engine_subclass(); } @@ -151,69 +167,110 @@ class Template * This method does most of the work for setting up * newly constructed objects. * - * @param string $template_id the template ID + * @param string $template_set_id the template ID * */ - function set_up_template($template_id) { + function set_up_template($template_set_id) { // FIXME: do we want to place any restrictions on the ID like // making sure no slashes included? // get template ID // - $this->template_id = $template_id; + $this->template_set_id = $template_set_id; - // FIXME: do we want to place any restrictions on the ID like - // making sure no slashes included? - // get default template ID - // - global $templateset_default, $aTemplateSet; - $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet) - ? array() : $aTemplateSet); - $templateset_default = (!isset($templateset_default) ? 0 : $templateset_default); - $this->default_template_id = (!empty($aTemplateSet[$templateset_default]['ID']) - ? $aTemplateSet[$templateset_default]['ID'] - : 'default'); + $this->fallback_template_set_id = Template::get_fallback_template_set(); // set up template directories // $this->template_dir - = Template::calculate_template_file_directory($this->template_id); - $this->default_template_dir - = Template::calculate_template_file_directory($this->default_template_id); + = Template::calculate_template_file_directory($this->template_set_id); + $this->fallback_template_dir + = Template::calculate_template_file_directory($this->fallback_template_set_id); - // pull in the template config file and load javascript and - // css files needed for this template set + // determine template engine + // FIXME: assuming PHP template engine may not necessarily be a good thing // - $template_config_file = SM_PATH . $this->get_template_file_directory() - . 'config.php'; - if (!file_exists($template_config_file)) { + $this->template_engine = Template::get_template_config($this->template_set_id, + 'template_engine', + SQ_PHP_TEMPLATE); - trigger_error('No template configuration file was found where expected: ("' - . $template_config_file . '")', E_USER_ERROR); - } else { + // get template file cache + // + $this->template_file_cache = Template::cache_template_file_hierarchy(); - require($template_config_file); - $this->required_js_files = is_array($required_js_files) - ? $required_js_files : array(); - $this->alternate_stylesheets = is_array($alternate_stylesheets) ? - $alternate_stylesheets : - array(); + } - } + /** + * Determine what the ultimate fallback template set is. + * + * NOTE that if the fallback setting cannot be found in the + * main SquirrelMail configuration settings that the value + * of $default is returned. + * + * @param string $default The template set ID to use if + * the fallback setting cannot be + * found in SM config (optional; + * defaults to "default"). + * + * @return string The ID of the fallback template set. + * + * @static + * + */ + function get_fallback_template_set($default='default') { +// FIXME: do we want to place any restrictions on the ID such as +// making sure no slashes included? - // determine template engine + // values are in main SM config file // - if (empty($template_engine)) { - trigger_error('No template engine ($template_engine) was specified in template configuration file: ("' - . $template_config_file . '")', E_USER_ERROR); - } else { - $this->template_engine = $template_engine; - } + global $templateset_fallback, $aTemplateSet; + $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet) + ? array() : $aTemplateSet); + $templateset_fallback = (!isset($templateset_fallback) + ? 0 : $templateset_fallback); + + return (!empty($aTemplateSet[$templateset_fallback]['ID']) + ? $aTemplateSet[$templateset_fallback]['ID'] : $default); + + } + + /** + * Determine what the default template set is. + * + * NOTE that if the default setting cannot be found in the + * main SquirrelMail configuration settings that the value + * of $default is returned. + * + * @param string $default The template set ID to use if + * the default setting cannot be + * found in SM config (optional; + * defaults to "default"). + * + * @return string The ID of the default template set. + * + * @static + * + */ + function get_default_template_set($default='default') { + +// FIXME: do we want to place any restrictions on the ID such as +// making sure no slashes included? + + // values are in main SM config file + // + global $templateset_default, $aTemplateSet; + $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet) + ? array() : $aTemplateSet); + $templateset_default = (!isset($templateset_default) + ? 0 : $templateset_default); + + return (!empty($aTemplateSet[$templateset_default]['ID']) + ? $aTemplateSet[$templateset_default]['ID'] : $default); } @@ -221,23 +278,34 @@ class Template * Instantiate and return correct subclass for this template * set's templating engine. * + * @param string $template_set_id The template set whose engine + * is to be used as an override + * (if not given, this template + * set's engine is used) (optional). + * * @return object The Template subclass object for the template engine. * */ - function get_template_engine_subclass() { + function get_template_engine_subclass($template_set_id='') { + + if (empty($template_set_id)) $template_set_id = $this->template_set_id; + // FIXME: assuming PHP template engine may not necessarily be a good thing + $engine = Template::get_template_config($template_set_id, + 'template_engine', SQ_PHP_TEMPLATE); + $engine_class_file = SM_PATH . 'class/template/' - . $this->template_engine . 'Template.class.php'; + . $engine . 'Template.class.php'; if (!file_exists($engine_class_file)) { - trigger_error('Unknown template engine (' . $this->template_engine + trigger_error('Unknown template engine (' . $engine . ') was specified in template configuration file', E_USER_ERROR); } - $engine_class = $this->template_engine . 'Template'; - require($engine_class_file); - return new $engine_class($this->template_id); + $engine_class = $engine . 'Template'; + require_once($engine_class_file); + return new $engine_class($template_set_id); } @@ -245,15 +313,17 @@ class Template * Determine the relative template directory path for * the given template ID. * - * @param string $template_id The template ID from which to build - * the directory path + * @param string $template_set_id The template ID from which to build + * the directory path * * @return string The relative template path (based off of SM_PATH) * + * @static + * */ - function calculate_template_file_directory($template_id) { + function calculate_template_file_directory($template_set_id) { - return 'templates/' . $template_id . '/'; + return 'templates/' . $template_set_id . '/'; } @@ -261,15 +331,17 @@ class Template * Determine the relative images directory path for * the given template ID. * - * @param string $template_id The template ID from which to build - * the directory path + * @param string $template_set_id The template ID from which to build + * the directory path * * @return string The relative images path (based off of SM_PATH) * + * @static + * */ - function calculate_template_images_directory($template_id) { + function calculate_template_images_directory($template_set_id) { - return 'templates/' . $template_id . '/images/'; + return 'templates/' . $template_set_id . '/images/'; } @@ -286,149 +358,633 @@ class Template } + /** + * Return the template ID for the fallback template set. + * + * @return string The ID of the fallback template set. + * + */ + function get_fallback_template_set_id() { + + return $this->fallback_template_set_id; + + } /** - * Return the relative template directory path for the DEFAULT template set. + * Return the relative template directory path for the + * fallback template set. * - * @return string The relative path to the default template directory based - * from the main SquirrelMail directory (SM_PATH). + * @return string The relative path to the fallback template + * directory based from the main SquirrelMail + * directory (SM_PATH). * */ - function get_default_template_file_directory() { + function get_fallback_template_file_directory() { + + return $this->fallback_template_dir; + + } + + /** + * Get template set config setting + * + * Given a template set ID and setting name, returns the + * setting's value. Note that settings are cached in + * session, so "live" changes to template configuration + * won't be reflected until the user logs out and back + * in again. + * + * @param string $template_set_id The template set for which + * to look up the setting. + * @param string $setting The name of the setting to + * retrieve. + * @param mixed $default When the requested setting + * is not found, the contents + * of this value are returned + * instead (optional; default + * is NULL). + * NOTE that unlike sqGetGlobalVar(), + * this function will also return + * the default value if the + * requested setting is found + * but is empty. + * @param boolean $live_config When TRUE, the target template + * set's configuration file is + * reloaded every time this + * method is called. Default + * behavior is to only load the + * configuration file if it had + * never been loaded before, but + * not again after that (optional; + * default FALSE). Use with care! + * Should mostly be used for + * debugging. + * + * @return mixed The desired setting's value or if not found, + * the contents of $default are returned. + * + * @static + * + */ + function get_template_config($template_set_id, $setting, + $default=NULL, $live_config=FALSE) { + + sqGetGlobalVar('template_configuration_settings', + $template_configuration_settings, + SQ_SESSION, + array()); + + if ($live_config) unset($template_configuration_settings[$template_set_id]); + + + // NOTE: could use isset() instead of empty() below, but + // this function is designed to replace empty values + // as well as non-existing values with $default + // + if (!empty($template_configuration_settings[$template_set_id][$setting])) + return $template_configuration_settings[$template_set_id][$setting]; + + + // if template set configuration has been loaded, but this + // setting is not known, return $default + // + if (!empty($template_configuration_settings[$template_set_id])) + return $default; + + + // otherwise (template set configuration has not been loaded before), + // load it into session and return the desired setting after that + // + $template_config_file = SM_PATH + . Template::calculate_template_file_directory($template_set_id) + . 'config.php'; + + if (!file_exists($template_config_file)) { + + trigger_error('No template configuration file was found where expected: ("' + . $template_config_file . '")', E_USER_ERROR); + + } else { + + // we require() the file to let PHP do the variable value + // parsing for us, and read the file in manually so we can + // know what variable names are used in the config file + // (settings can be different depending on specific requirements + // of different template engines)... the other way this may + // be accomplished is to somehow diff the symbol table + // before/after the require(), but anyway, this code should + // only run once for this template set... + // + require($template_config_file); + $file_contents = implode("\n", file($template_config_file)); + + + // note that this assumes no template settings have + // a string in them that looks like a variable name like $x + // also note that this will attempt to grab things like + // $Id found in CVS headers, so we try to adjust for that + // by checking that the variable is actually set + // + preg_match_all('/\$(\w+)/', $file_contents, $variables, PREG_PATTERN_ORDER); + foreach ($variables[1] as $variable) { + if (isset($$variable)) + $template_configuration_settings[$template_set_id][$variable] + = $$variable; + } + + sqsession_register($template_configuration_settings, + 'template_configuration_settings'); + + // NOTE: could use isset() instead of empty() below, but + // this function is designed to replace empty values + // as well as non-existing values with $default + // + if (!empty($template_configuration_settings[$template_set_id][$setting])) + return $template_configuration_settings[$template_set_id][$setting]; + else + return $default; + + } + + } + + /** + * Obtain template file hierarchy from cache. + * + * If the file hierarchy does not exist in session, it is + * constructed and stored in session before being returned + * to the caller. + * + * @param boolean $regenerate_cache When TRUE, the file hierarchy + * is reloaded and stored fresh + * (optional; default FALSE). + * @param array $additional_files Must be in same form as the + * files in the file hierarchy + * cache. These are then added + * to the cache (optional; default + * empty - no additional files). + * + * @return array Template file hierarchy array, whose keys + * are all the template file names (with path + * information relative to the template set's + * base directory, e.g., "css/style.css") + * found in all parent template sets including + * the ultimate fall-back template set. + * Array values are sub-arrays with the + * following key-value pairs: + * + * PATH -- file path, relative to SM_PATH + * SET_ID -- the ID of the template set that this file belongs to + * ENGINE -- the engine needed to render this template file + * + * @static + * + */ + function cache_template_file_hierarchy($regenerate_cache=FALSE, + $additional_files=array()) { + + sqGetGlobalVar('template_file_hierarchy', $template_file_hierarchy, + SQ_SESSION, array()); + + + if ($regenerate_cache) unset($template_file_hierarchy); + + + if (!empty($template_file_hierarchy)) { + + // have to add additional files if given before returning + // + if (!empty($additional_files)) { + $template_file_hierarchy = array_merge($template_file_hierarchy, + $additional_files); + sqsession_register($template_file_hierarchy, + 'template_file_hierarchy'); + } + + return $template_file_hierarchy; + } + + + // nothing in cache apparently, so go build it now + // + // FIXME: not sure if there is any possibility that + // this could be called when $sTemplateID has + // yet to be defined... throw error for now, + // but if the error occurs, it's a coding error + // rather than a configuration error + // + global $sTemplateID; + if (empty($sTemplateID)) { + + trigger_error('Template set ID unknown', E_USER_ERROR); + + } else { + + $template_file_hierarchy = Template::catalog_template_files($sTemplateID); + + // additional files, if any + // + if (!empty($additional_files)) { + $template_file_hierarchy = array_merge($template_file_hierarchy, + $additional_files); + } + + sqsession_register($template_file_hierarchy, + 'template_file_hierarchy'); + + return $template_file_hierarchy; + + } + + } + + /** + * Traverse template hierarchy and catalogue all template + * files (for storing in cache). + * + * Paths to all files in all parent, grand-parent, great grand + * parent, etc. template sets (including the fallback template) + * are catalogued; for identically named files, the file earlier + * in the hierarchy (closest to this template set) is used. + * + * @param string $template_set_id The template set in which to + * search for files + * @param array $file_list The file list so far to be added + * to (allows recursive behavior) + * (optional; default empty array). + * @param string $directory The directory in which to search for + * files (must be given as full path). + * If empty, starts at top-level template + * set directory (optional; default empty). + * NOTE! Use with care, as behavior is + * unpredictable if directory given is not + * part of correct template set. + * + * @return mixed The top-level caller will have an array of template + * files returned to it; recursive calls to this function + * do not receive any return value at all. The format + * of the template file array is as described for the + * Template class attribute $template_file_cache + * + * @static + * + */ + function catalog_template_files($template_set_id, $file_list=array(), $directory='') { + + $template_base_dir = SM_PATH + . Template::calculate_template_file_directory($template_set_id); + + if (empty($directory)) { + $directory = $template_base_dir; + } - return $this->default_template_dir; + $files_and_dirs = list_files($directory, '', FALSE, TRUE, FALSE, TRUE); + + // recurse for all the subdirectories in the template set + // + foreach ($files_and_dirs['DIRECTORIES'] as $dir) { + $file_list = Template::catalog_template_files($template_set_id, $file_list, $dir); + } + + // place all found files in the cache + // FIXME: assuming PHP template engine may not necessarily be a good thing + // + $engine = Template::get_template_config($template_set_id, + 'template_engine', SQ_PHP_TEMPLATE); + foreach ($files_and_dirs['FILES'] as $file) { + + // remove the part of the file path corresponding to the + // template set's base directory + // + $relative_file = substr($file, strlen($template_base_dir)); + + // only put file in cache if not already found in earlier template + // + if (!isset($file_list[$relative_file])) { + $file_list[$relative_file] = array( + 'PATH' => $file, + 'SET_ID' => $template_set_id, + 'ENGINE' => $engine, + ); + } + + } + + + // now if we are currently at the top-level of the template + // set base directory, we need to move on to the parent + // template set, if any + // + if ($directory == $template_base_dir) { + + // use fallback when we run out of parents + // + $fallback_id = Template::get_fallback_template_set(); + $parent_id = Template::get_template_config($template_set_id, + 'parent_template_set', + $fallback_id); + + // were we already all the way to the last level? just exit + // + // note that this code allows the fallback set to have + // a parent, too, but can result in endless loops + // if ($parent_id == $template_set_id) { + // + if ($fallback_id == $template_set_id) { + return $file_list; + } + + $file_list = Template::catalog_template_files($parent_id, $file_list); + + } + + return $file_list; } + /** + * Look for a template file in a plugin; add to template + * file cache if found. + * + * The file is searched for in the following order: + * + * - A directory for the current template set within the plugin: + * SM_PATH/plugins//templates/