Happy 2015
[squirrelmail.git] / class / template / Template.class.php
index a8d43f9ff282dc49e37a04d08f3a94323179ae15..10b0ca679b4b2875451aa0b4fa9756c15955f80a 100644 (file)
@@ -2,13 +2,13 @@
 /**
   * 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 © 2003-2006 The SquirrelMail Project Team
+  *
+  * @copyright 2003-2015 The SquirrelMail Project Team
   * @license http://opensource.org/licenses/gpl-license.php GNU Public License
   * @version $Id$
   * @package squirrelmail
@@ -18,7 +18,7 @@
   */
 
 /** load template functions */
-require(SM_PATH . 'functions/template.php');
+require(SM_PATH . 'functions/template/general_util.php');
 
 /**
   * The SquirrelMail Template class.
@@ -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,9 +242,9 @@ 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) 
+        $templateset_fallback = (!isset($templateset_fallback)
                                  ? $default : $templateset_fallback);
 
         // iterate through all template sets, is this a valid skin ID?
@@ -282,7 +295,7 @@ class Template
         global $templateset_default, $aTemplateSet;
         $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet)
                          ? array() : $aTemplateSet);
-        $templateset_default = (!isset($templateset_default) 
+        $templateset_default = (!isset($templateset_default)
                                  ? $default : $templateset_default);
 
         // iterate through all template sets, is this a valid skin ID?
@@ -306,13 +319,122 @@ class Template
 
     }
 
+    /**
+      * 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);
+
+        // 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);
+
+    }
+
     /**
       * 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 
+      *                                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.
@@ -322,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);
         }
@@ -342,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)
@@ -360,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)
@@ -403,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).
       *
       */
@@ -417,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.
@@ -429,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
@@ -452,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]);
@@ -477,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...
             //
@@ -520,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
@@ -540,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).
@@ -557,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
@@ -572,85 +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)) {
+        $template_file_hierarchy[$template_set_id] = Template::catalog_template_files($template_set_id);
 
-            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');
+        // 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
@@ -662,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
@@ -680,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) {
 
@@ -705,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) {
@@ -713,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
@@ -752,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.
@@ -765,12 +904,12 @@ 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'   => substr($file_path, strlen(SM_PATH)),
@@ -778,8 +917,10 @@ class Template
                                          '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;
         }
 
@@ -788,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
@@ -810,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
@@ -829,14 +970,14 @@ 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.
@@ -846,27 +987,27 @@ class Template
       *                                directory contents, sub-directory
       *                                names will also be included
       *                                (optional; default FALSE).
-      *                                NOTE that empty directories 
+      *                                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 
+      *                                  results.  (optional; default
       *                                  FALSE).  Setting this argument
-      *                                  to TRUE forces $directories_ok 
+      *                                  to TRUE forces $directories_ok
       *                                  to TRUE as well.
-      *                                  NOTE that empty directories 
+      *                                  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, 
-                                    $directories_ok=FALSE, 
+    function get_template_file_path($filename,
+                                    $directories_ok=FALSE,
                                     $directories_only=FALSE) {
 
         if ($directories_only) $directories_ok = TRUE;
@@ -885,7 +1026,7 @@ class Template
                 // only want files in the requested directory
                 // (AND not in a subdirectory!)
                 //
-                if (!$directories_only && strpos($file, $filename) === 0 
+                if (!$directories_only && strpos($file, $filename) === 0
                  && strpos($file, '/', strlen($filename)) === FALSE)
                     $return_array[] = SM_PATH . $file_info['PATH'];
 
@@ -896,9 +1037,9 @@ class Template
                 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, 
+                    $directory_name = SM_PATH
+                                    . substr($file_info['PATH'],
+                                             0,
                                              strrpos($file_info['PATH'], '/'));
                     if (!in_array($directory_name, $return_array))
                         $return_array[] = $directory_name;
@@ -925,8 +1066,8 @@ class Template
                     if (strpos($file, $filename) === 0
                      && ($pos = strpos($file, '/', strlen($filename))) !== FALSE
                      && strpos($file, '/', $pos + 1) === FALSE) {
-                        return SM_PATH . substr($file_info['PATH'], 
-                                                0, 
+                        return SM_PATH . substr($file_info['PATH'],
+                                                0,
                                                 strrpos($file_info['PATH'], '/'));
                     }
                 }
@@ -934,7 +1075,7 @@ class Template
                 if ($directories_only) return '';
             }
 
-            // plugins get one more chance 
+            // plugins get one more chance
             //
             if (strpos($filename, 'plugins/') === 0) {
 
@@ -970,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'];
@@ -990,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])) {
@@ -1026,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
@@ -1037,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
@@ -1052,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) {
@@ -1074,10 +1215,10 @@ class Template
     }
 
     /**
-      * Return all alternate stylesheets provided by template.  
+      * Return all alternate stylesheets provided by template.
       *
-      * All (non-empty) directories found in the template set's 
-      * "css/alternates" directory (and that of its ancestors) 
+      * 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
@@ -1091,17 +1232,17 @@ 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
         //
         $css_directories = $this->get_template_file_path('css/alternates/', TRUE, TRUE);
@@ -1112,9 +1253,10 @@ class Template
         $return_array = array();
         foreach ($css_directories as $directory) {
 
-            // CVS directories are not wanted
+            // CVS and SVN directories are not wanted
             //
-            if (strpos($directory, '/CVS') === strlen($directory) - 4) continue;
+            if ((strpos($directory, '/CVS') === strlen($directory) - 4)
+             || (strpos($directory, '/.svn') === strlen($directory) - 5)) continue;
 
             $pretty_name = ucwords(str_replace('_', ' ', basename($directory)));
 
@@ -1131,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
@@ -1148,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
@@ -1159,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) {
@@ -1177,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);
 
@@ -1187,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.
       *
@@ -1204,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?
@@ -1242,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.
       *
@@ -1320,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') {
@@ -1330,14 +1480,15 @@ FIXME: We could make the incoming array more complex so it can
 
         if (empty($template)) {
 
-            trigger_error('The template "' . htmlspecialchars($file)
+            trigger_error('The template "' . sm_encode_html_special_chars($file)
                           . '" could not be fetched!', E_USER_ERROR);
 
         } 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);
@@ -1349,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;
 
@@ -1467,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
       *
       */