Happy New Year!
[squirrelmail.git] / class / template / Template.class.php
index b9b992f402d6b870459faf77f5a5f528ab38aade..0ed0e64eb04d46b6c7b5363b053dfd11eaf7098d 100644 (file)
@@ -1,17 +1,14 @@
 <?php
-
-require(SM_PATH . 'functions/template.php');
-
 /**
   * Template.class.php
   *
-  * This file contains an abstract (PHP 4, so "abstract" is relative) 
-  * class meant to define the basic template interface for the 
+  * This file contains an abstract (PHP 4, so "abstract" is relative)
+  * class meant to define the basic template interface for the
   * SquirrelMail core application.  Subclasses should extend this
   * class with any custom functionality needed to interface a target
   * templating engine with SquirrelMail.
-  * 
-  * @copyright &copy; 2003-2006 The SquirrelMail Project Team
+  *
+  * @copyright 2003-2011 The SquirrelMail Project Team
   * @license http://opensource.org/licenses/gpl-license.php GNU Public License
   * @version $Id$
   * @package squirrelmail
@@ -20,6 +17,9 @@ require(SM_PATH . 'functions/template.php');
   *
   */
 
+/** load template functions */
+require(SM_PATH . 'functions/template/general_util.php');
+
 /**
   * The SquirrelMail Template class.
   *
@@ -37,7 +37,7 @@ require(SM_PATH . 'functions/template.php');
   *     append_by_ref()
   *     apply_template()
   *
-  * @author Paul Lesniewski
+  * @author Paul Lesniewski <paul at squirrelmail.org>
   * @package squirrelmail
   *
   */
@@ -53,7 +53,7 @@ class Template
     var $template_set_id = '';
 
     /**
-      * The template set base directory (relative path from 
+      * The template set base directory (relative path from
       * the main SquirrelMail directory (SM_PATH))
       *
       * @var string
@@ -69,6 +69,11 @@ class Template
       */
     var $template_engine = '';
 
+    /**
+      * The content type for this template set
+      */
+    var $content_type = '';
+
     /**
       * The fall-back template ID
       *
@@ -78,7 +83,7 @@ class Template
     var $fallback_template_set_id = '';
 
     /**
-      * The fall-back template directory (relative 
+      * The fall-back template directory (relative
       * path from the main SquirrelMail directory (SM_PATH))
       *
       * @var string
@@ -87,7 +92,7 @@ class Template
     var $fallback_template_dir = '';
 
     /**
-      * The fall-back template engine (please use 
+      * The fall-back template engine (please use
       * constants defined in constants.php)
       *
       * @var string
@@ -98,9 +103,9 @@ class 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 
+      * 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
@@ -130,7 +135,7 @@ class Template
       *
       */
     function Template($template_set_id) {
-//FIXME: find a way to test that this is ONLY ever called 
+//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 (???)
@@ -157,14 +162,15 @@ class Template
     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 
+      * Set up internal attributes
       *
-      * This method does most of the work for setting up 
+      * This method does most of the work for setting up
       * newly constructed objects.
       *
       * @param string $template_set_id the template ID
@@ -184,23 +190,30 @@ class Template
 
         // set up template directories
         //
-        $this->template_dir 
+        $this->template_dir
             = Template::calculate_template_file_directory($this->template_set_id);
-        $this->fallback_template_dir 
+        $this->fallback_template_dir
             = Template::calculate_template_file_directory($this->fallback_template_set_id);
 
 
-        // determine template engine 
+        // determine content type, defaulting to text/html
+        //
+        $this->content_type = Template::get_template_config($this->template_set_id,
+                                                            'content_type',
+                                                            'text/html');
+
+
+        // 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, 
+        $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();
+        $this->template_file_cache = Template::cache_template_file_hierarchy($template_set_id);
 
     }
 
@@ -229,13 +242,29 @@ class Template
         // values are in main SM config file
         //
         global $templateset_fallback, $aTemplateSet;
-        $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet) 
+        $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet)
                          ? array() : $aTemplateSet);
