changeset 710:6d354f3a8d90

Replaced class.FastTemplate.php with class.rFastTemplate.php in contrib/web/php-admin (Christoph Thiel)
author mortenp
date Mon, 11 Jan 2010 00:20:03 +1100
parents 2cb6a707f2bc
children ad31ac396459
files ChangeLog contrib/web/php-admin/htdocs/class.FastTemplate.php contrib/web/php-admin/htdocs/class.rFastTemplate.php contrib/web/php-admin/htdocs/edit.php contrib/web/php-admin/htdocs/index.php contrib/web/php-admin/htdocs/save.php
diffstat 6 files changed, 1096 insertions(+), 743 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Mon Dec 21 17:49:16 2009 +1100
+++ b/ChangeLog	Mon Jan 11 00:20:03 2010 +1100
@@ -1,3 +1,6 @@
+ o Replaced class.FastTemplate.php with class.rFastTemplate.php in
+   contrib/web/php-admin (Christoph Thiel)
+1.2.17-RC1
  o Added information about digest and nomail to listhelp (Robin H. Johnson)
  o Fixed bug in mlmmj-maintd which caused loss of archive files in some
    requeue cases (Robin H. Johnson)
--- a/contrib/web/php-admin/htdocs/class.FastTemplate.php	Mon Dec 21 17:49:16 2009 +1100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,737 +0,0 @@
-<?php
-
-/* class.FastTemplate.php
- *
- * Original Perl module CGI::FastTemplate by Jason Moore
- * Copyright (c) 1998, Jason Moore <jmoore at sober dot com>
- *
- * PHP3 port by CDI
- * Copyright (c) 1999 CDI <cdi at thewebmasters dot net>
- *
- * PHP4 support added by Christoph Thiel for mlmmj's php-admin
- * Copyright (c) 2004 Christoph Thiel <ct at kki dot org>
- *
- * This program is released under the General Artistic License.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the GNU General Artistic License, with the following stipulations;
- *
- * Changes or modifications must retain these Copyright statements. Changes or
- * modifications must be submitted to all AUTHORS.
- *
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the Artistic License for more
- * details. This software is distributed AS-IS.
- */
-
-class FastTemplate {
-
-    var $FILELIST = array();   /* Holds the array of filehandles
-				* FILELIST[HANDLE] == "fileName" 
-				*/
-
-    var $DYNAMIC = array();    /* Holds the array of dynamic
-				* blocks, and the fileHandles they
-				* live in. 
-				*/
-
-    var $PARSEVARS = array();	/* Holds the array of Variable
-				 * handles.
-				 * PARSEVARS[HANDLE] == "value"
-				 */
-
-    var $LOADED	= array();	/* We only want to load a template
-				 * once - when it's used.
-				 * LOADED[FILEHANDLE] == 1 if loaded
-				 * undefined if not loaded yet.
-				 */
-   
-    var	$HANDLE	= array();	/* Holds the handle names assigned
-				 * by a call to parse()
-				 */
-    
-    var	$ROOT =	"";             /* Holds path-to-templates */
-    
-    var $ERROR = "";		/* Holds the last error message */
-    
-    var $LAST =	"";		/* Holds the HANDLE to the last
-				 * template parsed by parse()
-				 */
-    
-    var $STRICT	= true;		/* Strict template checking.
-				 * Unresolved vars in templates will
-				 * generate a warning when found.
-				 */
-    
-/******************************************************************************/
-    
-    function FastTemplate($pathToTemplates = "")
-	{
-	    global $php_errormsg;
-	    
-	    if(!empty($pathToTemplates))
-	    {
-		$this->set_root($pathToTemplates);
-	    }
-	    
-	}
-    
-    /* 
-     * All templates will be loaded from this "root" directory
-     * Can be changed in mid-process by re-calling with a new
-     * value.
-     */
-    
-    function set_root($root)
-	{
-	    $trailer = substr($root,-1);
-	    
-	    if( (ord($trailer)) != 47 )
-	    {
-		$root = "$root". chr(47);
-	    }
-	    
-	    if(is_dir($root))
-	    {
-		$this->ROOT = $root;
-	    }
-	    else
-	    {
-		$this->ROOT = "";
-		$this->error("Specified ROOT dir [$root] is not a directory");
-	    }
-	}
-
-
-    /* Calculates current microtime
-     * I throw this into all my classes for benchmarking purposes
-     * It's not used by anything in this class and can be removed
-     * if you don't need it.
-     */
-
-    function utime()
-	{
-	    $time = explode( " ", microtime());
-	    $usec = (double)$time[0];
-	    $sec = (double)$time[1];
-	    return $sec + $usec;
-	}
-
-
-    /* Strict template checking, if true sends warnings to STDOUT when
-     * parsing a template with undefined variable references
-     * Used for tracking down bugs-n-such. Use no_strict() to disable.
-     */
-    
-    function strict()
-	{
-	    $this->STRICT = true;
-	}
-
-    
-    /* Silently discards (removes) undefined variable references
-     * found in templates
-     */
-    
-    function no_strict()
-	{
-	    $this->STRICT = false;
-	}
-
-
-    /* A quick check of the template file before reading it.
-     * This is -not- a reliable check, mostly due to inconsistencies
-     * in the way PHP determines if a file is readable.
-     */
-    
-    function is_safe($filename)
-	{
-	    if(!file_exists($filename))
-	    {
-		$this->error("[$filename] does not exist",0);
-		return false;
-	    }
-	    return true;
-	}
-
-
-    /* Grabs a template from the root dir and 
-     * reads it into a (potentially REALLY) big string
-     */
-
-    function get_template($template)
-	{
-	    if(empty($this->ROOT))
-	    {
-		$this->error("Cannot open template. Root not valid.",1);
-		return false;
-	    }
-	    
-	    $filename	=	"$this->ROOT"."$template";
-	    
-	    $contents = implode("",(@file($filename)));
-	    if( (!$contents) or (empty($contents)) )
-	    {
-		$this->error("get_template() failure: [$filename] $php_errormsg",1);
-	    }
-	    
-	    return $contents;
-	    
-	}
-
-
-    /* Prints the warnings for unresolved variable references
-     * in template files. Used if STRICT is true
-     */
-
-    function show_unknowns($Line)
-	{
-	    $unknown = array();
-	    if (ereg("(\{[A-Z0-9_]+\})",$Line,$unknown))
-	    {
-		$UnkVar = $unknown[1];
-		if(!(empty($UnkVar)))
-		{
-		    @error_log("[FastTemplate] Warning: no value found for variable: $UnkVar ",0);
-		}
-	    }
-	}
-    
-
-    /* This routine get's called by parse() and does the actual
-     * {VAR} to VALUE conversion within the template.
-     */
-
-    function parse_template($template, $tpl_array)
-	{
-	    while ( list ($key,$val) = each ($tpl_array) )
-	    {
-		if (!(empty($key)))
-		{
-		    if(gettype($val) != "string")
-		    {
-			settype($val,"string");
-		    }
-		    
-		    $template = ereg_replace("\{$key\}","$val","$template");
-		    //$template = str_replace("{$key}","$val","$template");
-		}
-	    }
-	    
-	    if(!$this->STRICT)
-	    {
-		// Silently remove anything not already found
-		
-		$template = ereg_replace("\{([A-Z0-9_]+)\}","",$template);
-	    }
-	    else
-	    {
-		// Warn about unresolved template variables
-		if (ereg("({[A-Z0-9_]+})",$template))
-		{
-		    $unknown = split("\n",$template);
-		    while (list ($Element,$Line) = each($unknown) )
-		    {
-			$UnkVar = $Line;
-			if(!(empty($UnkVar)))
-			{
-			    $this->show_unknowns($UnkVar);
-			}
-		    }
-		}
-	    }
-	    return $template;
-	    
-	}
-    
-
-    /* The meat of the whole class. The magic happens here. */
-
-    function parse($ReturnVar, $FileTags)
-	{
-	    $append = false;
-	    $this->LAST = $ReturnVar;
-	    $this->HANDLE[$ReturnVar] = 1;
-	    
-	    if (gettype($FileTags) == "array")
-	    {
-		unset($this->$ReturnVar);	// Clear any previous data
-		
-		while ( list ( $key , $val ) = each ( $FileTags ) )
-		{
-		    if ( (!isset($this->$val)) || (empty($this->$val)) )
-		    {
-			$this->LOADED["$val"] = 1;
-			if(isset($this->DYNAMIC["$val"]))
-			{
-			    $this->parse_dynamic($val,$ReturnVar);
-			}
-			else
-			{
-			    $fileName = $this->FILELIST["$val"];
-			    $this->$val = $this->get_template($fileName);
-			}
-		    }
-		    
-		    //	Array context implies overwrite
-		    
-		    $this->$ReturnVar = $this->parse_template($this->$val,$this->PARSEVARS);
-		    
-		    //	For recursive calls.
-		    
-		    $this->assign( array( $ReturnVar => $this->$ReturnVar ) );
-		    
-		}
-	    }	// end if FileTags is array()
-	    else
-	    {
-		// FileTags is not an array
-		
-		$val = $FileTags;
-		
-		if( (substr($val,0,1)) == '.' )
-		{
-		    // Append this template to a previous ReturnVar
-		    
-		    $append = true;
-		    $val = substr($val,1);
-		}
-		
-		if ( (!isset($this->$val)) || (empty($this->$val)) )
-		{
-		    $this->LOADED["$val"] = 1;
-		    if(isset($this->DYNAMIC["$val"]))
-		    {
-			$this->parse_dynamic($val,$ReturnVar);
-		    }
-		    else
-		    {
-			$fileName = $this->FILELIST["$val"];
-			$this->$val = $this->get_template($fileName);
-		    }
-		}
-		
-		if($append)
-		{
-		    $this->$ReturnVar .= $this->parse_template($this->$val,$this->PARSEVARS);
-		}
-		else
-		{
-		    $this->$ReturnVar = $this->parse_template($this->$val,$this->PARSEVARS);
-		}
-		
-		//	For recursive calls.
-		
-		$this->assign(array( $ReturnVar => $this->$ReturnVar) );
-		
-	    }
-	    return;
-	}
-    
-    
-    function FastPrint($template = "")
-	{
-	    if(empty($template))
-	    {
-		$template = $this->LAST;
-	    }
-	    
-	    if( (!(isset($this->$template))) || (empty($this->$template)) )
-	    {
-		$this->error("Nothing parsed, nothing printed",0);
-		return;
-	    }
-	    else
-	    {
-		print $this->$template;
-	    }
-	    return;
-	}
-
-
-    function fetch($template = "")
-	{
-	    if(empty($template))
-	    {
-		$template = $this->LAST;
-	    }
-	    if( (!(isset($this->$template))) || (empty($this->$template)) )
-	    {
-		$this->error("Nothing parsed, nothing printed",0);
-		return "";
-	    }
-	    
-	    return($this->$template);
-	}
-
-
-    function define_dynamic($Macro, $ParentName)
-	{
-	    //	A dynamic block lives inside another template file.
-	    //	It will be stripped from the template when parsed
-	    //	and replaced with the {$Tag}.
-	    
-	    $this->DYNAMIC["$Macro"] = $ParentName;
-	    return true;
-	}
-    
-
-    function parse_dynamic($Macro,$MacroName)
-	{
-	    // The file must already be in memory.
-	    
-	    $ParentTag = $this->DYNAMIC["$Macro"];
-	    if( (!$this->$ParentTag) or (empty($this->$ParentTag)) )
-	    {
-		$fileName = $this->FILELIST[$ParentTag];
-		$this->$ParentTag = $this->get_template($fileName);
-		$this->LOADED[$ParentTag] = 1;
-	    }
-	    if($this->$ParentTag)
-	    {
-		$template = $this->$ParentTag;
-		$DataArray = split("\n",$template);
-		$newMacro = "";
-		$newParent = "";
-		$outside = true;
-		$start = false;
-		$end = false;
-		while ( list ($lineNum,$lineData) = each ($DataArray) )
-		{
-		    $lineTest = trim($lineData);
-		    if("<!-- BEGIN DYNAMIC BLOCK: $Macro -->" == "$lineTest" )
-		    {
-			$start = true;
-			$end = false;
-			$outside = false;
-		    }
-		    if("<!-- END DYNAMIC BLOCK: $Macro -->" == "$lineTest" )
-		    {
-			$start = false;
-			$end = true;
-			$outside = true;
-		    }
-		    if( (!$outside) and (!$start) and (!$end) )
-		    {
-			$newMacro .= "$lineData\n"; // Restore linebreaks
-		    }
-		    if( ($outside) and (!$start) and (!$end) )
-		    {
-			$newParent .= "$lineData\n"; // Restore linebreaks
-		    }
-		    if($end)
-		    {
-			$newParent .= "{$MacroName}\n";
-		    }
-		    // Next line please
-		    if($end) { $end = false; }
-		    if($start) { $start = false; }
-		}	// end While
-		
-		$this->$Macro = $newMacro;
-		$this->$ParentTag = $newParent;
-		return true;
-		
-	    }	// $ParentTag NOT loaded - MAJOR oopsie
-	    else
-	    {
-		@error_log("ParentTag: [$ParentTag] not loaded!",0);
-		$this->error("ParentTag: [$ParentTag] not loaded!",0);
-	    }
-	    return false;
-	}
-    
-    
-    /* Strips a DYNAMIC BLOCK from a template. */
-    
-    function clear_dynamic($Macro="")
-	{
-	    if(empty($Macro)) { return false; }
-	    
-	    // The file must already be in memory.
-	    
-	    $ParentTag = $this->DYNAMIC["$Macro"];
-	    
-	    if( (!$this->$ParentTag) or (empty($this->$ParentTag)) )
-	    {
-		$fileName = $this->FILELIST[$ParentTag];
-		$this->$ParentTag = $this->get_template($fileName);
-		$this->LOADED[$ParentTag] = 1;
-	    }
-	    
-	    if($this->$ParentTag)
-	    {
-		$template = $this->$ParentTag;
-		$DataArray = split("\n",$template);
-		$newParent = "";
-		$outside = true;
-		$start = false;
-		$end = false;
-		while ( list ($lineNum,$lineData) = each ($DataArray) )
-		{
-		    $lineTest = trim($lineData);
-		    if("<!-- BEGIN DYNAMIC BLOCK: $Macro -->" == "$lineTest" )
-		    {
-			$start = true;
-			$end = false;
-			$outside = false;
-		    }
-		    if("<!-- END DYNAMIC BLOCK: $Macro -->" == "$lineTest" )
-		    {
-			$start = false;
-			$end = true;
-			$outside = true;
-		    }
-		    if( ($outside) and (!$start) and (!$end) )
-		    {
-			$newParent .= "$lineData\n"; // Restore linebreaks
-		    }
-		    // Next line please
-		    if($end) { $end = false; }
-		    if($start) { $start = false; }
-		}	// end While
-		
-		$this->$ParentTag = $newParent;
-		return true;
-		
-	    }	// $ParentTag NOT loaded - MAJOR oopsie
-	    else
-	    {
-		@error_log("ParentTag: [$ParentTag] not loaded!",0);
-		$this->error("ParentTag: [$ParentTag] not loaded!",0);
-	    }
-	    return false;
-	}
-    
-    
-    function define($fileList)
-	{
-	    while ( list ($FileTag,$FileName) = each ($fileList) )
-	    {
-		$this->FILELIST["$FileTag"] = $FileName;
-	    }
-	    return true;
-	}
-    
-    
-    function clear_parse($ReturnVar = "")
-	{
-	    $this->clear($ReturnVar);
-	}
-    
-    
-    function clear($ReturnVar="")
-	{
-	    // Clears out hash created by call to parse()
-	    
-	    if(!empty($ReturnVar))
-	    {
-		if( (gettype($ReturnVar)) != "array")
-		{
-		    unset($this->$ReturnVar);
-		    return;
-		}
-		else
-		{
-		    while ( list ($key,$val) = each ($ReturnVar) )
-		    {
-			unset($this->$val);
-		    }
-		    return;
-		}
-	    }
-	    
-	    // Empty - clear all of them
-	    
-	    while ( list ( $key,$val) = each ($this->HANDLE) )
-	    {
-		$KEY = $key;
-		unset($this->$KEY);
-	    }
-	    return;
-	    
-	}
-    
-
-    function clear_all ()
-	{
-	    $this->clear();
-	    $this->clear_assign();
-	    $this->clear_define();
-	    $this->clear_tpl();
-	    
-	    return;
-	    
-	}
-
-
-    function clear_tpl ($fileHandle = "")
-	{
-	    if(empty($this->LOADED))
-	    {
-		// Nothing loaded, nothing to clear
-		
-		return true;
-	    }
-	    if(empty($fileHandle))
-	    {
-		// Clear ALL fileHandles
-		
-		while ( list ($key, $val) = each ($this->LOADED) )
-		{
-		    unset($this->$key);
-		}
-		unset($this->LOADED);
-		
-		return true;
-	    }
-	    else
-	    {
-		if( (gettype($fileHandle)) != "array")
-		{
-		    if( (isset($this->$fileHandle)) || (!empty($this->$fileHandle)) )
-		    {
-			unset($this->LOADED[$fileHandle]);
-			unset($this->$fileHandle);
-			return true;
-		    }
-		}
-		else
-		{
-		    while ( list ($Key, $Val) = each ($fileHandle) )
-		    {
-			unset($this->LOADED[$Key]);
-			unset($this->$Key);
-		    }
-		    return true;
-		}
-	    }
-	    
-	    return false;
-	    
-	}
-    
-    
-    function clear_define ( $FileTag = "" )
-	{
-	    if(empty($FileTag))
-	    {
-		unset($this->FILELIST);
-		return;
-	    }
-	    
-	    if( (gettype($Files)) != "array")
-	    {
-		unset($this->FILELIST[$FileTag]);
-		return;
-	    }
-	    else
-	    {
-		while ( list ( $Tag, $Val) = each ($FileTag) )
-		{
-		    unset($this->FILELIST[$Tag]);
-		}
-		return;
-	    }
-	}
-
-    /* Clears all variables set by assign() */
-    
-    function clear_assign ()
-	{
-	    if(!(empty($this->PARSEVARS)))
-	    {
-		while(list($Ref,$Val) = each ($this->PARSEVARS) )
-		{
-		    unset($this->PARSEVARS["$Ref"]);
-		}
-	    }
-	}
-    
-    
-    function clear_href ($href)
-	{
-	    if(!empty($href))
-	    {
-		if( (gettype($href)) != "array")
-		{
-		    unset($this->PARSEVARS[$href]);
-		    return;
-		}
-		else
-		{
-		    while (list ($Ref,$val) = each ($href) )
-		    {
-			unset($this->PARSEVARS[$Ref]);
-		    }
-		    return;
-		}
-	    }
-	    else
-	    {
-		// Empty - clear them all
-		
-		$this->clear_assign();
-	    }
-	    return;
-	}
-    
-
-    function assign ($tpl_array, $trailer="")
-	{
-	    if(gettype($tpl_array) == "array")
-	    {
-		while ( list ($key,$val) = each ($tpl_array) )
-		{
-		    if (!(empty($key)))
-		    {
-			//	Empty values are allowed
-			//	Empty Keys are NOT
-			
-			$this->PARSEVARS["$key"] = $val;
-		    }
-		}
-	    }
-	    else
-	    {
-		// Empty values are allowed in non-array context now.
-		if (!empty($tpl_array))
-		{
-		    $this->PARSEVARS["$tpl_array"] = $trailer;
-		}
-	    }
-	}
-    
-    /* Return the value of an assigned variable.
-     * Christian Brandel cbrandel@gmx.de
-     */
-    
-    function get_assigned($tpl_name = "")
-	{
-	    
-	    if(empty($tpl_name)) { return false; }
-	    if(isset($this->PARSEVARS["$tpl_name"]))
-	    {
-		return ($this->PARSEVARS["$tpl_name"]);
-	    }
-	    else
-	    {
-		return false;
-	    }
-	}
-    
-
-    function error ($errorMsg, $die = 0)
-	{
-	    $this->ERROR = $errorMsg;
-
-	    if($die == 1)
-		die("ERROR: $this->ERROR <BR> \n");
-	    else
-		echo "ERROR: ".$this->ERROR."<BR>\n";
-	}
-}
-
-?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/web/php-admin/htdocs/class.rFastTemplate.php	Mon Jan 11 00:20:03 2010 +1100
@@ -0,0 +1,1087 @@
+<?php
+//
+// Copyright © 2000-2001, Roland Roberts <roland@astrofoto.org>
+//             2001 Alister Bulman <alister@minotaur.nu> Re-Port multi template-roots + more
+// PHP3 Port: Copyright © 1999 CDI <cdi@thewebmasters.net>, All Rights Reserved.
+// Perl Version: Copyright © 1998 Jason Moore <jmoore@sober.com>, All Rights Reserved.
+//
+// RCS Revision
+//   @(#) $Id$
+//   $Source$
+//
+// Copyright Notice
+//
+//    This program is free software; you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation; either version 2, or (at your option)
+//    any later version.
+//
+//    class.rFastTemplate.php is distributed in the hope that it will be
+//    useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+//    General Public License for more details.
+//
+// Comments
+//
+//    I would like to thank CDI <cdi@thewebmasters.net> for pointing out the
+//    copyright notice attached to his PHP3 port which I had blindly missed
+//    in my first release of this code.
+//
+//    This work is derived from class.FastTemplate.php3 version 1.1.0 as
+//    available from http://www.thewebmasters.net/.  That work makes
+//    reference to the "GNU General Artistic License".  In correspondence
+//    with the author, the intent was to use the GNU General Public License;
+//    this work does the same.
+//
+// Authors
+//
+//    Roland Roberts <roland@astrofoto.org>
+//    Alister Bulman <alister@minotaur.nu> (multi template-roots)
+//    Michal Rybarik <michal@rybarik.sk> (define_raw())
+//    CDI <cdi@thewebmasters.net>, PHP3 port
+//    Jason Moore <jmoore@sober.com>, original Perl version
+//
+// Synopsis
+//
+//    require ("PATH-TO-TEMPLATE-CODE/class.Template.php");
+//    $t = new Template("PATH-TO-TEMPLATE-DIRECTORY");
+//    $t->define (array(MAIN => "diary.html"));
+//    $t->setkey (VAR1, "some text");
+//    $t->subst (INNER, "inner")
+//    $t->setkey (VAR1, "some more text");
+//    $t->subst (INNER, ".inner")
+//    $t->setkey (VAR2, "var2 text");
+//    $t->subst (CONTENT, "main");
+//    $t->print (CONTENT);
+//
+// Description
+//
+//    This is a class.FastTemplate.php3 replacement that provides most of the
+//    same interface but has the ability to do nested dynamic templates.  The
+//    default is to do dynamic template expansion and no special action is
+//    required for this to happen.
+//
+// class.FastTemplate.php3 Methods Not Implemented
+//
+//    clear_parse
+//       Same as clear.  In fact, it was the same as clear in FastTemplate.
+//    clear_all
+//       If you really think you need this, try
+//          unset $t;
+//          $t = new Template ($path);
+//       which gives the same effect.
+//    clear_tpl
+//       Use unload instead.  This has the side effect of unloading all parent
+//       and sibling templates which may be more drastic than you expect and
+//       is different from class.FastTemplate.php3.  This difference is
+//       necessary since the only way we can force the reload of an embedded
+//       template is to force the reload of the parent and sibling templates.
+//
+// class.FastTemplate.php3 Methods by Another Name
+//
+//    The existence of these functions is a historical artifact.  I
+//    originally had in mind to write a functional equivalent from scratch.
+//    Then I came my senses and just grabbed class.FastTemplate.php3 and
+//    started hacking it.  So, you can use the names on the right, but the
+//    ones on the left are equivalent and are the names used in the original
+//    class.FastTemplate.php3.
+//
+//      parse        --> subst
+//      get_assiged  --> getkey
+//      assign       --> setkey
+//      clear_href   --> unsetkey
+//      clear_assign --> unsetkey
+//      FastPrint    --> xprint
+//
+
+class rFastTemplate {
+
+   // File name to be used for debugging output.  Needs to be set prior to
+   // calling anything other than option setting commands (debug, debugall,
+   // strict, dynamic) because once the file has been opened, this is ignored.
+   var $DEBUGFILE = '/tmp/class.rFastTemplate.php.dbg';
+
+   // File descriptor for debugging output.
+   var $DEBUGFD = -1;
+
+   // Array for individual member functions.  You can turn on debugging for a
+   // particular member function by calling $this->debug(FUNCTION_NAME)
+   var $DEBUG = array ();
+
+   // Turn this on to turn on debugging in all member functions via
+   // $this->debugall().  Turn if off via $this->debugall(false);
+   var $DEBUGALL = false;
+
+   // Names of actual templates.  Each element will be an array with template
+   // information including is originating file, file load status, parent
+   // template, variable list, and actual template contents.
+   var $TEMPLATE = array();
+
+   //  Holds paths-to-templates (See: set_root and FindTemplate)
+   var $ROOT     = array();
+
+   //  Holds the HANDLE to the last template parsed by parse()
+   var $LAST     = '';
+
+
+   // Strict template checking.  Unresolved variables in templates will generate a
+   // warning.
+   var $STRICT   = true;
+
+   // If true, this suppresses the warning generated by $STRICT=true.
+   var $QUIET    = false;
+
+   // If true, throw an error if the template file is empty.  This was previously the default
+   // behavior.  I'm not sure how this compares to the original FastTemplate().
+   var $NOEMPTY  = false;
+
+   // If true, a non-existent directory in the template path will not
+   // generate an error.  This was added to allow a directory to be
+   // prepended to the template path array based on where in the directory
+   // tree we are.  If the template path is non-existent, it is skipped.
+   var $MISSING_DIR_OKAY = false;
+
+   // Holds handles assigned by a call to parse().
+   var $HANDLE   = array();
+
+   // Holds all assigned variable names and values.
+   var $VAR      = array();
+
+   // Set to true is this is a WIN32 server.  This was part of the
+   // class.FastTemplate.php3 implementation and the only real place it kicks
+   // in is in setting the terminating character on the value of $ROOT, the
+   // path where all the templates live.
+   var $WIN32    = false;
+
+   // Automatically scan template for dynamic templates and assign new values
+   // to TEMPLATE based on whatever names the HTML comments use.  This can be
+   // changed up until the time the first parse() is called.  Well, you can
+   // change it anytime, but it will have no effect on already loaded
+   // templates.  Also, if you have dynamic templates, the first call to parse
+   // will load ALL of your templates, so changing it after that point will
+   // have no effect on any defined templates.
+   var $DYNAMIC   = true;
+
+   // Grrr.  Don't try to break these extra long regular expressions into
+   // multiple lines for readability.  PHP 4.03pl1 chokes on them if you do.
+   // I'm guessing the reason is something obscure with the parenthesis
+   // matching, the same sort of thing Tcl might have, but I'm not sure.
+
+   // Regular expression which matches the beginning of a dynamic/inferior
+   // template.  The critical bit is that we need two parts: (1) the entire
+   // match, and (2) the name of the dynamic template.  The first part is
+   // required because will do a strstr() to split the buffer into two
+   // pieces: everything before the dynamic template declaration and
+   // everything after.  The second is needed because after finding a BEGIN
+   // we will search for an END and they both have to have the same name of
+   // we consider the template malformed and throw and error.
+
+   // Both of these are written with PCRE (Perl-Compatible Regular
+   // Expressions) because we need the non-greedy operators to insure that
+   // we don't read past the end of the HTML comment marker in the case that
+   // the BEGIN/END block have trailing comments after the tag name.
+   var $REGEX_DYNBEG = '/(<!--\s*BEGIN\s+DYNAMIC\s+BLOCK:\s*([A-Za-z][-_A-Za-z0-9.]+)\n?(\s*|\s+.*?)-->)/s';
+
+   // Regular expression which matches the end of a dynamic/inferior
+   // template; see the comment about on the BEGIN match.
+   var $REGEX_DYNEND = '/(<!--\s*END\s+DYNAMIC\s+BLOCK:\s*([A-Za-z][-_A-Za-z0-9.]+)(\s*|\s+.*?)-->)/s';
+   // Regular expression which matches a variable in the template.
+
+   var $REGEX_VAR = '/\{[A-Za-z][-_A-Za-z0-9]*\}/';
+   //
+   // Description
+   //    Constructor.
+   //
+   function rFastTemplate ($pathToTemplates = '') {
+
+      // $pathToTemplates can also be an array of template roots, handled in set_root
+      global $php_errormsg;
+      if (!empty($pathToTemplates)) {
+         $this->set_root ($pathToTemplates);
+      }
+      $this->DEBUG = array ('subst' => false,
+                            'parse_internal' => false,
+                            'parse_internal_1' => false,
+                            'parsed' => false,
+                            'clear' => false,
+                            'clear_dynamic' => false,
+                            'load' => false);
+
+      return $this;
+   }
+
+   //
+   // Description
+   //    Set the name to be used for debugging output.  If another file has
+   //    already been opened, close it so the next call to logwrite will
+   //    reopen under this name.
+   //
+   function debugfile ($name) {
+      $this->DEBUGFILE = $name;
+   }
+
+   //
+   // Description
+   //    Turn on/off debugging output of an individual member function.
+   //
+   function debug ($what, $on = true) {
+      $this->DEBUG[$what] = $on;
+   }
+
+   //
+   // Description
+   //    Turn on/off debugging output of all member functions.
+   //
+   function debugall ($on = true) {
+      $this->DEBUGALL = $on;
+   }
+
+   //
+   // Description
+   //    Turn on/off automatic dynamic template expansion.  Note that a
+   //    template with an inferior dynamic template embedded will still
+   //    parse but only as if it were part of the main template.  When this
+   //    is turned on, it will be parsed out as as if it were a full-blown
+   //    template and can thus be both parsed and appended to as a separate
+   //    entity.
+   //
+   function dynamic ($on = true) {
+      $this->DYNAMIC = $on;
+   }
+
+   //
+   // Description
+   //    Turn on/off strict template checking.  When on, all template tags
+   //    must be assigned or we throw an error (but stilll parse the
+   //    template).
+   //
+   function strict ($on = true) {
+      $this->STRICT = $on;
+   }
+
+   function quiet ($on = true) {
+      $this->QUIET = $on;
+   }
+
+   //
+   // Description
+   //    For compatibility with class.FastTemplate.php3.
+   //
+   function no_strict () {
+      $this->STRICT = false;
+   }
+
+   //
+   // Description
+   //    Turn off errors for missing template directories.  This allows you
+   //    to specify a template path that may not yet exist.
+   //
+   function missing_dir_okay ($on = true) {
+      $this->MISSING_DIR_OKAY = $on;
+   }
+
+   //
+   // Description
+   //    Utility function for debugging.
+   //
+   function logwrite ($msg) {
+      if ($this->DEBUGFD < 0) {
+         $this->DEBUGFD = fopen ($this->DEBUGFILE, 'a');
+      }
+      fputs ($this->DEBUGFD,
+             strftime ('%Y/%m/%d %H:%M:%S ') . $msg . "\n");
+   }
+
+   //
+   // Description
+   //    This was lifted as-is from class.FastTemplate.php3.  Based on what
+   //    platform is in use, it makes sure the path specification ends with
+   //    the proper path separator; i.e., a slash on unix systems and a
+   //    back-slash on WIN32 systems.  When we can run on Mac or VMS I guess
+   //    we'll worry about other characters....
+   //
+   //    $root can now be an array of template roots which will be searched to
+   //    find the first matching name.
+   function set_root ($root) {
+
+      if (!is_array($root)) {
+         $trailer = substr ($root, -1);
+         if ($trailer != ($this->WIN32 ? '\\' : '/'))
+            $root .= ($this->WIN32 ? '\\' : '/');
+
+         if (!is_dir($root)) {
+	    if (!$this->MISSING_DIR_OKAY)
+	       $this->error ("Specified ROOT dir [$root] is not a directory", true);
+            return false;
+         }
+         $this->ROOT[] = $root;
+      } else {
+         reset($root);
+         while(list($k, $v) = each($root)) {
+            if (is_dir($v)) {
+               $trailer = substr ($v,-1);
+               if ($trailer != ($this->WIN32 ? '\\' : '/'))
+                  $v .= ($this->WIN32 ? '\\' : '/');
+               $this->ROOT[] = $v;
+            } else if (!$this->MISSING_DIR_OKAY) {
+	       $this->error ("Specified ROOT dir [$v] is not a directory", true);
+	    }
+         }
+      }
+      // FIXME: should add something here to make sure there is at least one
+      // entry in ROOT[].
+   }
+
+   //
+   // Description
+   //    Associate files with a template names.
+   //
+   // Sigh.  At least with the CVS version of PHP, $dynamic = false sets it
+   // to true.
+   //
+   function define ($fileList, $dynamic = 0) {
+      reset ($fileList);
+      while (list ($tpl, $file) = each ($fileList)) {
+         $this->TEMPLATE[$tpl] = array ('file' => $file, 'dynamic' => $dynamic);
+      }
+      return true;
+   }
+
+   function define_dynamic ($tplList, $parent='') {
+      if (is_array($tplList)) {
+         reset ($tplList);
+         while (list ($tpl, $parent) = each ($tplList)) {
+            $this->TEMPLATE[$tpl]['parent'] = $parent;
+            $this->TEMPLATE[$tpl]['dynamic'] = true;
+         }
+      } else {
+         // $tplList is not an array, but a single child/parent pair.
+         $this->TEMPLATE[$tplList]['parent'] = $parent;
+         $this->TEMPLATE[$tplList]['dynamic'] = true;
+      }
+   }
+
+   //
+   // Description
+   //    Defines a template from a string (not a file). This function has
+   //    not been ported from original PERL module to CDI's
+   //    class.FastTemplate.php3, and it comebacks in rFastTemplate
+   //    class. You can find it useful if you want to use templates, stored
+   //    in database or shared memory.
+   //
+   function define_raw ($stringList, $dynamic = 0) {
+      reset ($stringList);
+      while (list ($tpl, $string) = each ($stringList)) {
+         $this->TEMPLATE[$tpl] = array ('string' => $string, 'dynamic' => $dynamic, 'loaded' => 1);
+      }
+      return true;
+   }
+
+   //
+   // Description
+   //     Try each directory in our list of possible roots in turn until we
+   //     find a matching template
+   //
+   function FindTemplate ($file) {
+      // first try for a template in the current directory short path for
+      // absolute filenames
+      if (substr($file, 0, 1) == '/') {
+         if (file_exists($file)) {
+            return $file;
+         }
+      }
+
+      // search path for a matching file
+      reset($this->ROOT);
+      while(list($k, $v) = each($this->ROOT)) {
+         $f = $v . $file;
+         if (file_exists($f)) {
+            return $f;
+         }
+      }
+
+      $this->error ("FindTemplate: file $file does not exist anywhere in " . implode(' ', $this->ROOT), true);
+      return false;
+   }
+
+
+   //
+   // Description
+   //    Load a template into memory from the underlying file.
+   //
+   function &load ($file) {
+      $debug = $this->DEBUGALL || $this->DEBUG['load'];
+      if (! count($this->ROOT)) {
+         if ($debug)
+            $this->logwrite ("load: cannot open template $file, template base directory not set");
+         $this->error ("cannot open template $file, template base directory not set", true);
+         return false;
+      } else {
+         $contents = '';
+	 unset ($contents);
+
+         $filename = $this->FindTemplate ($file);
+
+         if ($filename)
+            @ $contents = implode ('', (@file($filename)));
+	 // This is inconsistent and depends on the setting of track_errors.  First, with no
+	 // @-directive above, the error will be reported immediately.  Second, if the template
+	 // is empty, it may be that no error occurred and we still end up here.  To get around
+	 // this, we explicitly unset $contents and then check to see if it isset().  If so,
+	 // the template was empty and we treat that as a non-error.
+         if (isset($contents) && ($this->NOEMPTY && empty($contents)) ) {
+            if ($debug)
+               $this->logwrite ("load($file): empty template file");
+            $this->error ("load($file): empty template file", true);
+	 } else if (!isset($contents)) {
+            if ($debug)
+               $this->logwrite ("load($file) failure: $php_errormsg");
+            $this->error ("load($file): failure: $php_errormsg", true);
+         } else {
+            if ($debug)
+               $this->logwrite ("load: found $filename");
+            return $contents;
+         }
+      }
+   }
+
+   //
+   // Description
+   //    Recursive internal parse routine.  This will recursively parse a
+   //    template containing dynamic inferior templates.  Each of these
+   //    inferior templates gets their own entry in the TEMPLATE array.
+   //
+   function &parse_internal_1 ($tag, $rest = '') {
+      $debug = $this->DEBUGALL || $this->DEBUG['parse_internal_1'];
+      if (empty($tag)) {
+         $this->error ("parse_internal_1: empty tag invalid", true);
+      }
+      if ($debug)
+         $this->logwrite ("parse_internal_1 (tag=$tag, rest=$rest)");
+      while (!empty($rest)) {
+         if ($debug)
+            $this->logwrite ('parse_internal_1: REGEX_DYNBEG search: rest => ' . $rest);
+         if (preg_match ($this->REGEX_DYNBEG, $rest, $dynbeg)) {
+            // Found match, now split into two pieces and search the second
+            // half for the matching END.  The string which goes into the
+            // next element includes the HTML comment which forms the BEGIN
+            // block.
+            if ($debug)
+               $this->logwrite ('parse_internal_1: match beg => ' . $dynbeg[1]);
+            $pos = strpos ($rest, $dynbeg[1]);
+
+            // See if the text on either side of the BEGIN comment is only
+            // whitespace.  If so, we delete the entire line.
+            $okay = false;
+            for ($offbeg = $pos - 1; $offbeg >= 0; $offbeg--) {
+               $c = $rest{$offbeg};
+               if ($c == "\n") {
+                  $okay = true;
+                  $offbeg++;
+                  break;
+               }
+               if (($c != ' ') && ($c != "\t")) {
+                  $offbeg = $pos;
+                  break;
+               }
+            }
+            if (! $okay) {
+               $offend = $pos + strlen($dynbeg[1]);
+            } else {
+               $l = strlen ($rest);
+               for ($offend = $pos + strlen($dynbeg[1]); $offend < $l; $offend++) {
+                  $c = $rest{$offend};
+                  if ($c == "\n") {
+                     $offend++;
+                     break;
+                  }
+                  if (($c != ' ') && ($c != "\t")) {
+                     $offend = $pos + strlen($dynbeg[1]);
+                     break;
+                  }
+               }
+            }
+
+            // This includes the contents of the REGEX_DYNBEG in the output
+            // $part[] = substr ($rest, 0, $pos);
+            // This preserves whitespace on the END block line(s).
+            // $part[] = substr ($rest, 0, $pos+strlen($dynbeg[1]));
+            // $rest = substr ($rest, $pos+strlen($dynbeg[1]));
+            // Catch case where BEGIN block is at position 0.
+            if ($offbeg > 0)
+               $part[] = substr ($rest, 0, $offbeg);
+            $rest = substr ($rest, $offend);
+            $sub = '';
+            if ($debug)
+               $this->logwrite ("parse_internal_1: found at pos = $pos");
+            // Okay, here we are actually NOT interested in just the next
+            // END block.  We are only interested in the next END block that
+            // matches this BEGIN block.  This is not the most efficient
+            // because we really could do this in one pass through the
+            // string just marking BEGIN and END blocks.  But the recursion
+            // makes for a simple algorithm (if there was a reverse
+            // preg...).
+            $found  = false;
+            while (preg_match ($this->REGEX_DYNEND, $rest, $dynend)) {
+               if ($debug)
+                  $this->logwrite ('parse_internal_1: REGEX_DYNEND search: rest => ' . $rest);
+               if ($debug)
+                  $this->logwrite ('parse_internal_1: match beg => ' . $dynend[1]);
+               $pos  = strpos ($rest, $dynend[1]);
+               if ($dynbeg[2] == $dynend[2]) {
+                  $found  = true;
+                  // See if the text on either side of the END comment is
+                  // only whitespace.  If so, we delete the entire line.
+                  $okay = false;
+                  for ($offbeg = $pos - 1; $offbeg >= 0; $offbeg--) {
+                     $c = $rest{$offbeg};
+                     if ($c == "\n") {
+                        $offbeg++;
+                        $okay = true;
+                        break;
+                     }
+                     if (($c != ' ') && ($c != "\t")) {
+                        $offbeg = $pos;
+                        break;
+                     }
+                  }
+                  if (! $okay) {
+                     $offend = $pos + strlen($dynend[1]);
+                  } else {
+                     $l = strlen ($rest);
+                     for ($offend = $pos + strlen($dynend[1]); $offend < $l; $offend++) {
+                        $c = $rest{$offend};
+                        if ($c == "\n") {
+                           $offend++;
+                           break;
+                        }
+                        if (($c != ' ') && ($c != "\t")) {
+                           $offend = $pos + strlen($dynend[1]);
+                           break;
+                        }
+                     }
+                  }
+                  // if ($debug)
+                  // $this->logwrite ("parse_internal_1: DYNAMIC BEGIN: (pos,len,beg,end) => ($pos, " . strlen($dynbeg[1]) . ", $offbeg, $offend)
+                  // This includes the contents of the REGEX_DYNEND in the output
+                  // $rest = substr ($rest, $pos);
+                  // This preserves whitespace on the END block line(s).
+                  // $rest = substr ($rest, $pos+strlen($dynend[1]));
+                  // $sub .= substr ($rest, 0, $pos);
+                  $sub .= substr ($rest, 0, $offbeg);
+                  $rest = substr ($rest, $offend);
+                  // Already loaded templates will not be reloaded.  The
+                  // 'clear' test was actually hiding a bug in the clear()
+                  // logic....
+                  if (false && isset($this->TEMPLATE[$dynend[2]]['clear'])
+                      && $this->TEMPLATE[$dynend[2]]['clear']) {
+                     $this->TEMPLATE[$dynend[2]]['string']  = '';
+                     $this->TEMPLATE[$dynend[2]]['result'] = '';
+                     $this->TEMPLATE[$dynend[2]]['part']    =
+                        $this->parse_internal_1 ($dynend[2], ' ');
+                  } else if (!isset($this->TEMPLATE[$dynend[2]]['loaded'])
+                             || !$this->TEMPLATE[$dynend[2]]['loaded']) {
+                     // Omit pathological case of empty dynamic template.
+                     if (strlen($sub) > 0) {
+                        $this->TEMPLATE[$dynend[2]]['string'] = $sub;
+                        $this->TEMPLATE[$dynend[2]]['part']   =
+                           $this->parse_internal_1 ($dynend[2], $sub);
+                        $this->TEMPLATE[$dynend[2]]['part']['parent'] = $tag;
+                     }
+                  }
+                  $this->TEMPLATE[$dynend[2]]['loaded'] = true;
+                  $part[] = &$this->TEMPLATE[$dynend[2]];
+                  $this->TEMPLATE[$dynend[2]]['tag']    = $dynend[2];
+                  break;
+               } else {
+                  $sub .= substr ($rest, 0, $pos+strlen($dynend[1]));
+                  $rest = substr ($rest, $pos+strlen($dynend[1]));
+                  if ($debug)
+                     $this->logwrite ("parse_internal_1: $dynbeg[2] != $dynend[2]");
+               }
+            }
+            if (!$found) {
+               $this->error ("malformed dynamic template, missing END<BR />\n" .
+                             "$dynbeg[1]<BR />\n", true);
+            }
+         } else {
+            // Although it would appear to make sense to check that we don't
+            // have a dangling END block, we will, in fact, ALWAYS appear to
+            // have a dangling END block.  We stuff the BEGIN string in the
+            // part before the inferior template and the END string in the
+            // part after the inferior template.  So for this test to work,
+            // we would need to look just past the final match.
+            if (preg_match ($this->REGEX_DYNEND, $rest, $dynend)) {
+               // $this->error ("malformed dynamic template, dangling END<BR />\n" .
+               //            "$dynend[1]<BR />\n", 1);
+            }
+            $part[] = $rest;
+            $rest = '';
+         }
+      }
+      return $part;
+   }
+
+   //
+   // Description
+   //    Parse the template.  If $tag is actually an array, we iterate over
+   //    the array elements.  If it is a simple string tag, we may still
+   //    recursively parse the template if it contains dynamic templates and
+   //    we are configured to automatically load those as well.
+   //
+   function parse_internal ($tag) {
+      $debug = $this->DEBUGALL || $this->DEBUG['parse_internal'];
+      $append = false;
+      if ($debug)
+         $this->logwrite ("parse_internal (tag=$tag)");
+
+      // If we are handed an array of tags, iterate over all of them.  This
+      // is really a holdover from the way class.FastTemplate.php3 worked;
+      // I think subst() already pulls that array apart for us, so this
+      // should not be necessary unless someone calls the internal member
+      // function directly.
+      if (gettype($tag) == 'array') {
+         reset ($tag);
+         foreach ($tag as $t) {
+            $this->parse_internal ($t);
+         }
+      } else {
+         // Load the file if it hasn't already been loaded.  It might be
+         // nice to put in some logic that reloads the file if it has
+         // changed since we last loaded it, but that probably gets way too
+         // complicated and only makes sense if we start keeping it floating
+         // around between page loads as a persistent variable.
+         if (!isset($this->TEMPLATE[$tag]['loaded'])) {
+            if ($this->TEMPLATE[$tag]['dynamic']) {
+               // Template was declared via define_dynamic().
+               if ($this->TEMPLATE[$tag]['parent'])
+                  $tag = $this->TEMPLATE[$tag]['parent'];
+               else {
+                  // Try to find a non-dynamic template with the same file.
+                  // This would have been defined via define(array(), true)
+                  reset ($this->TEMPLATE);
+                  foreach (array_keys($this->TEMPLATE) as $ptag) {
+                     if ($debug)
+                        $this->logwrite ("parse_internal: looking for non-dynamic parent, $ptag");
+                     if (!$this->TEMPLATE[$ptag]['dynamic']
+                         && ($this->TEMPLATE[$ptag]['file'] == $this->TEMPLATE[$tag]['file'])) {
+                        $tag = $ptag;
+                        break;
+                     }
+                  }
+               }
+            }
+            $this->TEMPLATE[$tag]['string'] = &$this->load($this->TEMPLATE[$tag]['file']);
+            $this->TEMPLATE[$tag]['loaded'] = 1;
+         }
+
+         // If we are supposed to automatically detect dynamic templates and the dynamic
+         // flag is not set, scan the template for dynamic sections.  Dynamic sections
+         // markers have a very rigid syntax as HTML comments....
+         if ($this->DYNAMIC) {
+            $this->TEMPLATE[$tag]['tag']  = $tag;
+            if (!isset($this->TEMPLATE[$tag]['parsed'])
+                || !$this->TEMPLATE[$tag]['parsed']) {
+               $this->TEMPLATE[$tag]['part'] = $this->parse_internal_1 ($tag, $this->TEMPLATE[$tag]['string']);
+               $this->TEMPLATE[$tag]['parsed'] = true;
+            }
+         }
+      }
+   }
+
+   //
+   // Description
+   //    class.FastTemplate.php3 compatible interface.
+   //
+   // Notes
+   //    I prefer the name `subst' to `parse' since during this phase we are
+   //    really doing variable substitution into the template.  However, at
+   //    some point we have to load and parse the template and `subst' will
+   //    do that as well...
+   //
+   function parse ($handle, $tag, $autoload = true) {
+      return $this->subst ($handle, $tag, $autoload);
+   }
+
+   //
+   // Description
+   //    Perform substitution on the template.  We do not really recurse
+   //    downward in the sense that we do not do subsitutions on inferior
+   //    templates.  For each inferior template which is a part of this
+   //    template, we insert the current value of their results.
+   //
+   // Notes
+   //    Do I want to make this return a reference?
+   function subst ($handle, $tag, $autoload = true) {
+      $append = false;
+      $debug = $this->DEBUGALL || $this->DEBUG['subst'];
+      $this->LAST = $handle;
+
+      if ($debug)
+         $this->logwrite ("subst (handle=$handle, tag=$tag, autoload=$autoload)");
+
+      // For compatibility with FastTemplate, the results need to overwrite
+      // for an array.  This really only seems to be useful in the case of
+      // something like
+      //     $t->parse ('MAIN', array ('array', 'main'));
+      // Where the 'main' template has a variable named MAIN which will be
+      // set on the first pass (i.e., when parasing 'array') and used on the
+      // second pass (i.e., when parsing 'main').
+      if (gettype($tag) == 'array') {
+         foreach (array_values($tag) as $t) {
+            if ($debug)
+               $this->logwrite ("subst: calling subst($handle,$t,$autoload)");
+            $this->subst ($handle, $t, $autoload);
+         }
+         return $this->HANDLE[$handle];
+      }
+
+      // Period prefix means append result to pre-existing value.
+      if (substr($tag,0,1) == '.') {
+         $append = true;
+         $tag = substr ($tag, 1);
+         if ($debug)
+            $this->logwrite ("subst (handle=$handle, tag=$tag, autoload=$autoload) in append mode");
+      }
+      // $this->TEMPLATE[$tag] will only be set if it was explicitly
+      // declared via define(); i.e., inferior templates will not have an
+      // entry.
+      if (isset($this->TEMPLATE[$tag])) {
+         if (!isset($this->TEMPLATE[$tag]['parsed'])
+             || !$this->TEMPLATE[$tag]['parsed'])
+            $this->parse_internal ($tag);
+      } else {
+         if (!$this->DYNAMIC) {
+            $this->error ("subst (handle=$handle, tag=$tag, autoload=$autoload): " .
+                          'no such tag and dynamic templates are turned off', true);
+         }
+         if ($autoload) {
+            if ($debug)
+               $this->logwrite ("subst: TEMPLATE[tag=$tag] not found, trying autoload");
+            foreach (array_keys($this->TEMPLATE) as $t) {
+               if ($debug)
+                  $this->logwrite ("subst: calling parse_internal (tag=$t)");
+               if (!isset($this->TEMPLATE[$tag]['parsed'])
+                   || !$this->TEMPLATE[$tag]['parsed'])
+                  $this->parse_internal ($t);
+            }
+            if ($debug)
+               $this->logwrite ('subst: retrying with autoload = false');
+            $this->subst ($handle, $tag, false);
+            if ($debug)
+               $this->logwrite ('subst: completed with autoload = false');
+            return;
+         } else {
+            $this->error ("subst (handle=$handle, tag=$tag, autoload=$autoload):  no such tag", true);
+         }
+      }
+      if (!$append) {
+         $this->TEMPLATE[$tag]['result'] = '';
+         if ($debug)
+            $this->logwrite ("subst (handle=$handle, tag=$tag, autoload=$autoload) in overwrite mode");
+      }
+      if ($debug)
+         $this->logwrite ('subst: type(this->TEMPLATE[$tag][\'part\']) => ' .
+                          gettype($this->TEMPLATE[$tag]['part']));
+      // Hmmm, clear() called before subst() seems to result in this not
+      // being defined which leaves me a bit confused....
+      $result = '';
+      if (isset($this->TEMPLATE[$tag]['part'])) {
+         reset ($this->TEMPLATE[$tag]['part']);
+         foreach (array_keys($this->TEMPLATE[$tag]['part']) as $p) {
+            if ($debug)
+               $this->logwrite ("subst: looking at TEMPLATE[$tag]['part'][$p]");
+            $tmp = $this->TEMPLATE[$tag]['part'][$p];
+            // Don't try if ($p == 'parent')....
+            if (strcmp ($p, 'parent') == 0) {
+               if ($debug)
+                  $this->logwrite ("subst: skipping part $p");
+               $tmp = '';
+            } else if (gettype($this->TEMPLATE[$tag]['part'][$p]) == 'string') {
+               if ($debug)
+                  $this->logwrite ("subst: using part $p");
+               reset ($this->VAR);
+               // Because we treat VAR and HANDLE separately (unlike
+               // class.FastTemplate.php3), we have to iterate over both or we
+               // miss some substitutions and are not 100% compatible.
+               while (list($key,$val) = each ($this->VAR)) {
+                  if ($debug)
+                     $this->logwrite ("subst: substituting VAR $key = $val in $tag");
+                  $key = '{'.$key.'}';
+                  $tmp = str_replace ($key, $val, $tmp);
+               }
+               reset ($this->HANDLE);
+               while (list($key,$val) = each ($this->HANDLE)) {
+                  if ($debug)
+                     $this->logwrite ("subst: substituting HANDLE $key = $val in $tag");
+                  $key = '{'.$key.'}';
+                  $tmp = str_replace ($key, $val, $tmp);
+               }
+               $result .= $tmp;
+            } else {
+               $xtag = $this->TEMPLATE[$tag]['part'][$p]['tag'];
+               if ($debug) {
+                  $this->logwrite ("subst: substituting other tag $xtag result in $tag");
+               }
+               // The assignment is a no-op if the result is not set, but when
+               // E_ALL is in effect, a warning is generated without the
+               // isset() test.
+               if (isset ($this->TEMPLATE[$xtag]['result']))
+                  $result .= $this->TEMPLATE[$xtag]['result'];
+            }
+         }
+      }
+      if ($this->STRICT) {
+	 // If quiet-mode is turned on, skip the check since we're not going
+	 // to do anything anyway.
+	 if (!$this->QUIET) {
+	    if (preg_match ($this->REGEX_VAR, $result)) {
+	       $this->error ("<B>unmatched tags still present in $tag</B><BR />");
+	    }
+	 }
+      } else {
+         $result = preg_replace ($this->REGEX_VAR, '', $result);
+      }
+      if ($append) {
+         if ($debug) {
+            $this->logwrite ("subst: appending TEMPLATE[$tag]['result'] = $result");
+            $this->logwrite ("subst: old HANDLE[$handle] = {$this->HANDLE[$handle]}");
+            $this->logwrite ("subst: old TEMPLATE[$tag]['result'] = {$this->TEMPLATE[$tag]['result']}");
+         }
+         // The isset() tests are to suppresss warning when E_ALL is in effect
+         // and the variables have not actually been set yet (even though the
+         // user specified append-mode).
+         if (isset ($this->HANDLE[$handle]))
+            $this->HANDLE[$handle] .= $result;
+         else
+            $this->HANDLE[$handle] = $result;
+         if (isset ($this->TEMPLATE[$tag]['result']))
+            $this->TEMPLATE[$tag]['result'] .= $result;
+         else
+            $this->TEMPLATE[$tag]['result'] = $result;
+         if ($debug) {
+            $this->logwrite ("subst: new HANDLE[$handle] = {$this->HANDLE[$handle]}");
+            $this->logwrite ("subst: new TEMPLATE[$tag]['result'] = {$this->TEMPLATE[$tag]['result']}");
+         }
+
+      } else {
+         if ($debug)
+            $this->logwrite ("subst: setting TEMPLATE[$tag]['result'] = $result");
+         $this->HANDLE[$handle]  = $result;
+         $this->TEMPLATE[$tag]['result'] = $result;
+      }
+      return $this->HANDLE[$handle];
+   }
+
+   //
+   // Description
+   //    Clear a block from a template.  The intent is to remove an inferior
+   //    template from a parent.  This works even if the template has already
+   //    been parsed since we go straight to the specified template and clear
+   //    the results element.  If the given template has not yet been
+   //    loaded, the load is forced by calling parse_internal().
+   //
+   function clear_dynamic ($tag = NULL) {
+      $debug = $this->DEBUGALL || $this->DEBUG['clear_dynamic'];
+      if (is_null ($tag)) {
+         // Clear all result elements.  Uhm, needs to be tested.
+         if ($debug)
+            $this->logwrite ("clear_dynamic (NULL)");
+         foreach (array_values ($this->TEMPLATE) as $t) {
+            $this->clear_dynamic ($t);
+         }
+         return;
+      } else if (gettype($tag) == 'array') {
+         if ($debug)
+            $this->logwrite ("clear_dynamic ($tag)");
+         foreach (array_values($tag) as $t) {
+            $this->clear_dynamic ($t);
+         }
+         return;
+      }
+      else if (!isset($this->TEMPLATE[$tag])) {
+         if ($debug)
+            $this->logwrite ("clear_dynamic ($tag) --> $tag not set, calling parse_internal");
+         $this->parse_internal ($tag);
+         // $this->TEMPLATE[$tag] = array ();
+      }
+      if ($debug)
+         $this->logwrite ("clear_dynamic ($tag)");
+      // $this->TEMPLATE[$tag]['loaded']  = true;
+      // $this->TEMPLATE[$tag]['string']  = '';
+      $this->TEMPLATE[$tag]['result'] = '';
+      // $this->TEMPLATE[$tag]['clear']   = true;
+   }
+
+   //
+   // Description
+   //    Clear the results of a handle set by parse().  The input handle can
+   //    be a single value, an array, or the PHP constant NULL.  For the
+   //    last case, all handles cleared.
+   //
+   function clear ($handle = NULL) {
+      $debug = $this->DEBUGALL || $this->DEBUG['clear'];
+      if (is_null ($handle)) {
+         // Don't bother unsetting them, just set the whole thing to a new,
+         // empty array.
+         if ($debug)
+            $this->logwrite ("clear (NULL)");
+         $this->HANDLE = array ();
+      } else if (gettype ($handle) == 'array') {
+         if ($debug)
+            $this->logwrite ("clear ($handle)");
+         foreach (array_values ($handle) as $h) {
+            $this->clear ($h);
+         }
+      } else if (isset ($this->HANDLE[$handle])) {
+         if ($debug)
+            $this->logwrite ("clear ($handle)");
+         unset ($this->HANDLE[$handle]);
+      }
+   }
+
+   //
+   // Description
+   //   Clears all information associated with the specified tag as well as
+   //   any information associated with embedded templates.  This will force
+   //   the templates to be reloaded on the next call to subst().
+   //   Additionally, any results of previous calls to subst() will also be
+   //   cleared.
+   //
+   // Notes
+   //   This leaves dangling references in $this->HANDLE.  Or does PHP do
+   //   reference counting so they are still valid?
+   //
+   function unload ($tag) {
+      if (!isset($this->TEMPLATE[$tag]))
+         return;
+      if (isset ($this->TEMPLATE[$tag]['parent'])) {
+         $ptag = $this->TEMPLATE[$tag]['parent'];
+         foreach (array_keys($this->TEMPLATE) as $t) {
+            if ($this->TEMPLATE[$t]['parent'] == $ptag) {
+               unset ($this->TEMPLATE[$t]);
+            }
+         }
+      }
+      unset ($this->TEMPLATE[$tag]);
+      return;
+   }
+
+   //
+   // Description
+   //    class.FastTemplate.php3 compatible interface.
+   //
+   function assign ($tplkey, $rest = '') {
+      $this->setkey ($tplkey, $rest);
+   }
+
+   //
+   // Description
+   //    Set a (key,value) in our internal variable array.  These will be
+   //    used during the substitution phase to replace template variables.
+   //
+   function setkey ($tplkey, $rest = '') {
+      if (gettype ($tplkey) == 'array') {
+         reset ($tplkey);
+         while (list($key,$val) = each ($tplkey)) {
+            if (!empty($key)) {
+               $this->VAR[$key] = $val;
+            }
+         }
+      } else {
+         if (!empty($tplkey)) {
+            $this->VAR[$tplkey] = $rest;
+         }
+      }
+   }
+
+   function append ($tplkey, $rest = '') {
+      if (gettype ($tplkey) == 'array') {
+         reset ($tplkey);
+         while (list($key,$val) = each ($tplkey)) {
+            if (!empty($key)) {
+               $this->VAR[$key] .= $val;
+            }
+         }
+      } else {
+         if (!empty($tplkey)) {
+            $this->VAR[$tplkey] .= $rest;
+         }
+      }
+   }
+
+   //
+   // Description
+   //    class.FastTemplate.php3 compatible interface
+   //
+   function get_assigned ($key = '') {
+      return $this->getkey ($key);
+   }
+
+   //
+   // Description
+   //    Retrieve a value from our internal variable array given the key name.
+   //
+   function getkey ($key = '') {
+      if (empty($key)) {
+         return false;
+      } else if (isset ($this->VAR[$key])) {
+         return $this->VAR[$key];
+      } else {
+         return false;
+      }
+   }
+
+   function fetch ($handle = '') {
+      if (empty($handle)) {
+         $handle = $this->LAST;
+      }
+      return $this->HANDLE[$handle];
+   }
+
+   function xprint ($handle = '') {
+      if (empty($handle)) {
+         $handle = $this->LAST;
+      }
+      print ($this->HANDLE[$handle]);
+   }
+
+   function FastPrint ($handle = '') {
+      $this->xprint ($handle);
+   }
+
+   function clear_href ($key = '') {
+      $this->unsetkey ($key);
+   }
+
+   function unsetkey ($key = '') {
+      if (empty($key)) {
+         unset ($this->VAR);
+         $this->VAR = array ();
+      } else if (gettype($key) == 'array') {
+         reset ($key);
+         foreach (array_values($key) as $k) {
+            unset ($this->VAR[$k]);
+         }
+      } else {
+         unset ($this->VAR[$key]);
+      }
+   }
+
+   function define_nofile ($stringList, $dynamic = 0) {
+      $this->define_raw ($stringList, $dynamic);
+   }
+   //
+   // Description
+   //    Member function to control explicit error messages.  We don't do
+   //    real PHP error handling.
+   //
+   function error ($errorMsg, $die = 0) {
+      $this->ERROR = $errorMsg;
+      echo "ERROR: {$this->ERROR} <br /> \n";
+      if ($die) {
+         exit;
+      }
+      return;
+   }
+}
--- a/contrib/web/php-admin/htdocs/edit.php	Mon Dec 21 17:49:16 2009 +1100
+++ b/contrib/web/php-admin/htdocs/edit.php	Mon Jan 11 00:20:03 2010 +1100
@@ -27,7 +27,7 @@
  */
 
 require("../conf/config.php");
