* @package squirrelmail * */ class Template { /** * The template ID * * @var string * */ var $template_set_id = ''; /** * The template set base directory (relative path from * the main SquirrelMail directory (SM_PATH)) * * @var string * */ var $template_dir = ''; /** * The template engine (please use constants defined in constants.php) * * @var string * */ var $template_engine = ''; /** * The fall-back template ID * * @var string * */ var $fallback_template_set_id = ''; /** * The fall-back template directory (relative * path from the main SquirrelMail directory (SM_PATH)) * * @var string * */ var $fallback_template_dir = ''; /** * The fall-back template engine (please use * constants defined in constants.php) * * @var string * */ var $fallback_template_engine = ''; /** * 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: * * 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 $template_file_cache = 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_set_id the 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_set_id); } /** * Construct Template * * This method should always be called instead of trying * to get a Template object from the normal/default constructor, * and is necessary in order to control the return value. * * @param string $template_set_id the template ID * * @return object The correct Template object for the given template set * * @static * */ function construct_template($template_set_id) { $template = new Template($template_set_id); $template->override_plugins(); return $template->get_template_engine_subclass(); } /** * Set up internal attributes * * This method does most of the work for setting up * newly constructed objects. * * @param string $template_set_id the 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_set_id = $template_set_id; $this->fallback_template_set_id = Template::get_fallback_template_set(); // set up template directories // $this->template_dir = Template::calculate_template_file_directory($this->template_set_id); $this->fallback_template_dir = Template::calculate_template_file_directory($this->fallback_template_set_id); // determine template engine // FIXME: assuming PHP template engine may not necessarily be a good thing // $this->template_engine = Template::get_template_config($this->template_set_id, 'template_engine', SQ_PHP_TEMPLATE); // get template file cache // $this->template_file_cache = Template::cache_template_file_hierarchy(); } /** * 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? // values are in main SM config file // global $templateset_fallback, $aTemplateSet; $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet) ? array() : $aTemplateSet); $templateset_fallback = (!isset($templateset_fallback) ? $default : $templateset_fallback); // iterate through all template sets, is this a valid skin ID? // $found_it = FALSE; foreach ($aTemplateSet as $aTemplate) { if ($aTemplate['ID'] === $templateset_fallback) { $found_it = TRUE; break; } } if ($found_it) return $templateset_fallback; // FIXME: note that it is possible for $default to // point to an invalid (nonexistent) template set // and that error will not be caught here // return $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) ? $default : $templateset_default); // iterate through all template sets, is this a valid skin ID? // $found_it = FALSE; foreach ($aTemplateSet as $aTemplate) { if ($aTemplate['ID'] === $templateset_default) { $found_it = TRUE; break; } } if ($found_it) return $templateset_default; // FIXME: note that it is possible for $default to // point to an invalid (nonexistent) template set // and that error will not be caught here // return $default; } /** * Allow template set to override plugin configuration by either * adding or removing plugins. * * NOTE: due to when this code executes, plugins activated here * do not have access to the config_override and loading_prefs * hooks; instead, such plugins can use the * "template_plugins_override_after" hook defined below. * */ function override_plugins() { global $disable_plugins, $plugins, $squirrelmail_plugin_hooks, $null; if ($disable_plugins) return; $add_plugins = Template::get_template_config($this->template_set_id, 'add_plugins', array()); $remove_plugins = Template::get_template_config($this->template_set_id, 'remove_plugins', array()); //FIXME (?) we assume $add_plugins and $remove_plugins are arrays -- we could // error check here, or just assume that template authors or admins // won't screw up their config files // disable all plugins? (can still add some by using $add_plugins) // if (in_array('*', $remove_plugins)) { $plugins = array(); $squirrelmail_plugin_hooks = array(); $remove_plugins = array(); } foreach ($add_plugins as $plugin_name) { // add plugin to global plugin array // $plugins[] = $plugin_name; // enable plugin -- emulate code from use_plugin() function // in SquirrelMail core, but also need to call the // "squirrelmail_plugin_init_" function, which // in static configuration is not called (this inconsistency // could be a source of anomalous-seeming bugs in poorly // coded plugins) // if (file_exists(SM_PATH . "plugins/$plugin_name/setup.php")) { include_once(SM_PATH . "plugins/$plugin_name/setup.php"); $function = "squirrelmail_plugin_init_$plugin_name"; if (function_exists($function)) $function(); } } foreach ($remove_plugins as $plugin_name) { // remove plugin from both global plugin & plugin hook arrays // $plugin_key = array_search($plugin_name, $plugins); if (!is_null($plugin_key) && $plugin_key !== FALSE) { unset($plugins[$plugin_key]); if (is_array($squirrelmail_plugin_hooks)) foreach (array_keys($squirrelmail_plugin_hooks) as $hookName) { unset($squirrelmail_plugin_hooks[$hookName][$plugin_name]); } } } do_hook('template_plugins_override_after', $null); } /** * 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($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/' . $engine . 'Template.class.php'; if (!file_exists($engine_class_file)) { trigger_error('Unknown template engine (' . $engine . ') was specified in template configuration file', E_USER_ERROR); } $engine_class = $engine . 'Template'; require_once($engine_class_file); return new $engine_class($template_set_id); } /** * Determine the relative template directory path for * the given template ID. * * @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_set_id) { return 'templates/' . $template_set_id . '/'; } /** * Determine the relative images directory path for * the given template ID. * * @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_set_id) { return 'templates/' . $template_set_id . '/images/'; } /** * Return the relative template directory path for this template set. * * @return string The relative path to the template directory based * from the main SquirrelMail directory (SM_PATH). * */ function get_template_file_directory() { return $this->template_dir; } /** * 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 * fallback template set. * * @return string The relative path to the fallback template * directory based from the main SquirrelMail * directory (SM_PATH). * */ 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. * * Refuses to traverse directories called ".svn" * * @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; } // bail if we have been asked to traverse a Subversion directory // if (strpos($directory, '/.svn') === strlen($directory) - 5) return $file_list; $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 * PATH should be relative to SquirrelMail top directory */ if (!isset($file_list[$relative_file])) { $file_list[$relative_file] = array( 'PATH' => substr($file,strlen(SM_PATH)), '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/