-        $templateset_fallback = (!isset($templateset_fallback) 
-                                 ? 0 : $templateset_fallback);
+        $templateset_fallback = (!isset($templateset_fallback)
+                                 ? $default : $templateset_fallback);
 
-        return (!empty($aTemplateSet[$templateset_fallback]['ID'])
-                ? $aTemplateSet[$templateset_fallback]['ID'] : $default);
+        // 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;
 
     }
 
@@ -266,11 +295,136 @@ class Template
         global $templateset_default, $aTemplateSet;
         $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet)
                          ? array() : $aTemplateSet);
-        $templateset_default = (!isset($templateset_default) 
-                                 ? 0 : $templateset_default);
+        $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;
+
+    }
+
+    /**
+      * Determine what the RPC 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_rpc").
+      *
+      * @return string The ID of the RPC template set.
+      *
+      * @static
+      *
+      */
+    function get_rpc_template_set($default='default_rpc') {
+
+// 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 $rpc_templateset;
+        $rpc_templateset = (!isset($rpc_templateset)
+                         ? $default : $rpc_templateset);
 
-        return (!empty($aTemplateSet[$templateset_default]['ID'])
-                ? $aTemplateSet[$templateset_default]['ID'] : $default);
+        // FIXME: note that it is possible for this to
+        // point to an invalid (nonexistent) template set
+        // and that error will not be caught here
+        //
+        return $rpc_templateset;
+
+    }
+
+    /**
+      * 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_<plugin_name>" 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);
 
     }
 
@@ -279,8 +433,8 @@ class 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 
+      *                                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.
@@ -290,15 +444,15 @@ class Template
 
         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, 
+        $engine = Template::get_template_config($template_set_id,
                                                 'template_engine', SQ_PHP_TEMPLATE);
-        
 
-        $engine_class_file = SM_PATH . 'class/template/' 
+
+        $engine_class_file = SM_PATH . 'class/template/'
                            . $engine . 'Template.class.php';
 
         if (!file_exists($engine_class_file)) {
-            trigger_error('Unknown template engine (' . $engine 
+            trigger_error('Unknown template engine (' . $engine
                         . ') was specified in template configuration file',
                          E_USER_ERROR);
         }
@@ -310,10 +464,10 @@ class Template
     }
 
     /**
-      * Determine the relative template directory path for 
+      * Determine the relative template directory path for
       * the given template ID.
       *
-      * @param string $template_set_id The template ID from which to build 
+      * @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)
@@ -328,10 +482,10 @@ class Template
     }
 
     /**
-      * Determine the relative images directory path for 
+      * Determine the relative images directory path for
       * the given template ID.
       *
-      * @param string $template_set_id The template ID from which to build 
+      * @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)
@@ -371,11 +525,11 @@ class Template
     }
 
     /**
-      * Return the relative template directory path for the 
+      * 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 
+      * @return string The relative path to the fallback template
+      *                directory based from the main SquirrelMail
       *                directory (SM_PATH).
       *
       */
@@ -385,11 +539,23 @@ class Template
 
     }
 