-require("class.FastTemplate.php");
+require("class.rFastTemplate.php");
 
 function mlmmj_boolean($name, $nicename, $text) 
 {
@@ -97,7 +97,7 @@
 function encode_entities($str) { return htmlentities($str); } 
 
 
-$tpl = new FastTemplate($templatedir);
+$tpl = new rFastTemplate($templatedir);
 
 $list = $HTTP_GET_VARS["list"];
 
--- a/contrib/web/php-admin/htdocs/index.php	Mon Dec 21 17:49:16 2009 +1100
+++ b/contrib/web/php-admin/htdocs/index.php	Mon Jan 11 00:20:03 2010 +1100
@@ -26,10 +26,10 @@
  * IN THE SOFTWARE.
  */
 
-require("class.FastTemplate.php");
+require("class.rFastTemplate.php");
 require("../conf/config.php");
 
-$tpl = new FastTemplate($templatedir);
+$tpl = new rFastTemplate($templatedir);
 
 $tpl->define(array("main" => "index.html"));
 
--- a/contrib/web/php-admin/htdocs/save.php	Mon Dec 21 17:49:16 2009 +1100
+++ b/contrib/web/php-admin/htdocs/save.php	Mon Jan 11 00:20:03 2010 +1100
@@ -27,7 +27,7 @@
  */
 
 require("../conf/config.php");
-require("class.FastTemplate.php");
+require("class.rFastTemplate.php");
 
 function mlmmj_boolean($name, $nicename, $text)
 {
@@ -72,7 +72,7 @@
 function encode_entities($str) { return htmlentities($str); }
 
 
-$tpl = new FastTemplate($templatedir);
+$tpl = new rFastTemplate($templatedir);
 
 $list = $HTTP_POST_VARS["list"];