<?PHP

#
#   FILE:  ApplicationFramework.php
#
#   Part of the ScoutLib application support library
#   Copyright 2009-2012 Edward Almasy and Internet Scout
#   http://scout.wisc.edu
#

/**
* Top-level framework for web applications.
* \nosubgrouping
*/
class ApplicationFramework {

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /** @name Application Framework */ /*@(*/

    /** @cond */
    /**
    * Object constructor.
    **/
    function __construct()
    {
        # save execution start time
        $this->ExecutionStartTime = microtime(TRUE);

        # begin/restore PHP session
        $SessionPath = isset($_SERVER["REQUEST_URI"])
                ? dirname($_SERVER["REQUEST_URI"])
                : isset($_SERVER["SCRIPT_NAME"])
                ? dirname($_SERVER["SCRIPT_NAME"])
                : isset($_SERVER["PHP_SELF"])
                ? dirname($_SERVER["PHP_SELF"])
                : "";
        $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
                : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
                : php_uname("n");
        $SessionStorage = session_save_path()
                ."/".self::$AppName."_".md5($SessionDomain.$SessionPath);
        if (!is_dir($SessionStorage)) {  mkdir($SessionStorage, 0700 );  }
        session_save_path($SessionStorage);
        ini_set("session.gc_maxlifetime", self::$SessionLifetime);
        session_set_cookie_params(
                self::$SessionLifetime, $SessionPath, $SessionDomain);
        session_start();

        # set up object file autoloader
        $this->SetUpObjectAutoloading();

        # set up function to output any buffered text in case of crash
        register_shutdown_function(array($this, "OnCrash"));

        # set up our internal environment
        $this->DB = new Database();

        # set up our exception handler
        set_exception_handler(array($this, "GlobalExceptionHandler"));

        # load our settings from database
        $this->LoadSettings();

        # set PHP maximum execution time
        $this->MaxExecutionTime($this->Settings["MaxExecTime"]);

        # register events we handle internally
        $this->RegisterEvent($this->PeriodicEvents);
        $this->RegisterEvent($this->UIEvents);
    }
    /** @endcond */