+    /**
+      * Return the content-type for this template set.
+      *
+      * @return string The content-type.
+      *
+      */
+    function get_content_type() {
+
+        return $this->content_type;
+
+    }
+
     /**
       * Get template set config setting
       *
-      * Given a template set ID and setting name, returns the 
-      * setting's value.  Note that settings are cached in 
+      * 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.
@@ -397,20 +563,20 @@ class Template
       * @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.  
+      *                                 retrieve.
       * @param mixed   $default         When the requested setting
       *                                 is not found, the contents
       *                                 of this value are returned
-      *                                 instead (optional; default 
+      *                                 instead (optional; default
       *                                 is NULL).
       *                                 NOTE that unlike sqGetGlobalVar(),
       *                                 this function will also return
-      *                                 the default value if the 
-      *                                 requested setting is found 
+      *                                 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 
+      *                                 reloaded every time this
       *                                 method is called.  Default
       *                                 behavior is to only load the
       *                                 configuration file if it had
@@ -420,18 +586,18 @@ class Template
       *                                 Should mostly be used for
       *                                 debugging.
       *
-      * @return mixed The desired setting's value or if not found, 
+      * @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, 
+    function get_template_config($template_set_id, $setting,
                                  $default=NULL, $live_config=FALSE) {
 
-        sqGetGlobalVar('template_configuration_settings', 
-                       $template_configuration_settings, 
-                       SQ_SESSION, 
+        sqGetGlobalVar('template_configuration_settings',
+                       $template_configuration_settings,
+                       SQ_SESSION,
                        array());
 
         if ($live_config) unset($template_configuration_settings[$template_set_id]);
@@ -445,33 +611,33 @@ class Template
            return $template_configuration_settings[$template_set_id][$setting];
 
 
-        // if template set configuration has been loaded, but this 
+        // 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), 
+        // 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_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: ("' 
+            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 
+            // 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 
+            // 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...
             //
@@ -488,11 +654,11 @@ class Template
             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] 
+                    $template_configuration_settings[$template_set_id][$variable]
                         = $$variable;
             }
 
-            sqsession_register($template_configuration_settings, 
+            sqsession_register($template_configuration_settings,
                                'template_configuration_settings');
 
             // NOTE: could use isset() instead of empty() below, but
@@ -508,13 +674,21 @@ class Template
 
     }
 
-    /** 
+    /**
       * 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 string  $template_set_id  The template set for which
+      *                                  the cache should be built.
+      *                                  This function will save more
+      *                                  than one set's files, so it
+      *                                  may be called multiple times
+      *                                  with different values for this
+      *                                  argument.  When regenerating,
+      *                                  all set caches are dumped.
       * @param boolean $regenerate_cache When TRUE, the file hierarchy
       *                                  is reloaded and stored fresh
       *                                  (optional; default FALSE).
@@ -525,12 +699,12 @@ class Template
       *                                  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 
+      *               are all the template file names for the given
+      *               template set ID (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
@@ -540,86 +714,76 @@ class Template
       * @static
       *
       */
-    function cache_template_file_hierarchy($regenerate_cache=FALSE,
+    function cache_template_file_hierarchy($template_set_id,
+                                           $regenerate_cache=FALSE,
                                            $additional_files=array()) {
 
-        sqGetGlobalVar('template_file_hierarchy', $template_file_hierarchy, 
+        sqGetGlobalVar('template_file_hierarchy', $template_file_hierarchy,
                        SQ_SESSION, array());
 
 
         if ($regenerate_cache) unset($template_file_hierarchy);
 
-
-        if (!empty($template_file_hierarchy)) {
+        if (!empty($template_file_hierarchy[$template_set_id])) {
 
             // have to add additional files if given before returning
             //
             if (!empty($additional_files)) {
-                $template_file_hierarchy = array_merge($template_file_hierarchy, 
-                                                       $additional_files);
+                $template_file_hierarchy[$template_set_id]
+                    = array_merge($template_file_hierarchy[$template_set_id],
+                                  $additional_files);
+
                 sqsession_register($template_file_hierarchy,
                                    'template_file_hierarchy');
             }
 
-            return $template_file_hierarchy;
+            return $template_file_hierarchy[$template_set_id];
         }
 
 
         // 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);
+        $template_file_hierarchy[$template_set_id] = Template::catalog_template_files($template_set_id);
 
-        } 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');
+        // additional files, if any
+        //
+        if (!empty($additional_files)) {
+            $template_file_hierarchy[$template_set_id]
+                = array_merge($template_file_hierarchy[$template_set_id],
+                              $additional_files);
+        }
 
-            return $template_file_hierarchy;
+        sqsession_register($template_file_hierarchy,
+                           'template_file_hierarchy');
 
-        }
+        return $template_file_hierarchy[$template_set_id];
 
     }
 
     /**
-      * Traverse template hierarchy and catalogue all template 
+      * 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 
+      *
+      * 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 
+      * @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
@@ -631,13 +795,19 @@ class Template
       */
     function catalog_template_files($template_set_id, $file_list=array(), $directory='') {
 
-        $template_base_dir = SM_PATH 
+        $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
@@ -649,7 +819,7 @@ class Template
         // 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, 
+        $engine = Template::get_template_config($template_set_id,
                                                 'template_engine', SQ_PHP_TEMPLATE);
         foreach ($files_and_dirs['FILES'] as $file) {
 
@@ -658,11 +828,13 @@ class Template
             //
             $relative_file = substr($file, strlen($template_base_dir));
 
-            // only put file in cache if not already found in earlier template
-            //
+            /**
+             * 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'   => $file,
+                                                     'PATH'   => substr($file,strlen(SM_PATH)),
                                                      'SET_ID' => $template_set_id,
                                                      'ENGINE' => $engine,
                                                   );
@@ -672,7 +844,7 @@ class Template
 
 
         // now if we are currently at the top-level of the template
-        // set base directory, we need to move on to the parent 
+        // set base directory, we need to move on to the parent
         // template set, if any
         //
         if ($directory == $template_base_dir) {
@@ -680,8 +852,8 @@ class Template
             // 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', 
+            $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
@@ -719,8 +891,8 @@ class Template
       * @param string $plugin          The name of the plugin
       * @param string $file            The name of the template file
       * @param string $template_set_id The ID of the template for which
-      *                                to start looking for the file 
-      *                                (optional; default is current 
+      *                                to start looking for the file
+      *                                (optional; default is current
       *                                template set ID).
       *
       * @return boolean TRUE if the template file was found, FALSE otherwise.
@@ -732,21 +904,23 @@ class Template
             $template_set_id = $this->template_set_id;
 
         $file_path = SM_PATH . 'plugins/' . $plugin . '/'
-                   . $this->calculate_template_file_directory($template_set_id) 
+                   . $this->calculate_template_file_directory($template_set_id)
                    . $file;
 
         if (file_exists($file_path)) {
             // FIXME: assuming PHP template engine may not necessarily be a good thing
-            $engine = $this->get_template_config($template_set_id, 
+            $engine = $this->get_template_config($template_set_id,
                                                  'template_engine', SQ_PHP_TEMPLATE);
             $file_list = array('plugins/' . $plugin . '/' . $file => array(
-                                                      'PATH'   => $file_path,
-                                                      'SET_ID' => $template_set_id,
-                                                      'ENGINE' => $engine,
+                                         'PATH'   => substr($file_path, strlen(SM_PATH)),
+                                         'SET_ID' => $template_set_id,
+                                         'ENGINE' => $engine,
                                                                           )
                               );
-            $this->template_file_cache 
-                = $this->cache_template_file_hierarchy(FALSE, $file_list);
+            $this->template_file_cache
+                = $this->cache_template_file_hierarchy($this->template_set_id,
+                                                       FALSE,
+                                                       $file_list);
             return TRUE;
         }
 
@@ -755,8 +929,8 @@ class Template
         // (use fallback when we run out of parents)
         //
         $fallback_id = $this->get_fallback_template_set();
-        $parent_id = $this->get_template_config($template_set_id, 
-                                                'parent_template_set', 
+        $parent_id = $this->get_template_config($template_set_id,
+                                                'parent_template_set',
                                                 $fallback_id);
 
         // were we already all the way to the last level? just exit
@@ -777,12 +951,12 @@ class Template
       * Find the right template file.
       *
       * The template file is taken from the template file cache, thus
-      * the file is taken from the current template, one of its 
+      * the file is taken from the current template, one of its
       * ancestors or the fallback template.
       *
       * Note that it is perfectly acceptable to load template files from
-      * template subdirectories.  For example, JavaScript templates found 
-      * in the js/ subdirectory would be loaded by passing 
+      * template subdirectories.  For example, JavaScript templates found
+      * in the js/ subdirectory would be loaded by passing
       * "js/<javascript file name>" as the $filename.
       *
       * Note that the caller can also ask for ALL files in a directory
@@ -796,28 +970,51 @@ class Template
       * before just returning nothing.
       *
       * Plugin authors must note that the $filename MUST be prefaced
-      * with "plugins/<plugin name>/" in order to correctly resolve the 
+      * with "plugins/<plugin name>/" in order to correctly resolve the
       * template file.
       *
       * @param string $filename The name of the template file,
-      *                         possibly prefaced with 
+      *                         possibly prefaced with
       *                         "plugins/<plugin name>/"
       *                         indicating that it is a plugin
-      *                         template, or ending with a 
+      *                         template, or ending with a
       *                         slash, indicating that all files
       *                         for that directory name should
       *                         be returned.
+      * @param boolean $directories_ok When TRUE, directory names
+      *                                are acceptable search values,
+      *                                and when returning a list of
+      *                                directory contents, sub-directory
+      *                                names will also be included
+      *                                (optional; default FALSE).
+      *                                NOTE that empty directories
+      *                                are NOT included in the cache!
+      * @param boolean $directories_only When TRUE, only directory names
+      *                                  are included in the returned
+      *                                  results.  (optional; default
+      *                                  FALSE).  Setting this argument
+      *                                  to TRUE forces $directories_ok
+      *                                  to TRUE as well.
+      *                                  NOTE that empty directories
+      *                                  are NOT included in the cache!
       *
       * @return mixed The full path to the template file or a list
       *               of all files in the given directory if $filename
       *               ends with a slash; if not found, an empty string
-      *               is returned.  The caller is responsible for 
-      *               throwing errors or other actions if template 
+      *               is returned.  The caller is responsible for
+      *               throwing errors or other actions if template
       *               file is not found.
       *
       */
-    function get_template_file_path($filename) {
+    function get_template_file_path($filename,
+                                    $directories_ok=FALSE,
+                                    $directories_only=FALSE) {
+
+        if ($directories_only) $directories_ok = TRUE;
+
 
+        // only looking for directory listing first...
+        //
         // return list of all files in a directory (and that
         // of any ancestors)
         //
@@ -829,34 +1026,77 @@ class Template
                 // only want files in the requested directory
                 // (AND not in a subdirectory!)
                 //
-                if (strpos($file, $filename) === 0 
+                if (!$directories_only && strpos($file, $filename) === 0
                  && strpos($file, '/', strlen($filename)) === FALSE)
-                    $return_array[] = $file_info['PATH'];
+                    $return_array[] = SM_PATH . $file_info['PATH'];
+
+                // directories too?  detect by finding any
+                // array key that matches a file in a sub-directory
+                // of the directory being processed
+                //
+                if ($directories_ok && strpos($file, $filename) === 0
+                 && ($pos = strpos($file, '/', strlen($filename))) !== FALSE
+                 && strpos($file, '/', $pos + 1) === FALSE) {
+                    $directory_name = SM_PATH
+                                    . substr($file_info['PATH'],
+                                             0,
+                                             strrpos($file_info['PATH'], '/'));
+                    if (!in_array($directory_name, $return_array))
+                        $return_array[] = $directory_name;
+                }
 
             }
             return $return_array;
 
         }
 
+
+        // just looking for singular file or directory below...
+        //
         // figure out what to do with files not found
         //
-        if (empty($this->template_file_cache[$filename]['PATH'])) {
+        if ($directories_only || empty($this->template_file_cache[$filename]['PATH'])) {
+
+            // if looking for directories...
+            // have to iterate through cache and detect
+            // directory by matching any file inside of it
+            //
+            if ($directories_ok) {
+                foreach ($this->template_file_cache as $file => $file_info) {
+                    if (strpos($file, $filename) === 0
+                     && ($pos = strpos($file, '/', strlen($filename))) !== FALSE
+                     && strpos($file, '/', $pos + 1) === FALSE) {
+                        return SM_PATH . substr($file_info['PATH'],
+                                                0,
+                                                strrpos($file_info['PATH'], '/'));
+                    }
+                }
+
+                if ($directories_only) return '';
+            }
 
-            // plugins get one more chance below; any other
-            // files we just give up now
+            // plugins get one more chance
             //
-            if (strpos($filename, 'plugins/') !== 0) 
-                return '';
+            if (strpos($filename, 'plugins/') === 0) {
 
-            $plugin_name = substr($filename, 8, strpos($filename, '/', 8) - 8);
-            $file = substr($filename, strlen($plugin_name) + 9);
+                $plugin_name = substr($filename, 8, strpos($filename, '/', 8) - 8);
+                $file = substr($filename, strlen($plugin_name) + 9);
 
-            if (!$this->find_and_cache_plugin_template_file($plugin_name, $file))
-                return '';
+                if (!$this->find_and_cache_plugin_template_file($plugin_name, $file))
+                    return '';
+                //FIXME: technically I guess we should check for directories
+                //       here too, but that's overkill (no need) presently
+                //       (plugin-provided alternate stylesheet dirs?!?  bah.)
+
+            }
+
+            // nothing... return empty string (yes, the else is intentional!)
+            //
+            else return '';
 
         }
 
-        return $this->template_file_cache[$filename]['PATH'];
+        return SM_PATH . $this->template_file_cache[$filename]['PATH'];
 
     }
 
@@ -871,17 +1111,17 @@ class Template
       * @param string $filename The name of the template file,
       *
       * @return object The needed template object to render the template.
-      * 
+      *
       */
     function get_rendering_template_engine_object($filename) {
-        
+
         // for files that we cannot find engine info for,
         // just return $this
-        //  
+        //
         if (empty($this->template_file_cache[$filename]['ENGINE']))
             return $this;
 
-            
+
         // otherwise, compare $this' engine to the file's engine
         //
         $engine = $this->template_file_cache[$filename]['ENGINE'];
@@ -891,10 +1131,10 @@ class Template
 
         // need to load another engine... if already instantiated,
         // and stored herein, return that
-        // FIXME: this assumes same engine setup in all template 
+        // FIXME: this assumes same engine setup in all template
         //        set config files that have same engine in common
         //        (but keeping a separate class object for every
-        //        template set seems like overkill... for now we 
+        //        template set seems like overkill... for now we
         //        won't do that unless it becomes a problem)
         //
         if (!empty($this->other_template_engine_objects[$engine])) {
@@ -927,10 +1167,10 @@ class Template
 
     }
 
-    /** 
+    /**
       * Return all JavaScript files provided by the template.
-      * 
-      * All files found in the template set's "js" directory (and 
+      *
+      * All files found in the template set's "js" directory (and
       * that of its ancestors) with the extension ".js" are returned.
       *
       * @param boolean $full_path When FALSE, only the file names
@@ -938,13 +1178,13 @@ class Template
       *                           otherwise, path information is
       *                           included (relative to SM_PATH)
       *                           (OPTIONAL; default only file names)
-      *     
+      *
       * @return array The required file names/paths.
-      *     
+      *
       */
     function get_javascript_includes($full_path=FALSE) {
 
-        // since any page from a parent template set 
+        // since any page from a parent template set
         // could end up being loaded, we have to load
         // all js files from ancestor template sets,
         // not just this set
@@ -953,13 +1193,13 @@ class Template
         //$js_files = list_files($directory, '.js', !$full_path);
         //
         $js_files = $this->get_template_file_path('js/');
-        
-        
+
+
         // parse out .js files only
         //
         $return_array = array();
         foreach ($js_files as $file) {
-    
+
             if (substr($file, strlen($file) - 3) != '.js') continue;
 
             if ($full_path) {
@@ -975,16 +1215,15 @@ class Template
     }
 
     /**
-      * Return all alternate stylesheets provided by template.  
+      * Return all alternate stylesheets provided by template.
       *
-      * All files found in the template set's "css/alternates" 
-      * directory (and that of its ancestors) with the extension
-      * ".css" are returned.
+      * All (non-empty) directories found in the template set's
+      * "css/alternates" directory (and that of its ancestors)
+      * are returned.
       *
       * Note that prettified names are constructed herein by
-      * taking the file name, changing underscores to spaces,
-      * removing the ".css" from the end of the file, and 
-      * capitalizing each word in the resultant name.
+      * taking the directory name, changing underscores to spaces
+      * and capitalizing each word in the resultant name.
       *
       * @param boolean $full_path When FALSE, only the file names
       *                           are included in the return array;
@@ -993,38 +1232,38 @@ class Template
       *                           (OPTIONAL; default only file names)
       *
       * @return array A list of the available alternate stylesheets,
-      *               where the keys are the file names (formatted 
-      *               according to $full_path) for the stylesheets, 
-      *               and the values are the prettified version of 
+      *               where the keys are the file names (formatted
+      *               according to $full_path) for the stylesheets,
+      *               and the values are the prettified version of
       *               the file names for display to the user.
-      *               
+      *
       */
     function get_alternative_stylesheets($full_path=FALSE) {
 
         // since any page from a parent template set
         // could end up being loaded, we will load
-        // all alternate css files from ancestor 
+        // all alternate css files from ancestor
         // template sets, not just this set
         //
-        //$directory = SM_PATH . $this->get_template_file_directory() . 'css/alternates';
-        //$css_files = list_files($directory, '.css', !$full_path);
-        //
-        $css_files = $this->get_template_file_path('css/alternates/');
+        $css_directories = $this->get_template_file_path('css/alternates/', TRUE, TRUE);
 
 
-        // parse out .css files only
+        // prettify names
         //
         $return_array = array();
-        foreach ($css_files as $file) {
+        foreach ($css_directories as $directory) {
 
-            if (substr($file, strlen($file) - 4) != '.css') continue;
+            // CVS and SVN directories are not wanted
+            //
+            if ((strpos($directory, '/CVS') === strlen($directory) - 4)
+             || (strpos($directory, '/.svn') === strlen($directory) - 5)) continue;
 
-            $pretty_name = ucwords(str_replace('_', ' ', substr(basename($file), 0, -4)));
+            $pretty_name = ucwords(str_replace('_', ' ', basename($directory)));
 
             if ($full_path) {
-                $return_array[$file] = $pretty_name;
+                $return_array[$directory] = $pretty_name;
             } else {
-                $return_array[basename($file)] = $pretty_name;
+                $return_array[basename($directory)] = $pretty_name;
             }
 
         }
@@ -1034,10 +1273,10 @@ class Template
     }
 
     /**
-      * Return all standard stylsheets provided by the template.  
+      * Return all standard stylsheets provided by the template.
       *
       * All files found in the template set's "css" directory (and
-      * that of its ancestors) with the extension ".css" except 
+      * that of its ancestors) with the extension ".css" except
       * "rtl.css" (which is dealt with separately) are returned.
       *
       * @param boolean $full_path When FALSE, only the file names
@@ -1051,7 +1290,7 @@ class Template
       */
     function get_stylesheets($full_path=FALSE) {
 
-        // since any page from a parent template set 
+        // since any page from a parent template set
         // could end up being loaded, we have to load
         // all css files from ancestor template sets,
         // not just this set
@@ -1062,7 +1301,7 @@ class Template
         $css_files = $this->get_template_file_path('css/');
 
 
-        // need to leave out "rtl.css" 
+        // need to leave out "rtl.css"
         //
         $return_array = array();
         foreach ($css_files as $file) {
@@ -1080,8 +1319,8 @@ class Template
 
 
         // return sheets for the current template set
-        // last so we can enable any custom overrides 
-        // of styles in ancestor sheets 
+        // last so we can enable any custom overrides
+        // of styles in ancestor sheets
         //
         return array_reverse($return_array);
 
@@ -1090,7 +1329,7 @@ class Template
     /**
       * Generate links to all this template set's standard stylesheets
       *
-      * Subclasses can override this function if stylesheets are 
+      * Subclasses can override this function if stylesheets are
       * created differently for the template set's target output
       * interface.
       *
@@ -1107,18 +1346,18 @@ class Template
     }
 
     /**
-      * Push out any other stylesheet links as provided (for 
+      * Push out any other stylesheet links as provided (for
       * stylesheets not included with the current template set)
       *
-      * Subclasses can override this function if stylesheets are 
+      * Subclasses can override this function if stylesheets are
       * created differently for the template set's target output
       * interface.
       *
-      * @param mixed $sheets List of the desired stylesheets 
+      * @param mixed $sheets List of the desired stylesheets
       *                      (file path to be used in stylesheet
-      *                      href attribute) to output (or single 
+      *                      href attribute) to output (or single
       *                      stylesheet file path).
-FIXME: We could make the incoming array more complex so it can 
+FIXME: We could make the incoming array more complex so it can
        also contain the other parameters for create_css_link()
        such as $name, $alt, $mtype, and $xhtml_end
        But do we need to?
@@ -1145,32 +1384,40 @@ FIXME: We could make the incoming array more complex so it can
       * Send HTTP header(s) to browser.
       *
       * Subclasses can override this function if headers are
-      * managed differently in the template set's target output
+      * managed differently in the engine's target output
       * interface.
       *
-      * @param mixed $headers A list of (or a single) header 
+      * @param mixed $headers A list of (or a single) header
       *                       text to be sent.
+      * @param boolean $replace Whether or not to replace header(s)
+      *                         previously sent header(s) of the
+      *                         same type (this parameter may be
+      *                         ignored in some implementations
+      *                         of this class if the target interface
+      *                         does not support this functionality)
+      *                         (OPTIONAL; default = TRUE, always replace).
       *
       */
-    function header($headers)
+    function header($headers, $replace=TRUE)
     {
 
         if (!is_array($headers)) $headers = array($headers);
 
         foreach ($headers as $header) {
-            header($header);
+            $this->assign('header', $header);
+            header($this->fetch('header.tpl'), $replace);
         }
 
     }
 
     /**
-      * Generate a link to the right-to-left stylesheet for 
+      * Generate a link to the right-to-left stylesheet for
       * this template set by getting the "rtl.css" file from
       * this template set, its parent (or grandparent, etc.)
-      * template set, the fall-back template set, or finally, 
+      * template set, the fall-back template set, or finally,
       * fall back to SquirrelMail's own "rtl.css" if need be.
       *
-      * Subclasses can override this function if stylesheets are 
+      * Subclasses can override this function if stylesheets are
       * created differently for the template set's target output
       * interface.
       *
@@ -1223,7 +1470,7 @@ FIXME: We could make the incoming array more complex so it can
         $template = $this->get_template_file_path($file);
 
 
-        // special case stylesheet.tpl falls back to SquirrelMail's 
+        // special case stylesheet.tpl falls back to SquirrelMail's
         // own default stylesheet
         //
         if (empty($template) && $file == 'css/stylesheet.tpl') {
@@ -1239,8 +1486,9 @@ FIXME: We could make the incoming array more complex so it can
         } else {
 
             $aPluginOutput = array();
+            $temp = array(&$aPluginOutput, &$this);
             $aPluginOutput = concat_hook_function('template_construct_' . $file,
-                                                  array($aPluginOutput, $this));
+                                                  $temp, TRUE);
             $this->assign('plugin_output', $aPluginOutput);
 
             //$output = $this->apply_template($template);
@@ -1252,7 +1500,7 @@ FIXME: We could make the incoming array more complex so it can
             // using this hook will probably be rejected by the
             // SquirrelMail team.
             //
-            $output = filter_hook_function('template_output', $output);
+            do_hook('template_output', $output);
 
             return $output;
 
@@ -1370,7 +1618,7 @@ FIXME: We could make the incoming array more complex so it can
       * Note: this is an abstract method that must be implemented by subclass.
       *
       * @param string $filepath The full file path to the template to be applied
-      * 
+      *
       * @return string The output for the given template
       *
       */