    /** @cond */
    /**
    * Object destructor.
    **/
    function __destruct()
    {
        # if template location cache is flagged to be saved
        if ($this->SaveTemplateLocationCache)
        {
            # write template location cache out and update cache expiration
            $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                    ." SET TemplateLocationCache = '"
                            .addslashes(serialize(
                                    $this->Settings["TemplateLocationCache"]))."',"
                    ." TemplateLocationCacheExpiration = "
                            ." NOW() + INTERVAL "
                                    .$this->Settings["TemplateLocationCacheInterval"]
                                    ." MINUTE");
        }
    }
    /** @endcond */

    /** @cond */
    /**
    * Default top-level exception handler.
    **/
    function GlobalExceptionHandler($Exception)
    {
        # display exception info
        $Location = $Exception->getFile()."[".$Exception->getLine()."]";
        ?><table width="100%" cellpadding="5"
                style="border: 2px solid #666666;  background: #CCCCCC;
                font-family: Courier New, Courier, monospace;
                margin-top: 10px;"><tr><td>
        <div style="color: #666666;">
            <span style="font-size: 150%;">
                    <b>Uncaught Exception</b></span><br />
            <b>Message:</b> <i><?PHP  print $Exception->getMessage();  ?></i><br />
            <b>Location:</b> <i><?PHP  print $Location;  ?></i><br />
            <b>Trace:</b>
            <blockquote><pre><?PHP  print $Exception->getTraceAsString();
                    ?></pre></blockquote>
        </div>
        </td></tr></table><?PHP

        # log exception if possible
        $LogMsg = "Uncaught exception (".$Exception->getMessage().").";
        $this->LogError(self::LOGLVL_ERROR, $LogMsg);
    }
    /** @endcond */

    /**
    * Add directory to be searched for object files when autoloading.  Directories
    * are searched in the order they are added.
    * @param string Dir Directory to be searched.
    * @param string Prefix Leading prefix to be stripped from file names
    *      when comparing them against objects (e.g. "Axis--").  (OPTIONAL)
    * @param string|array ClassPattern Pattern string or array of pattern
    *      strings to run on class name via preg_replace().  (OPTIONAL)
    * @param string|array ClassReplacement Replacement string or array of
    *      replacement strings to run on class name via preg_replace().  (OPTIONAL)
    */
    static function AddObjectDirectory(
            $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
    {
        # make sure directory has trailing slash
        $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");

        # add directory to directory list
        self::$ObjectDirectories = array_merge(
                array($Dir => array(
                        "Prefix" => $Prefix,
                        "ClassPattern" => $ClassPattern,
                        "ClassReplacement" => $ClassReplacement,
                        )),
                self::$ObjectDirectories);
    }

    /**
    * Add additional directory(s) to be searched for image files.  Specified
    * directory(s) will be searched, in order, before the default directories
    * or any other directories previously specified.  If a directory is already
    * present in the list, it will be moved to front to be searched first (or
    * to the end to be searched last, if SearchLast is set).  SearchLast only
    * affects whether added directories are searched before or after those
    * currently in the list;  when multiple directories are added, they are
    * always searched in the order they appear in the array.  The token
    * "%ACTIVEUI%" may be included in the directory names, and will be replaced
    * with the canonical name of the currently active UI when searching for files.
    * @param string $Dir String with directory or array with directories to be searched.
    * @param bool $SearchLast If TRUE, the directory(s) are searched after the entries
    *       current in the list, instead of before.  (OPTIONAL, defaults to FALSE)
    * @param bool $SkipSlashCheck If TRUE, check for trailing slash will be omitted.
    *       (OPTIONAL, defaults to FALSE)
    * @see ApplicationFramework::GUIFile()
    * @see ApplicationFramework::PUIFile()
    */
    function AddImageDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
    {
        # add directories to existing image directory list
        $this->ImageDirList = $this->AddToDirList(
                $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
    }

    /**
    * Add additional directory(s) to be searched for user interface include
    * (CSS, JavaScript, common PHP, common HTML, etc) files.  Specified
    * directory(s) will be searched, in order, before the default directories
    * or any other directories previously specified.  If a directory is already
    * present in the list, it will be moved to front to be searched first (or
    * to the end to be searched last, if SearchLast is set).  SearchLast only
    * affects whether added directories are searched before or after those
    * currently in the list;  when multiple directories are added, they are
    * always searched in the order they appear in the array.  The token
    * "%ACTIVEUI%" may be included in the directory names, and will be replaced
    * with the canonical name of the currently active UI when searching for files.
    * @param string $Dir String with directory or array with directories to be searched.
    * @param bool $SearchLast If TRUE, the directory(s) are searched after the entries
    *       current in the list, instead of before.  (OPTIONAL, defaults to FALSE)
    * @param bool $SkipSlashCheck If TRUE, check for trailing slash will be omitted.
    *       (OPTIONAL, defaults to FALSE)
    * @see ApplicationFramework::GUIFile()
    * @see ApplicationFramework::PUIFile()
    */
    function AddIncludeDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
    {
        # add directories to existing image directory list
        $this->IncludeDirList = $this->AddToDirList(
                $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
    }

    /**
    * Add additional directory(s) to be searched for user interface (HTML/TPL)
    * files.  Specified directory(s) will be searched, in order, before the default
    * directories or any other directories previously specified.  If a directory
    * is already present in the list, it will be moved to front to be searched first
    * (or to the end to be searched last, if SearchLast is set).  SearchLast only
    * affects whether added directories are searched before or after those
    * currently in the list;  when multiple directories are added, they are
    * always searched in the order they appear in the array.  The token
    * "%ACTIVEUI%" may be included in the directory names, and will be replaced
    * with the canonical name of the currently active UI when searching for files.
    * @param string $Dir String with directory or array with directories to be searched.
    * @param bool $SearchLast If TRUE, the directory(s) are searched after the entries
    *       current in the list, instead of before.  (OPTIONAL, defaults to FALSE)
    * @param bool $SkipSlashCheck If TRUE, check for trailing slash will be omitted.
    *       (OPTIONAL, defaults to FALSE)
    * @see ApplicationFramework::GUIFile()
    * @see ApplicationFramework::PUIFile()
    */
    function AddInterfaceDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
    {
        # add directories to existing image directory list
        $this->InterfaceDirList = $this->AddToDirList(
                $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
    }

    /**
    * Add additional directory(s) to be searched for function ("F-") files.
    * Specified directory(s) will be searched, in order, before the default
    * directories or any other directories previously specified.  If a directory
    * is already present in the list, it will be moved to front to be searched
    * first (or to the end to be searched last, if SearchLast is set).  SearchLast
    * only affects whether added directories are searched before or after those
    * currently in the list;  when multiple directories are added, they are
    * always searched in the order they appear in the array.  The token
    * "%ACTIVEUI%" may be included in the directory names, and will be replaced
    * with the canonical name of the currently active UI when searching for files.
    * @param string $Dir String with directory or array with directories to be searched.
    * @param bool $SearchLast If TRUE, the directory(s) are searched after the entries
    *       current in the list, instead of before.  (OPTIONAL, defaults to FALSE)
    * @param bool $SkipSlashCheck If TRUE, check for trailing slash will be omitted.
    *       (OPTIONAL, defaults to FALSE)
    * @see ApplicationFramework::GUIFile()
    * @see ApplicationFramework::PUIFile()
    */
    function AddFunctionDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
    {
        # add directories to existing image directory list
        $this->FunctionDirList = $this->AddToDirList(
                $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
    }

    /**
    * Specify function to use to detect the web browser type.  Function should
    * return an array of browser names.
    * @param callback $DetectionFunc Browser detection function callback.
    */
    function SetBrowserDetectionFunc($DetectionFunc)
    {
        $this->BrowserDetectFunc = $DetectionFunc;
    }

    /**
    * Add a callback that will not be executed after buffered content has
    * been output and that won't have its output buffered.
    * @param $Callback callback
    * @param $Parameters optional callback parameters in an array
    */
    function AddUnbufferedCallback($Callback, $Parameters=array())
    {
        if (is_callable($Callback))
        {
            $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
        }
    }

    /**
    * Get/set UI template location cache expiration period in minutes.  An
    * expiration period of 0 disables caching.
    * @param int $NewInterval New expiration period in minutes.  (OPTIONAL)
    * @return Current expiration period in minutes.
    */
    function TemplateLocationCacheExpirationInterval($NewInterval = -1)
    {
        if ($NewInterval >= 0)
        {
            $this->Settings["TemplateLocationCacheInterval"] = $NewInterval;
            $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                    ." SET TemplateLocationCacheInterval = '"
                            .intval($NewInterval)."'");
        }
        return $this->Settings["TemplateLocationCacheInterval"];
    }

    /**
    * Get/set session timeout in seconds.
    * @param int $NewValue New session timeout value.  (OPTIONAL)
    * @return Current session timeout value in seconds.
    */
    static function SessionLifetime($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            self::$SessionLifetime = $NewValue;
        }
        return self::$SessionLifetime;
    }

    /**
    * Load page PHP and HTML/TPL files.
    * @param PageName Name of page to be loaded (e.g. "BrowseResources").
    */
    function LoadPage($PageName)
    {
        # sanitize incoming page name and save local copy
        $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
        $this->PageName = $PageName;

        # buffer any output from includes or PHP file
        ob_start();

        # include any files needed to set up execution environment
        foreach ($this->EnvIncludes as $IncludeFile)
        {
            include($IncludeFile);
        }

        # signal page load
        $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));

        # signal PHP file load
        $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
                "PageName" => $PageName));

        # if signal handler returned new page name value
        $NewPageName = $PageName;
        if (($SignalResult["PageName"] != $PageName)
                && strlen($SignalResult["PageName"]))
        {
            # if new page name value is page file
            if (file_exists($SignalResult["PageName"]))
            {
                # use new value for PHP file name
                $PageFile = $SignalResult["PageName"];
            }
            else
            {
                # use new value for page name
                $NewPageName = $SignalResult["PageName"];
            }
        }

        # if we do not already have a PHP file
        if (!isset($PageFile))
        {
            # look for PHP file for page
            $OurPageFile = "pages/".$NewPageName.".php";
            $LocalPageFile = "local/pages/".$NewPageName.".php";
            $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
                    : (file_exists($OurPageFile) ? $OurPageFile
                    : "pages/".$this->DefaultPage.".php");
        }

        # load PHP file
        include($PageFile);

        # save buffered output to be displayed later after HTML file loads
        $PageOutput = ob_get_contents();
        ob_end_clean();

        # signal PHP file load is complete
        ob_start();
        $Context["Variables"] = get_defined_vars();
        $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
                array("PageName" => $PageName, "Context" => $Context));
        $PageCompleteOutput = ob_get_contents();
        ob_end_clean();

        # set up for possible TSR (Terminate and Stay Resident :))
        $ShouldTSR = $this->PrepForTSR();

        # if PHP file indicated we should autorefresh to somewhere else
        if ($this->JumpToPage)
        {
            if (!strlen(trim($PageOutput)))
            {
                ?><html>
                <head>
                    <meta http-equiv="refresh" content="0; URL=<?PHP
                            print($this->JumpToPage);  ?>">
                </head>
                <body bgcolor="white">
                </body>
                </html><?PHP
            }
        }
        # else if HTML loading is not suppressed
        elseif (!$this->SuppressHTML)
        {
            # set content-type to get rid of diacritic errors
            header("Content-Type: text/html; charset="
                .$this->HtmlCharset, TRUE);

            # load common HTML file (defines common functions) if available
            $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
                    "Common", array("tpl", "html"));
            if ($CommonHtmlFile) {  include($CommonHtmlFile);  }

            # load UI functions
            $this->LoadUIFunctions();

            # begin buffering content
            ob_start();

            # signal HTML file load
            $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
                    "PageName" => $PageName));

            # if signal handler returned new page name value
            $NewPageName = $PageName;
            $PageContentFile = NULL;
            if (($SignalResult["PageName"] != $PageName)
                    && strlen($SignalResult["PageName"]))
            {
                # if new page name value is HTML file
                if (file_exists($SignalResult["PageName"]))
                {
                    # use new value for HTML file name
                    $PageContentFile = $SignalResult["PageName"];
                }
                else
                {
                    # use new value for page name
                    $NewPageName = $SignalResult["PageName"];
                }
            }

            # load page content HTML file if available
            if ($PageContentFile === NULL)
            {
                $PageContentFile = $this->FindFile(
                        $this->InterfaceDirList, $NewPageName,
                        array("tpl", "html"));
            }
            if ($PageContentFile)
            {
                include($PageContentFile);
            }
            else
            {
                print "<h2>ERROR:  No HTML/TPL template found"
                        ." for this page.</h2>";
            }

            # signal HTML file load complete
            $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");

            # stop buffering and save output
            $PageContentOutput = ob_get_contents();
            ob_end_clean();

            # load page start HTML file if available
            ob_start();
            $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
                    array("tpl", "html"), array("StdPage", "StandardPage"));
            if ($PageStartFile) {  include($PageStartFile);  }
            $PageStartOutput = ob_get_contents();
            ob_end_clean();

            # load page end HTML file if available
            ob_start();
            $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
                    array("tpl", "html"), array("StdPage", "StandardPage"));
            if ($PageEndFile) {  include($PageEndFile);  }
            $PageEndOutput = ob_get_contents();
            ob_end_clean();

            # get list of any required files not loaded
            $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);

            # if a browser detection function has been made available
            if (is_callable($this->BrowserDetectFunc))
            {
                # call function to get browser list
                $Browsers = call_user_func($this->BrowserDetectFunc);

                # for each required file
                $NewRequiredFiles = array();
                foreach ($RequiredFiles as $File)
                {
                    # if file name includes browser keyword
                    if (preg_match("/%BROWSER%/", $File))
                    {
                        # for each browser
                        foreach ($Browsers as $Browser)
                        {
                            # substitute in browser name and add to new file list
                            $NewRequiredFiles[] = preg_replace(
                                    "/%BROWSER%/", $Browser, $File);
                        }
                    }
                    else
                    {
                        # add to new file list
                        $NewRequiredFiles[] = $File;
                    }
                }
                $RequiredFiles = $NewRequiredFiles;
            }

            # for each required file
            foreach ($RequiredFiles as $File)
            {
                # locate specific file to use
                $FilePath = $this->GUIFile($File);

                # if file was found
                if ($FilePath)
                {
                    # determine file type
                    $NamePieces = explode(".", $File);
                    $FileSuffix = strtolower(array_pop($NamePieces));

                    # add file to HTML output based on file type
                    $FilePath = htmlspecialchars($FilePath);
                    switch ($FileSuffix)
                    {
                        case "js":
                            $Tag = '<script type="text/javascript" src="'
                                    .$FilePath.'"></script>';
                            $PageEndOutput = preg_replace(
                                    "#</body>#i", $Tag."\n</body>", $PageEndOutput, 1);
                            break;

                        case "css":
                            $Tag = '<link rel="stylesheet" type="text/css"'
                                    .' media="all" href="'.$FilePath.'">';
                            $PageStartOutput = preg_replace(
                                    "#</head>#i", $Tag."\n</head>", $PageStartOutput, 1);
                            break;
                    }
                }
            }

            # write out page
            print $PageStartOutput.$PageContentOutput.$PageEndOutput;
        }

        # run any post-processing routines
        foreach ($this->PostProcessingFuncs as $Func)
        {
            call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
        }

        # write out any output buffered from page code execution
        if (strlen($PageOutput))
        {
            if (!$this->SuppressHTML)
            {
                ?><table width="100%" cellpadding="5"
                        style="border: 2px solid #666666;  background: #CCCCCC;
                        font-family: Courier New, Courier, monospace;
                        margin-top: 10px;"><tr><td><?PHP
            }
            if ($this->JumpToPage)
            {
                ?><div style="color: #666666;"><span style="font-size: 150%;">
                <b>Page Jump Aborted</b></span>
                (because of error or other unexpected output)<br />
                <b>Jump Target:</b>
                <i><?PHP  print($this->JumpToPage);  ?></i></div><?PHP
            }
            print $PageOutput;
            if (!$this->SuppressHTML)
            {
                ?></td></tr></table><?PHP
            }
        }

        # write out any output buffered from the page code execution complete signal
        if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
        {
            print $PageCompleteOutput;
        }

        # execute callbacks that should not have their output buffered
        foreach ($this->UnbufferedCallbacks as $Callback)
        {
            call_user_func_array($Callback[0], $Callback[1]);
        }

        # terminate and stay resident (TSR!) if indicated and HTML has been output
        # (only TSR if HTML has been output because otherwise browsers will misbehave)
        if ($ShouldTSR) {  $this->LaunchTSR();  }
    }

    /**
    * Get name of page being loaded.  The page name will not include an extension.
    * This call is only meaningful once LoadPage() has been called.
    * @return Page name.
    */
    function GetPageName()
    {
        return $this->PageName;
    }

    /**
    * Set URL of page to autoload after PHP page file is executed.  The HTML/TPL
    * file will never be loaded if this is set.  Pass in NULL to clear any autoloading.
    * @param Page URL of page to jump to (autoload).  If the URL does not appear
    *       to point to a PHP or HTML file then "index.php?P=" will be prepended to it.
    */
    function SetJumpToPage($Page)
    {
        if (!is_null($Page) && (strpos($Page, "?") === FALSE)
                && ((strpos($Page, "=") !== FALSE)
                    || ((stripos($Page, ".php") === FALSE)
                        && (stripos($Page, ".htm") === FALSE)
                        && (strpos($Page, "/") === FALSE)))
                && (stripos($Page, "http://") !== 0)
                && (stripos($Page, "https://") !== 0))
        {
            $this->JumpToPage = "index.php?P=".$Page;
        }
        else
        {
            $this->JumpToPage = $Page;
        }
    }

    /**
    * Report whether a page to autoload has been set.
    * @return TRUE if page is set to autoload, otherwise FALSE.
    */
    function JumpToPageIsSet()
    {
        return ($this->JumpToPage === NULL) ? FALSE : TRUE;
    }

    /**
    * Get/set HTTP character encoding value.  This is set for the HTTP header and
    * may be queried and set in the HTML header by the active user interface.
    * The default charset is UTF-8.
    * A list of valid character set values can be found at
    * http://www.iana.org/assignments/character-sets
    * @param string $NewSetting New character encoding value string (e.g. "ISO-8859-1").
    * @return Current character encoding value.
    */
    function HtmlCharset($NewSetting = NULL)
    {
        if ($NewSetting !== NULL) {  $this->HtmlCharset = $NewSetting;  }
        return $this->HtmlCharset;
    }

    /**
    * Suppress loading of HTML files.  This is useful when the only output from a
    * page is intended to come from the PHP page file.
    * @param NewSetting TRUE to suppress HTML output, FALSE to not suppress HTML
    *       output.  (OPTIONAL, defaults to TRUE)
    */
    function SuppressHTMLOutput($NewSetting = TRUE)
    {
        $this->SuppressHTML = $NewSetting;
    }

    /**
    * Get/set name of current active user interface.  Any "SPTUI--" prefix is
    * stripped out for backward compatibility in CWIS.
    * @param string $UIName Name of new active user interface.  (OPTIONAL)
    * @return Name of currently active user interface.
    */
    function ActiveUserInterface($UIName = NULL)
    {
        if ($UIName !== NULL)
        {
            $this->ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
        }
        return $this->ActiveUI;
    }

    /**
    * Get the list of available user interfaces. The result contains a map of
    * interface paths to interface labels.
    * @return array list of users interfaces (interface path => label)
    */
    function GetUserInterfaces()
    {
        # possible UI directories
        $InterfaceDirs = array(
            "interface",
            "local/interface");

        # start out with an empty list
        $Interfaces = array();

        # for each possible UI directory
        foreach ($InterfaceDirs as $InterfaceDir)
        {
            $Dir = dir($InterfaceDir);

            # for each file in current directory
            while (($DirEntry = $Dir->read()) !== FALSE)
            {
                $InterfacePath = $InterfaceDir."/".$DirEntry;

                # skip anything that doesn't have a name in the required format
                if (!preg_match('/^[a-zA-Z]+$/', $DirEntry))
                {
                    continue;
                }

                # skip anything that isn't a directory
                if (!is_dir($InterfacePath))
                {
                    continue;
                }

                # read the UI name (if available)
                $UIName = @file_get_contents($InterfacePath."/NAME");

                # use the directory name if the UI name isn't available
                if ($UIName === FALSE || !strlen($UIName))
                {
                    $UIName = $DirEntry;
                }

                $Interfaces[$InterfacePath] = $UIName;
            }

            $Dir->close();
        }

        # return list to caller
        return $Interfaces;
    }

    /**
    * Add function to be called after HTML has been loaded.  The arguments are
    * optional and are saved as references so that any changes to their value
    * that occured while loading the HTML will be recognized.
    * @param string $FunctionName Name of function to be called.
    * @param mixed $Arg1 First argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg2 Second argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg3 Third argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg4 Fourth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg5 FifthFirst argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg6 Sixth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg7 Seventh argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg8 Eighth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param mixed $Arg9 Ninth argument to be passed to function.  (OPTIONAL, REFERENCE)
    */
    function AddPostProcessingCall($FunctionName,
            &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
            &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
            &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
    {
        $FuncIndex = count($this->PostProcessingFuncs);
        $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
        $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
        $Index = 1;
        while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
        {
            $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
                    =& ${"Arg".$Index};
            $Index++;
        }
    }

    /**
    * Add file to be included to set up environment.  This file is loaded right
    * before the PHP file.
    * @param string $FileName Name of file to be included.
    */
    function AddEnvInclude($FileName)
    {
        $this->EnvIncludes[] = $FileName;
    }

    /**
    * Search UI directories for specified image or CSS file and return name
    * of correct file.
    * @param string $FileName Base file name.
    * @return Full relative path name of file or NULL if file not found.
    */
    function GUIFile($FileName)
    {
        # determine which location to search based on file suffix
        $FileIsImage = preg_match("/\.(gif|jpg|png)$/", $FileName);
        $DirList = $FileIsImage ? $this->ImageDirList : $this->IncludeDirList;

        # search for file
        $FoundFileName = $this->FindFile($DirList, $FileName);

        # add non-image files to list of found files (used for required files loading)
        if (!$FileIsImage) {  $this->FoundUIFiles[] = basename($FoundFileName);  }

        # return file name to caller
        return $FoundFileName;
    }

    /**
    * Search UI directories for specified image or CSS file and print name
    * of correct file.  If the file is not found, nothing is printed.
    *
    * This is intended to be called from within interface HTML files to
    * ensure that the correct file is loaded, regardless of which interface
    * it is in.
    * @param string $FileName Base file name.
    */
    function PUIFile($FileName)
    {
        $FullFileName = $this->GUIFile($FileName);
        if ($FullFileName) {  print($FullFileName);  }
    }

    /**
    * Add file to list of required UI files.  This is used to make sure a
    * particular JavaScript or CSS file is loaded.  Only files loaded with
    * ApplicationFramework::GUIFile() or ApplicationFramework::PUIFile()
    * are considered when deciding if a file has already been loaded.
    * @param string $FileName Base name (without path) of required file.
    */
    function RequireUIFile($FileName)
    {
        $this->AdditionalRequiredUIFiles[] = $FileName;
    }

    /**
    * Attempt to load code for function or method if not currently available.
    * Function code to be loaded should be located in a file named "F-XXX.php",
    * where "XXX" is the function name.  The file may reside in "local/include",
    * any of the interface "include" directories, or any of the object directories.
    * @param callback $Callback Function or method info.
    * @return TRUE if function/method is now available, else FALSE.
    */
    function LoadFunction($Callback)
    {
        # if specified function is not currently available
        if (!is_callable($Callback))
        {
            # if function info looks legal
            if (is_string($Callback) && strlen($Callback))
            {
                # start with function directory list
                $Locations = $this->FunctionDirList;

                # add object directories to list
                $Locations = array_merge(
                        $Locations, array_keys(self::$ObjectDirectories));

                # look for function file
                $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
                        array("php", "html"));

                # if function file was found
                if ($FunctionFileName)
                {
                    # load function file
                    include_once($FunctionFileName);
                }
                else
                {
                    # log error indicating function load failed
                    $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
                            ." for callback \"".$Callback."\".");
                }
            }
            else
            {
                # log error indicating specified function info was bad
                $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
                        ." (".$Callback.")"
                        ." passed to AF::LoadFunction().");
            }
        }

        # report to caller whether function load succeeded
        return is_callable($Callback);
    }

    /**
    * Get time elapsed since constructor was called.
    * @return Elapsed execution time in seconds (as a float).
    */
    function GetElapsedExecutionTime()
    {
        return microtime(TRUE) - $this->ExecutionStartTime;
    }

    /**
    * Get remaining available (PHP) execution time.
    * @return Number of seconds remaining before script times out (as a float).
    */
    function GetSecondsBeforeTimeout()
    {
        return ini_get("max_execution_time") - $this->GetElapsedExecutionTime();
    }

    /**
    * Determine if .htaccess files are enabled.
    * @return TRUE if .htaccess files are enabled or FALSE otherwise
    */
    function HtaccessSupport()
    {
        # HTACCESS_SUPPORT is set in the .htaccess file
        return isset($_SERVER["HTACCESS_SUPPORT"]);
    }

    /**
    * Write error message to log.  The difference between this and LogMessage
    * is the way that an inability to write to the log is handled.
    * @param Level Current message logging must be at or above specified
    *       level for error message to be written.  (See LoggingLevel() for
    *       definitions of the error logging levels.)
    * @param Msg Error message text.
    * @return TRUE if message was logged, otherwise FALSE.
    * @see LoggingLevel
    * @see LogMessage
    */
    function LogError($Level, $Msg)
    {
        # if error level is at or below current logging level
        if ($this->Settings["LoggingLevel"] >= $Level)
        {
            # attempt to log error message
            $Result = $this->LogMessage($Level, $Msg);

            # if logging attempt failed and level indicated significant error
            if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
            {
                # throw exception about inability to log error
                static $AlreadyThrewException = FALSE;
                if (!$AlreadyThrewException)
                {
                    $AlreadyThrewException = TRUE;
                    throw new Exception("Unable to log error (".$Level.": ".$Msg.").");
                }
            }

            # report to caller whether message was logged
            return $Result;
        }
        else
        {
            # report to caller that message was not logged
            return FALSE;
        }
    }

    /**
    * Write status message to log.  The difference between this and LogError
    * is the way that an inability to write to the log is handled.
    * @param int $Level Current message logging must be at or above specified
    *       level for message to be written.  (See LoggingLevel() for
    *       definitions of the error logging levels.)
    * @param string $Msg Message text.
    * @return TRUE if message was logged, otherwise FALSE.
    * @see ApplicationFramework::LoggingLevel()
    * @see ApplicationFramework::LogError()
    */
    function LogMessage($Level, $Msg)
    {
        # if message level is at or below current logging level
        if ($this->Settings["LoggingLevel"] >= $Level)
        {
            # attempt to open log file
            $FHndl = @fopen("local/logs/cwis.log", "a");

            # if log file could not be open
            if ($FHndl === FALSE)
            {
                # report to caller that message was not logged
                return FALSE;
            }
            else
            {
                # format log entry
                $ErrorAbbrevs = array(
                        self::LOGLVL_FATAL => "FTL",
                        self::LOGLVL_ERROR => "ERR",
                        self::LOGLVL_WARNING => "WRN",
                        self::LOGLVL_INFO => "INF",
                        self::LOGLVL_DEBUG => "DBG",
                        self::LOGLVL_TRACE => "TRC",
                        );
                $LogEntry = date("Y-m-d H:i:s")
                        ." ".($this->RunningInBackground ? "B" : "F")
                        ." ".$ErrorAbbrevs[$Level]
                        ." ".$Msg;

                # write entry to log
                $Success = fputs($FHndl, $LogEntry."\n");

                # close log file
                fclose($FHndl);

                # report to caller whether message was logged
                return ($Success === FALSE) ? FALSE : TRUE;
            }
        }
        else
        {
            # report to caller that message was not logged
            return FALSE;
        }
    }

    /**
    * Get/set logging level.  Status and error messages are only written if
    * their associated level is at or below this value.  The six levels of
    * log messages are, in increasing level of severity:
    *   6: TRACE - Very detailed logging, usually only used when attempting
    *       to diagnose a problem in one specific section of code.
    *   5: DEBUG - Information that is diagnostically helpful when debugging.
    *   4: INFO - Generally-useful information, that may come in handy but
    *       to which little attention is normally paid.  (This should not be
    *       used for events that routinely occur with every page load.)
    *   3: WARNING - An event that may potentially cause problems, but is
    *       automatically recovered from.
    *   2: ERROR - Any error which is fatal to the operation currently being
    *       performed, but does not result in overall application shutdown or
    *       persistent data corruption.
    *   1: FATAL - Any error which results in overall application shutdown or
    *       persistent data corruption.
    * @param int $NewValue New error logging level.  (OPTIONAL)
    * @return Current error logging level.
    * @see ApplicationFramework::LogError()
    */
    function LoggingLevel($NewValue = NULL)
    {
        # if new logging level was specified
        if ($NewValue !== NULL)
        {
            # constrain new level to within legal bounds and store locally
            $this->Settings["LoggingLevel"] = max(min($NewValue, 6), 1);

            # save new logging level in database
            $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                    ." SET LoggingLevel = "
                    .intval($this->Settings["LoggingLevel"]));
        }

        # report current logging level to caller
        return $this->Settings["LoggingLevel"];
    }

    /**
    * TRACE error logging level.  Very detailed logging, usually only used
    * when attempting to diagnose a problem in one specific section of code.
    */
    const LOGLVL_TRACE = 6;
    /**
    * DEBUG error logging leve.  Information that is diagnostically helpful
    * when debugging.
    */
    const LOGLVL_DEBUG = 5;
    /**
    * INFO error logging level.  Generally-useful information, that may
    * come in handy but to which little attention is normally paid.  (This
    * should not be used for events that routinely occur with every page load.)
    */
    const LOGLVL_INFO = 4;
    /**
    * WARNING error logging level.  An event that may potentially cause
    * problems, but is automatically recovered from.
    */
    const LOGLVL_WARNING = 3;
    /**
    * ERROR error logging level.  Any error which is fatal to the operation
    * currently being performed, but does not result in overall application
    * shutdown or persistent data corruption.
    */
    const LOGLVL_ERROR = 2;
    /**
    * FATAL error logging level.  Any error which results in overall
    * application shutdown or persistent data corruption.
    */
    const LOGLVL_FATAL = 1;

    /*@)*/ /* Application Framework */

    # ---- Event Handling ----------------------------------------------------

    /** @name Event Handling */ /*@(*/

    /**
    * Default event type.  Any handler return values are ignored.
    */
    const EVENTTYPE_DEFAULT = 1;
    /**
    * Result chaining event type.  For this type the parameter array to each
    * event handler is the return value from the previous handler, and the
    * final return value is sent back to the event signaller.
    */
    const EVENTTYPE_CHAIN = 2;
    /**
    * First response event type.  For this type event handlers are called
    * until one returns a non-NULL result, at which point no further handlers
    * are called and that last result is passed back to the event signaller.
    */
    const EVENTTYPE_FIRST = 3;
    /**
    * Named result event type.  Return values from each handler are placed into an
    * array with the handler (function or class::method) name as the index, and
    * that array is returned to the event signaller.  The handler name for
    * class methods is the class name plus "::" plus the method name.
    * are called and that last result is passed back to the event signaller.
    */
    const EVENTTYPE_NAMED = 4;

    /** Run hooked function first (i.e. before ORDER_MIDDLE events). */
    const ORDER_FIRST = 1;
    /** Run hooked function after ORDER_FIRST and before ORDER_LAST events. */
    const ORDER_MIDDLE = 2;
    /** Run hooked function last (i.e. after ORDER_MIDDLE events). */
    const ORDER_LAST = 3;

    /**
    * Register one or more events that may be signaled.
    * @param array|string $EventsOrEventName Name of event (string).  To register multiple
    *       events, this may also be an array, with the event names as the index
    *       and the event types as the values.
    * @param int $EventType Type of event (constant).  (OPTIONAL if EventsOrEventName
    *       is an array of events)
    */
    function RegisterEvent($EventsOrEventName, $EventType = NULL)
    {
        # convert parameters to array if not already in that form
        $Events = is_array($EventsOrEventName) ? $EventsOrEventName
                : array($EventsOrEventName => $Type);

        # for each event
        foreach ($Events as $Name => $Type)
        {
            # store event information
            $this->RegisteredEvents[$Name]["Type"] = $Type;
            $this->RegisteredEvents[$Name]["Hooks"] = array();
        }
    }

    /**
    * Check if event has been registered (is available to be signaled).
    * @param string $EventName Name of event (string).
    * @return TRUE if event is registered, otherwise FALSE.
    */
    function IsRegisteredEvent($EventName)
    {
        return array_key_exists($EventName, $this->RegisteredEvents)
                ? TRUE : FALSE;
    }

    /**
    * Hook one or more functions to be called when the specified event is
    * signaled.  The callback parameter is of the PHP type "callback", which
    * allows object methods to be passed.
    * @param array|string $EventsOrEventName Name of the event to hook.  To hook multiple
    *       events, this may also be an array, with the event names as the index
    *       and the callbacks as the values.
    * @param callback $Callback Function to be called when event is signaled.  (OPTIONAL
    *       if EventsOrEventName is an array of events)
    * @param int $Order Preference for when function should be called, primarily for
    *       CHAIN and FIRST events.  (OPTIONAL, defaults to ORDER_MIDDLE)
    * @return TRUE if all callbacks were successfully hooked, otherwise FALSE.
    */
    function HookEvent($EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
    {
        # convert parameters to array if not already in that form
        $Events = is_array($EventsOrEventName) ? $EventsOrEventName
                : array($EventsOrEventName => $Callback);

        # for each event
        $Success = TRUE;
        foreach ($Events as $EventName => $EventCallback)
        {
            # if callback is valid
            if (is_callable($EventCallback))
            {
                # if this is a periodic event we process internally
                if (isset($this->PeriodicEvents[$EventName]))
                {
                    # process event now
                    $this->ProcessPeriodicEvent($EventName, $EventCallback);
                }
                # if specified event has been registered
                elseif (isset($this->RegisteredEvents[$EventName]))
                {
                    # add callback for event
                    $this->RegisteredEvents[$EventName]["Hooks"][]
                            = array("Callback" => $EventCallback, "Order" => $Order);

                    # sort callbacks by order
                    if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
                    {
                        usort($this->RegisteredEvents[$EventName]["Hooks"],
                                array("ApplicationFramework", "HookEvent_OrderCompare"));
                    }
                }
                else
                {
                    $Success = FALSE;
                }
            }
            else
            {
                $Success = FALSE;
            }
        }

        # report to caller whether all callbacks were hooked
        return $Success;
    }
    private static function HookEvent_OrderCompare($A, $B)
    {
        if ($A["Order"] == $B["Order"]) {  return 0;  }
        return ($A["Order"] < $B["Order"]) ? -1 : 1;
    }

    /**
    * Signal that an event has occured.
    * @param string $EventName Name of event being signaled.
    * @param array $Parameters Associative array of parameters for event, with CamelCase
    *       parameter names as indexes.  (OPTIONAL)
    * @return Appropriate return value for event type.  Returns NULL if no event
    *       with specified name was registered and for EVENTTYPE_DEFAULT events.
    */
    function SignalEvent($EventName, $Parameters = NULL)
    {
        $ReturnValue = NULL;

        # if event has been registered
        if (isset($this->RegisteredEvents[$EventName]))
        {
            # set up default return value (if not NULL)
            switch ($this->RegisteredEvents[$EventName]["Type"])
            {
                case self::EVENTTYPE_CHAIN:
                    $ReturnValue = $Parameters;
                    break;

                case self::EVENTTYPE_NAMED:
                    $ReturnValue = array();
                    break;
            }

            # for each callback for this event
            foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
            {
                # invoke callback
                $Callback = $Hook["Callback"];
                $Result = ($Parameters !== NULL)
                        ? call_user_func_array($Callback, $Parameters)
                        : call_user_func($Callback);

                # process return value based on event type
                switch ($this->RegisteredEvents[$EventName]["Type"])
                {
                    case self::EVENTTYPE_CHAIN:
                        if ($Result !== NULL)
                        {
                            foreach ($Parameters as $Index => $Value)
                            {
                                if (array_key_exists($Index, $Result))
                                {
                                    $Parameters[$Index] = $Result[$Index];
                                }
                            }
                            $ReturnValue = $Parameters;
                        }
                        break;

                    case self::EVENTTYPE_FIRST:
                        if ($Result !== NULL)
                        {
                            $ReturnValue = $Result;
                            break 2;
                        }
                        break;

                    case self::EVENTTYPE_NAMED:
                        $CallbackName = is_array($Callback)
                                ? (is_object($Callback[0])
                                        ? get_class($Callback[0])
                                        : $Callback[0])."::".$Callback[1]
                                : $Callback;
                        $ReturnValue[$CallbackName] = $Result;
                        break;

                    default:
                        break;
                }
            }
        }
        else
        {
            $this->LogError(self::LOGLVL_WARNING,
                    "Unregistered event signaled (".$EventName.").");
        }

        # return value if any to caller
        return $ReturnValue;
    }

    /**
    * Report whether specified event only allows static callbacks.
    * @param string $EventName Name of event to check.
    * @return TRUE if specified event only allows static callbacks, otherwise FALSE.
    */
    function IsStaticOnlyEvent($EventName)
    {
        return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
    }

    /*@)*/ /* Event Handling */

    # ---- Task Management ---------------------------------------------------

    /** @name Task Management */ /*@(*/

    /**  Highest priority. */
    const PRIORITY_HIGH = 1;
    /**  Medium (default) priority. */
    const PRIORITY_MEDIUM = 2;
    /**  Lower priority. */
    const PRIORITY_LOW = 3;
    /**  Lowest priority. */
    const PRIORITY_BACKGROUND = 4;

    /**
    * Add task to queue.  The Callback parameters is the PHP "callback" type.
    * If $Callback refers to a function (rather than an object method) that function
    * must be available in a global scope on all pages or must be loadable by
    * ApplicationFramework::LoadFunction().
    * @param callback $Callback Function or method to call to perform task.
    * @param array $Parameters Array containing parameters to pass to function or
    *       method.  (OPTIONAL, pass NULL for no parameters)
    * @param int $Priority Priority to assign to task.  (OPTIONAL, defaults
    *       to PRIORITY_MEDIUM)
    * @param string $Description Text description of task.  (OPTIONAL)
    */
    function QueueTask($Callback, $Parameters = NULL,
            $Priority = self::PRIORITY_MEDIUM, $Description = "")
    {
        # pack task info and write to database
        if ($Parameters === NULL) {  $Parameters = array();  }
        $this->DB->Query("INSERT INTO TaskQueue"
                ." (Callback, Parameters, Priority, Description)"
                ." VALUES ('".addslashes(serialize($Callback))."', '"
                .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
                .addslashes($Description)."')");
    }

    /**
    * Add task to queue if not already in queue or currently running.
    * If task is already in queue with a lower priority than specified, the task's
    * priority will be increased to the new value.
    * The Callback parameter is the PHP "callback" type.
    * If $Callback refers to a function (rather than an object method) that function
    * must be available in a global scope on all pages or must be loadable by
    * ApplicationFramework::LoadFunction().
    * @param callback $Callback Function or method to call to perform task.
    * @param array $Parameters Array containing parameters to pass to function or
    *       method.  (OPTIONAL, pass NULL for no parameters)
    * @param int $Priority Priority to assign to task.  (OPTIONAL, defaults
    *       to PRIORITY_MEDIUM)
    * @param string $Description Text description of task.  (OPTIONAL)
    * @return TRUE if task was added, otherwise FALSE.
    * @see ApplicationFramework::TaskIsInQueue()
    */
    function QueueUniqueTask($Callback, $Parameters = NULL,
            $Priority = self::PRIORITY_MEDIUM, $Description = "")
    {
        if ($this->TaskIsInQueue($Callback, $Parameters))
        {
            $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
                    ." WHERE Callback = '".addslashes(serialize($Callback))."'"
                    .($Parameters ? " AND Parameters = '"
                            .addslashes(serialize($Parameters))."'" : ""));
            if ($QueryResult !== FALSE)
            {
                $Record = $this->DB->FetchRow();
                if ($Record["Priority"] > $Priority)
                {
                    $this->DB->Query("UPDATE TaskQueue"
                            ." SET Priority = ".intval($Priority)
                            ." WHERE TaskId = ".intval($Record["TaskId"]));
                }
            }
            return FALSE;
        }
        else
        {
            $this->QueueTask($Callback, $Parameters, $Priority, $Description);
            return TRUE;
        }
    }

    /**
    * Check if task is already in queue or currently running.
    * When no $Parameters value is specified the task is checked against
    * any other entries with the same $Callback.
    * @param callback $Callback Function or method to call to perform task.
    * @param array $Parameters Array containing parameters to pass to function or
    *       method.  (OPTIONAL)
    * @return TRUE if task is already in queue, otherwise FALSE.
    */
    function TaskIsInQueue($Callback, $Parameters = NULL)
    {
        $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount FROM TaskQueue"
                ." WHERE Callback = '".addslashes(serialize($Callback))."'"
                .($Parameters ? " AND Parameters = '"
                        .addslashes(serialize($Parameters))."'" : ""),
                "FoundCount")
                + $this->DB->Query("SELECT COUNT(*) AS FoundCount FROM RunningTasks"
                ." WHERE Callback = '".addslashes(serialize($Callback))."'"
                .($Parameters ? " AND Parameters = '"
                        .addslashes(serialize($Parameters))."'" : ""),
                "FoundCount");
        return ($FoundCount ? TRUE : FALSE);
    }

    /**
    * Retrieve current number of tasks in queue.
    * @param int $Priority of tasks.  (OPTIONAL, defaults to all priorities)
    * @return Number of tasks currently in queue.
    */
    function GetTaskQueueSize($Priority = NULL)
    {
        return $this->DB->Query("SELECT COUNT(*) AS QueueSize FROM TaskQueue"
                .($Priority ? " WHERE Priority = ".intval($Priority) : ""),
                "QueueSize");
    }

    /**
    * Retrieve list of tasks currently in queue.
    * @param int $Count Number to retrieve.  (OPTIONAL, defaults to 100)
    * @param int $Offset Offset into queue to start retrieval.  (OPTIONAL)
    * @return Array with task IDs for index and task info for values.  Task info
    *       is stored as associative array with "Callback" and "Parameter" indices.
    */
    function GetQueuedTaskList($Count = 100, $Offset = 0)
    {
        return $this->GetTaskList("SELECT * FROM TaskQueue"
                ." ORDER BY Priority, TaskId ", $Count, $Offset);
    }

    /**
    * Retrieve list of tasks currently in queue.
    * @param int $Count Number to retrieve.  (OPTIONAL, defaults to 100)
    * @param int $Offset Offset into queue to start retrieval.  (OPTIONAL)
    * @return Array with task IDs for index and task info for values.  Task info
    *       is stored as associative array with "Callback" and "Parameter" indices.
    */
    function GetRunningTaskList($Count = 100, $Offset = 0)
    {
        return $this->GetTaskList("SELECT * FROM RunningTasks"
                ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
                        (time() - ini_get("max_execution_time")))."'"
                ." ORDER BY StartedAt", $Count, $Offset);
    }

    /**
    * Retrieve list of tasks currently in queue.
    * @param int $Count Number to retrieve.  (OPTIONAL, defaults to 100)
    * @param int $Offset Offset into queue to start retrieval.  (OPTIONAL)
    * @return Array with task IDs for index and task info for values.  Task info
    *       is stored as associative array with "Callback" and "Parameter" indices.
    */
    function GetOrphanedTaskList($Count = 100, $Offset = 0)
    {
        return $this->GetTaskList("SELECT * FROM RunningTasks"
                ." WHERE StartedAt < '".date("Y-m-d H:i:s",
                        (time() - ini_get("max_execution_time")))."'"
                ." ORDER BY StartedAt", $Count, $Offset);
    }

    /**
    * Retrieve current number of orphaned tasks.
    * @return Number of orphaned tasks.
    */
    function GetOrphanedTaskCount()
    {
        return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
                ." WHERE StartedAt < '".date("Y-m-d H:i:s",
                        (time() - ini_get("max_execution_time")))."'",
                "Count");
    }

    /**
    * Move orphaned task back into queue.
    * @param int $TaskId Task ID.
    * @param int $NewPriority New priority for task being requeued.  (OPTIONAL)
    */
    function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
    {
        $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
        $this->DB->Query("INSERT INTO TaskQueue"
                ." (Callback,Parameters,Priority,Description) "
                ."SELECT Callback, Parameters, Priority, Description"
                ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
        if ($NewPriority !== NULL)
        {
            $NewTaskId = $this->DB->LastInsertId("TaskQueue");
            $this->DB->Query("UPDATE TaskQueue SET Priority = "
                            .intval($NewPriority)
                    ." WHERE TaskId = ".intval($NewTaskId));
        }
        $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
        $this->DB->Query("UNLOCK TABLES");
    }

    /**
    * Remove task from task queues.
    * @param int $TaskId Task ID.
    */
    function DeleteTask($TaskId)
    {
        $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
        $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
    }

    /**
    * Retrieve task info from queue (either running or queued tasks).
    * @param int $TaskId Task ID.
    * @return Array with task info for values or NULL if task is not found.
    *       Task info is stored as associative array with "Callback" and
    *       "Parameter" indices.
    */
    function GetTask($TaskId)
    {
        # assume task will not be found
        $Task = NULL;

        # look for task in task queue
        $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));

        # if task was not found in queue
        if (!$this->DB->NumRowsSelected())
        {
            # look for task in running task list
            $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
                    .intval($TaskId));
        }

        # if task was found
        if ($this->DB->NumRowsSelected())
        {
            # if task was periodic
            $Row = $this->DB->FetchRow();
            if ($Row["Callback"] ==
                    serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
            {
                # unpack periodic task callback
                $WrappedCallback = unserialize($Row["Parameters"]);
                $Task["Callback"] = $WrappedCallback[1];
                $Task["Parameters"] = $WrappedCallback[2];
            }
            else
            {
                # unpack task callback and parameters
                $Task["Callback"] = unserialize($Row["Callback"]);
                $Task["Parameters"] = unserialize($Row["Parameters"]);
            }
        }

        # return task to caller
        return $Task;
    }

    /**
    * Get/set whether automatic task execution is enabled.  (This does not
    * prevent tasks from being manually executed.)
    * @param bool $NewValue TRUE to enable or FALSE to disable.
    * @return Returns TRUE if automatic task execution is enabled or
    *       otherwise FALSE.
    */
    function TaskExecutionEnabled($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            $this->Settings["TaskExecutionEnabled"] = $NewValue ? 1 : 0;
            $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                    ." SET TaskExecutionEnabled = "
                    .$this->Settings["TaskExecutionEnabled"]);
        }
        return $this->Settings["TaskExecutionEnabled"];
    }

    /**
    * Get/set maximum number of tasks to have running simultaneously.
    * @param int $NewValue New setting for max number of tasks.  (OPTIONAL)
    * @return Current maximum number of tasks to run at once.
    */
    function MaxTasks($NewValue = NULL)
    {
        if (func_num_args() && ($NewValue >= 1))
        {
            $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                    ." SET MaxTasksRunning = ".intval($NewValue));
            $this->Settings["MaxTasksRunning"] = intval($NewValue);
        }
        return $this->Settings["MaxTasksRunning"];
    }

    /**
    * Get/set maximum PHP execution time.  Setting a new value is not possible if
    *       PHP is running in safe mode.
    * @param int $NewValue New setting for max execution time in seconds.  (OPTIONAL,
    *       but minimum value is 5 if specified)
    * @return Current max execution time in seconds.
    */
    function MaxExecutionTime($NewValue = NULL)
    {
        if (func_num_args() && !ini_get("safe_mode"))
        {
            if ($NewValue != $this->Settings["MaxExecTime"])
            {
                $this->Settings["MaxExecTime"] = max($NewValue, 5);
                $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                        ." SET MaxExecTime = '"
                                .intval($this->Settings["MaxExecTime"])."'");
            }
            ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
            set_time_limit($this->Settings["MaxExecTime"]);
        }
        return ini_get("max_execution_time");
    }

    /*@)*/ /* Task Management */

    # ---- Backward Compatibility --------------------------------------------

    /**
    * Preserved for backward compatibility for use with code written prior
    * to October 2012.
    */
    function FindCommonTemplate($BaseName)
    {
        return $this->FindFile(
                $this->IncludeDirList, $BaseName, array("tpl", "html"));
    }

    /** @name Backward Compatibility */ /*@(*/


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $ActiveUI = "default";
    private $BrowserDetectFunc;
    private $DB;
    private $DefaultPage = "Home";
    private $EnvIncludes = array();
    private $ExecutionStartTime;
    private $FoundUIFiles = array();
    private $AdditionalRequiredUIFiles = array();
    private $HtmlCharset = "UTF-8";
    private $JumpToPage = NULL;
    private $PageName;
    private $MaxRunningTasksToTrack = 250;
    private $PostProcessingFuncs = array();
    private $RunningInBackground = FALSE;
    private $RunningTask;
    private $Settings;
    private $SuppressHTML = FALSE;
    private $SaveTemplateLocationCache = FALSE;
    private $UnbufferedCallbacks = array();

    private static $AppName = "ScoutAF";
    private static $ObjectDirectories = array();
    private static $SessionLifetime = 1440;

    /**
    * Set to TRUE to not close browser connection before running
    *       background tasks (useful when debugging)
    */
    private $NoTSR = FALSE;

    private $PeriodicEvents = array(
                "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
                "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
                "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
                "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
                "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
                );
    private $UIEvents = array(
                "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
                "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
                "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
                "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
                "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
                );

    /**
    * Load our settings from database, initializing them if needed.
    */
    private function LoadSettings()
    {
        # read settings in from database
        $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
        $this->Settings = $this->DB->FetchRow();

        # if settings were not previously initialized
        if (!$this->Settings)
        {
            # initialize settings in database
            $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
                    ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");

            # read new settings in from database
            $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
            $this->Settings = $this->DB->FetchRow();
        }

        # if template location cache has been saved to database
        if (isset($this->Settings["TemplateLocationCache"]))
        {
            # unserialize cache values into array and use if valid
            $Cache = unserialize($this->Settings["TemplateLocationCache"]);
            $this->Settings["TemplateLocationCache"] =
                    count($Cache) ? $Cache : array();
        }
        else
        {
            # start with empty cache
            $this->Settings["TemplateLocationCache"] = array();
        }
    }

    /**
    * Look for template file in supplied list of possible locations,
    * including the currently active UI in the location path where
    * indicated.  Locations are read from a cache, which is discarded
    * when the cache expiration time is reached.  If updated, the cache
    * is saved to the database in __destruct().
    * @param DirectoryList Array of directories (or array of arrays
    *       of directories) to search.  Directories must include a
    *       trailing slash.
    * @param BaseName File name or file name base.
    * @param PossibleSuffixes Array with possible suffixes for file
    *       name, if no suffix evident.  (Suffixes should not include
    *       a leading period.)  (OPTIONAL)
    * @param PossiblePrefixes Array with possible prefixes for file to
    *       check.  (OPTIONAL)
    */
    private function FindFile($DirectoryList, $BaseName,
            $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
    {
        # generate template cache index for this page
        $CacheIndex = md5(serialize($DirectoryList))
                .":".$this->ActiveUI.":".$BaseName;

        # if we have cached location and cache expiration time has not elapsed
        if (($this->Settings["TemplateLocationCacheInterval"] > 0)
                && count($this->Settings["TemplateLocationCache"])
                && array_key_exists($CacheIndex,
                        $this->Settings["TemplateLocationCache"])
                && (time() < strtotime(
                        $this->Settings["TemplateLocationCacheExpiration"])))
        {
            # use template location from cache
            $FoundFileName = $this->Settings[
                    "TemplateLocationCache"][$CacheIndex];
        }
        else
        {
            # if suffixes specified and base name does not include suffix
            if (count($PossibleSuffixes)
                    && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
            {
                # add versions of file names with suffixes to file name list
                $FileNames = array();
                foreach ($PossibleSuffixes as $Suffix)
                {
                    $FileNames[] = $BaseName.".".$Suffix;
                }
            }
            else
            {
                # use base name as file name
                $FileNames = array($BaseName);
            }

            # if prefixes specified
            if (count($PossiblePrefixes))
            {
                # add versions of file names with prefixes to file name list
                $NewFileNames = array();
                foreach ($FileNames as $FileName)
                {
                    foreach ($PossiblePrefixes as $Prefix)
                    {
                        $NewFileNames[] = $Prefix.$FileName;
                    }
                }
                $FileNames = $NewFileNames;
            }

            # for each possible location
            $FoundFileName = NULL;
            foreach ($DirectoryList as $Dir)
            {
                # substitute active UI name into path
                $Dir = str_replace("%ACTIVEUI%", $this->ActiveUI, $Dir);

                # for each possible file name
                foreach ($FileNames as $File)
                {
                    # if template is found at location
                    if (file_exists($Dir.$File))
                    {
                        # save full template file name and stop looking
                        $FoundFileName = $Dir.$File;
                        break 2;
                    }
                }
            }

            # save location in cache
            $this->Settings["TemplateLocationCache"][$CacheIndex]
                    = $FoundFileName;

            # set flag indicating that cache should be saved
            $this->SaveTemplateLocationCache = TRUE;
        }

        # return full template file name to caller
        return $FoundFileName;
    }

    /**
    * Figure out which required UI files have not yet been loaded for specified
    * page content file.
    * @param PageContentFile Page content file.
    * @return Array of names of required files (without paths).
    */
    private function GetRequiredFilesNotYetLoaded($PageContentFile)
    {
        # start out assuming no files required
        $RequiredFiles = array();

        # if page content file supplied
        if ($PageContentFile)
        {
            # if file containing list of required files is available
            $Path = dirname($PageContentFile);
            $RequireListFile = $Path."/REQUIRES";
            if (file_exists($RequireListFile))
            {
                # read in list of required files
                $RequestedFiles = file($RequireListFile);

                # for each line in required file list
                foreach ($RequestedFiles as $Line)
                {
                    # if line is not a comment
                    $Line = trim($Line);
                    if (!preg_match("/^#/", $Line))
                    {
                        # if file has not already been loaded
                        if (!in_array($Line, $this->FoundUIFiles))
                        {
                            # add to list of required files
                            $RequiredFiles[] = $Line;
                        }
                    }
                }
            }
        }

        # add in additional required files if any
        if (count($this->AdditionalRequiredUIFiles))
        {
            # make sure there are no duplicates
            $AdditionalRequiredUIFiles = array_unique(
                $this->AdditionalRequiredUIFiles);

            $RequiredFiles = array_merge(
                    $RequiredFiles, $AdditionalRequiredUIFiles);
        }

        # return list of required files to caller
        return $RequiredFiles;
    }

    /**
    * Set up autoloading of object files.
    */
    private function SetUpObjectAutoloading()
    {
        function __autoload($ClassName)
        {
            ApplicationFramework::AutoloadObjects($ClassName);
        }
    }

    /** @cond */
    /**
    * Load object file for specified class.
    * @param string $ClassName Name of class.
    */
    static function AutoloadObjects($ClassName)
    {
        static $FileLists;
        foreach (self::$ObjectDirectories as $Location => $Info)
        {
            if (is_dir($Location))
            {
                $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
                        ? preg_replace($Info["ClassPattern"],
                                $Info["ClassReplacement"], $ClassName)
                        : $ClassName;
                if (!isset($FileLists[$Location]))
                {
                    $FileLists[$Location] = self::ReadDirectoryTree(
                            $Location, '/^.+\.php$/i');
                }
                $FileNames = $FileLists[$Location];
                $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
                foreach ($FileNames as $FileName)
                {
                    if (strtolower($FileName) == $TargetName)
                    {
                        require_once($Location.$FileName);
                        break 2;
                    }
                }
            }
        }
    }

    /**
    * Recursively read in list of file names matching pattern beginning at
    * specified directory.
    * @param string $Directory Directory at top of tree to search.
    * @param string $Pattern Regular expression pattern to match.
    * @return array Array containing names of matching files with relative paths.
    */
    private static function ReadDirectoryTree($Directory, $Pattern)
    {
        $CurrentDir = getcwd();
        chdir($Directory);
        $DirIter = new RecursiveDirectoryIterator(".");
        $IterIter = new RecursiveIteratorIterator($DirIter);
        $RegexResults = new RegexIterator($IterIter, $Pattern,
                RecursiveRegexIterator::GET_MATCH);
        $FileList = array();
        foreach ($RegexResults as $Result)
        {
            $FileList[] = substr($Result[0], 2);
        }
        chdir($CurrentDir);
        return $FileList;
    }
    /** @endcond */

    /**
    * Load any user interface functions available in interface include
    * directories (in F-FuncName.html or F-FuncName.php files).
    */
    private function LoadUIFunctions()
    {
        $Dirs = array(
                "local/interface/%ACTIVEUI%/include",
                "interface/%ACTIVEUI%/include",
                "local/interface/default/include",
                "interface/default/include",
                );
        foreach ($Dirs as $Dir)
        {
            $Dir = str_replace("%ACTIVEUI%", $this->ActiveUI, $Dir);
            if (is_dir($Dir))
            {
                $FileNames = scandir($Dir);
                foreach ($FileNames as $FileName)
                {
                    if (preg_match("/^F-([A-Za-z_]+)\.php/", $FileName, $Matches)
                            || preg_match("/^F-([A-Za-z_]+)\.html/", $FileName, $Matches))
                    {
                        if (!function_exists($Matches[1]))
                        {
                            include_once($Dir."/".$FileName);
                        }
                    }
                }
            }
        }
    }

    /**
    * Run periodic event if appropriate.
    * @param string $EventName Name of event.
    * @param callback $Callback Event callback.
    */
    private function ProcessPeriodicEvent($EventName, $Callback)
    {
        # retrieve last execution time for event if available
        $Signature = self::GetCallbackSignature($Callback);
        $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
                ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");

        # determine whether enough time has passed for event to execute
        $EventPeriods = array(
                "EVENT_HOURLY" => 60*60,
                "EVENT_DAILY" => 60*60*24,
                "EVENT_WEEKLY" => 60*60*24*7,
                "EVENT_MONTHLY" => 60*60*24*30,
                "EVENT_PERIODIC" => 0,
                );
        $ShouldExecute = (($LastRun === NULL)
                || (time() > (strtotime($LastRun) + $EventPeriods[$EventName])))
                ? TRUE : FALSE;

        # if event should run
        if ($ShouldExecute)
        {
            # add event to task queue
            $WrapperCallback = array("ApplicationFramework", "PeriodicEventWrapper");
            $WrapperParameters = array(
                    $EventName, $Callback, array("LastRunAt" => $LastRun));
            $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
        }
    }

    /**
    * Wrapper to run periodic events and then save info needed to know when to
    * run them again.
    * @param string $EventName Name of event.
    * @param callback $Callback Event callback.
    * @param array $Parameters Array of parameters to pass to event.
    */
    private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
    {
        static $DB;
        if (!isset($DB)) {  $DB = new Database();  }

        # run event
        $ReturnVal = call_user_func_array($Callback, $Parameters);

        # if event is already in database
        $Signature = self::GetCallbackSignature($Callback);
        if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
                ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
        {
            # update last run time for event
            $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
                    .(($EventName == "EVENT_PERIODIC")
                            ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
                            : "NOW()")
                    ." WHERE Signature = '".addslashes($Signature)."'");
        }
        else
        {
            # add last run time for event to database
            $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
                    ."('".addslashes($Signature)."', "
                    .(($EventName == "EVENT_PERIODIC")
                            ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
                            : "NOW()").")");
        }
    }

    /**
    * Generate and return signature for specified callback.
    * @param callback $Callback Callback to generate signature for.
    * @return string Signature string.
    */
    private static function GetCallbackSignature($Callback)
    {
        return !is_array($Callback) ? $Callback
                : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
                        ."::".$Callback[1];
    }

    /**
    * Prepare environment for eventual background task execution.
    * @return bool TRUE if there are tasks to be run, otherwise FALSE.
    */
    private function PrepForTSR()
    {
        # if HTML has been output and it's time to launch another task
        # (only TSR if HTML has been output because otherwise browsers
        #       may misbehave after connection is closed)
        if (($this->JumpToPage || !$this->SuppressHTML)
                && (time() > (strtotime($this->Settings["LastTaskRunAt"])
                        + (ini_get("max_execution_time")
                                / $this->Settings["MaxTasksRunning"]) + 5))
                && $this->GetTaskQueueSize()
                && $this->Settings["TaskExecutionEnabled"])
        {
            # begin buffering output for TSR
            ob_start();

            # let caller know it is time to launch another task
            return TRUE;
        }
        else
        {
            # let caller know it is not time to launch another task
            return FALSE;
        }
    }

    /**
    * Attempt to close out page loading with the browser and then execute
    * background tasks.
    */
    private function LaunchTSR()
    {
        # set headers to close out connection to browser
        if (!$this->NoTSR)
        {
            ignore_user_abort(TRUE);
            header("Connection: close");
            header("Content-Length: ".ob_get_length());
        }

        # output buffered content
        ob_end_flush();
        flush();

        # write out any outstanding data and end HTTP session
        session_write_close();

        # set flag indicating that we are now running in background
        $this->RunningInBackground = TRUE;

        # if there is still a task in the queue
        if ($this->GetTaskQueueSize())
        {
            # turn on output buffering to (hopefully) record any crash output
            ob_start();

            # lock tables and grab last task run time to double check
            $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
            $this->LoadSettings();

            # if still time to launch another task
            if (time() > (strtotime($this->Settings["LastTaskRunAt"])
                        + (ini_get("max_execution_time")
                                / $this->Settings["MaxTasksRunning"]) + 5))
            {
                # update the "last run" time and release tables
                $this->DB->Query("UPDATE ApplicationFrameworkSettings"
                        ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
                $this->DB->Query("UNLOCK TABLES");

                # run tasks while there is a task in the queue and enough time left
                do
                {
                    # run the next task
                    $this->RunNextTask();
                }
                while ($this->GetTaskQueueSize()
                        && ($this->GetSecondsBeforeTimeout() > 65));
            }
            else
            {
                # release tables
                $this->DB->Query("UNLOCK TABLES");
            }
        }
    }

    /**
    * Retrieve list of tasks with specified query.
    * @param Count Number to retrieve.  (OPTIONAL, defaults to 100)
    * @param Offset Offset into queue to start retrieval.  (OPTIONAL)
    * @return Array with task IDs for index and task info for values.  Task info
    *       is stored as associative array with "Callback" and "Parameter" indices.
    */
    private function GetTaskList($DBQuery, $Count, $Offset)
    {
        $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
        $Tasks = array();
        while ($Row = $this->DB->FetchRow())
        {
            $Tasks[$Row["TaskId"]] = $Row;
            if ($Row["Callback"] ==
                    serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
            {
                $WrappedCallback = unserialize($Row["Parameters"]);
                $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
                $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
            }
            else
            {
                $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
                $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
            }
        }
        return $Tasks;
    }

    /**
    * Run the next task in the queue.
    */
    private function RunNextTask()
    {
        # look for task at head of queue
        $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
        $Task = $this->DB->FetchRow();

        # if there was a task available
        if ($Task)
        {
            # move task from queue to running tasks list
            $this->DB->Query("INSERT INTO RunningTasks "
                             ."(TaskId,Callback,Parameters,Priority,Description) "
                             ."SELECT * FROM TaskQueue WHERE TaskId = "
                                    .intval($Task["TaskId"]));
            $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
                    .intval($Task["TaskId"]));

            # unpack stored task info
            $Callback = unserialize($Task["Callback"]);
            $Parameters = unserialize($Task["Parameters"]);

            # attempt to load task callback if not already available
            $this->LoadFunction($Callback);

            # run task
            $this->RunningTask = $Task;
            if ($Parameters)
            {
                call_user_func_array($Callback, $Parameters);
            }
            else
            {
                call_user_func($Callback);
            }
            unset($this->RunningTask);

            # remove task from running tasks list
            $this->DB->Query("DELETE FROM RunningTasks"
                    ." WHERE TaskId = ".intval($Task["TaskId"]));

            # prune running tasks list if necessary
            $RunningTasksCount = $this->DB->Query(
                    "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
            if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
            {
                $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
                        ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
            }
        }
    }

    /**
    * Called automatically at program termination to ensure output is written out.
    * (Not intended to be called directly, could not be made private to class because
    * of automatic execution method.)
    */
    function OnCrash()
    {
        if (isset($this->RunningTask))
        {
            if (function_exists("error_get_last"))
            {
                $CrashInfo["LastError"] = error_get_last();
            }
            if (ob_get_length() !== FALSE)
            {
                $CrashInfo["OutputBuffer"] = ob_get_contents();
            }
            if (isset($CrashInfo))
            {
                $DB = new Database();
                $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
                        .addslashes(serialize($CrashInfo))
                        ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
            }
        }

        print("\n");
        return;

        if (ob_get_length() !== FALSE)
        {
            ?>
            <table width="100%" cellpadding="5" style="border: 2px solid #666666;  background: #FFCCCC;  font-family: Courier New, Courier, monospace;  margin-top: 10px;  font-weight: bold;"><tr><td>
            <div style="font-size: 200%;">CRASH OUTPUT</div><?PHP
            ob_end_flush();
            ?></td></tr></table><?PHP
        }
    }

    /**
    * Add additional directory(s) to be searched for files.  Specified
    * directory(s) will be searched, in order, before the default directories
    * or any other directories previously specified.  If a directory is already
    * present in the list, it will be moved to front to be searched first (or
    * to the end to be searched last, if SearchLast is set).  SearchLast only
    * affects whether added directories are searched before or after those
    * currently in the list;  when multiple directories are added, they are
    * always searched in the order they appear in the array.
    * @param DirList Current directory list.
    * @param Dir String with directory or array with directories to be searched.
    * @param SearchLast If TRUE, the directory(s) are searched after the entries
    *       current in the list, instead of before.
    * @param SkipSlashCheck If TRUE, check for trailing slash will be skipped.
    * @return Modified directory list.
    */
    private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
    {
        # convert incoming directory to array of directories (if needed)
        $Dirs = is_array($Dir) ? $Dir : array($Dir);

        # reverse array so directories are searched in specified order
        $Dirs = array_reverse($Dirs);

        # for each directory
        foreach ($Dirs as $Location)
        {
            # make sure directory includes trailing slash
            if (!$SkipSlashCheck)
            {
                $Location = $Location
                        .((substr($Location, -1) != "/") ? "/" : "");
            }

            # remove directory from list if already present
            if (in_array($Location, $DirList))
            {
                $DirList = array_diff(
                        $DirList, array($Location));
            }

            # add directory to list of directories
            if ($SearchLast)
            {
                array_push($DirList, $Location);
            }
            else
            {
                array_unshift($DirList, $Location);
            }
        }

        # return updated directory list to caller
        return $DirList;
    }

    /** Default list of directories to search for user interface (HTML/TPL) files. */
    private $InterfaceDirList = array(
            "local/interface/%ACTIVEUI%/",
            "interface/%ACTIVEUI%/",
            "local/interface/default/",
            "interface/default/",
            );
    /**
    * Default list of directories to search for UI include (CSS, JavaScript,
    * common HTML, common PHP, /etc) files.
    */
    private $IncludeDirList = array(
            "local/interface/%ACTIVEUI%/include/",
            "interface/%ACTIVEUI%/include/",
            "local/interface/default/include/",
            "interface/default/include/",
            );
    /** Default list of directories to search for image files. */
    private $ImageDirList = array(
            "local/interface/%ACTIVEUI%/images/",
            "interface/%ACTIVEUI%/images/",
            "local/interface/default/images/",
            "interface/default/images/",
            );
    /** Default list of directories to search for files containing PHP functions. */
    private $FunctionDirList = array(
            "local/interface/%ACTIVEUI%/include/",
            "interface/%ACTIVEUI%/include/",
            "local/interface/default/include/",
            "interface/default/include/",
            "local/include/",
            "include/",
            );

    const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
};


?>
