<?PHP

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

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

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

    /** @cond */
    /**
    * Object constructor.
    * @param ObjectDirectories Array of directories to search for object (class)
    *       source files, with directory as indexes and any prefixes to strip (e.g.
    *       "Axis--") as values.
    **/
    function __construct($ObjectDirectories = NULL)
    {
        # save execution start time
        $this->ExecutionStartTime = microtime(TRUE);

        # save object directory search list
        if ($ObjectDirectories) {  $this->AddObjectDirectories($ObjectDirectories);  }

        # 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();

        # load our settings from database
        $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
        $this->Settings = $this->DB->FetchRow();
        if (!$this->Settings)
        {
            $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
                    ." (LastTaskRunAt) VALUES (NOW())");
            $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
            $this->Settings = $this->DB->FetchRow();
        }

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

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

    /**
    * Add additional directories to be searched for object files when autoloading.
    * @param Dirs Array with directories to be searched, with directory paths as
    *       indexes and any prefixes to strip (e.g. "Axis--") as values..
    */
    function AddObjectDirectories($Dirs)
    {
        foreach ($Dirs as $Location => $Prefix)
        {
            $Location = $Location
                    .((substr($Location, -1) != "/") ? "/" : "");
            self::$ObjectDirectories = array_merge(
                    array($Location => $Prefix),
                    self::$ObjectDirectories);
        }
    }

    /**
    * Load page PHP and HTML/TPL files.
    * @param PageName Name of page to be loaded (e.g. "BrowseResources").
    */
    function LoadPage($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();

        # 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 if available
            $HtmlFile = $this->FindCommonTemplate("Common");
            if ($HtmlFile) {  include($HtmlFile);  }

            # 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;
            $HtmlFile = 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
                    $HtmlFile = $SignalResult["PageName"];
                }
                else
                {
                    # use new value for page name
                    $NewPageName = $SignalResult["PageName"];
                }
            }

            # load page content HTML file if available
            if ($HtmlFile === NULL)
            {
                $HtmlFile = $this->FindTemplate($this->ContentTemplateList, $NewPageName);
            }
            if ($HtmlFile)
            {
                include($HtmlFile);
            }
            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 content
            $BufferedContent = ob_get_contents();
            ob_end_clean();

            # load page start HTML file if available
            $HtmlFile = $this->FindCommonTemplate("Start");
            if ($HtmlFile) {  include($HtmlFile);  }

            # write out page content
            print($BufferedContent);

            # load page end HTML file if available
            $HtmlFile = $this->FindCommonTemplate("End");
            if ($HtmlFile) {  include($HtmlFile);  }
        }

        # 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
            }
        }

        # 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();  }
    }

    /**
    * 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 ((strpos($Page, "?") === FALSE)
                && ((strpos($Page, "=") !== FALSE)
                    || ((strpos($Page, ".php") === FALSE)
                        && (strpos($Page, ".htm") === FALSE)
                        && (strpos($Page, "/") === FALSE))))
        {
            $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 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 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;
    }

    /**
    * 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 FunctionName Name of function to be called.
    * @param Arg1 First argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg2 Second argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg3 Third argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg4 Fourth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg5 FifthFirst argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg6 Sixth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg7 Seventh argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param Arg8 Eighth argument to be passed to function.  (OPTIONAL, REFERENCE)
    * @param 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 FileName Name of file to be included.
    */
    function AddEnvInclude($FileName)
    {
        $this->EnvIncludes[] = $FileName;
    }

    /**
    * Return path to specified image or CSS file.
    * @param FileName Base file name.
    * @return Full relative path name of file or NULL if file not found.
    */
    function GUIFile($FileName)
    {
        # pull off file name suffix
        $NamePieces = explode(".", $FileName);
        $Suffix = strtolower(array_pop($NamePieces));

        # determine which location to search based on file suffix
        $ImageSuffixes = array("gif", "jpg", "png");
        $FileList = in_array($Suffix, $ImageSuffixes)
                ? $this->ImageFileList : $this->CommonTemplateList;

        # search for file and return result to caller
        return $this->FindTemplate($FileList, $FileName);
    }

    /**
    * Print path to specified image or CSS 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 FileName Base file name.
    */
    function PUIFile($FileName)
    {
        $FullFileName = $this->GUIFile($FileName);
        if ($FullFileName) {  print($FullFileName);  }
    }

    /**
    * Get full path to HTML or CSS file.
    * @param PageName File or short page name.
    */
    function FindCommonTemplate($PageName)
    {
        return $this->FindTemplate(
                array_merge($this->CommonTemplateList, $this->ContentTemplateList),
                $PageName);;
    }

    /**
    * 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 Function or method info.
    * @return TRUE if function/method is now available, else FALSE.
    */
    function LoadFunction($Callback)
    {
        if (!is_callable($Callback) && is_string($Callback))
        {
            $Locations = $this->FunctionFileList;
            foreach (self::$ObjectDirectories as $Location => $Prefix)
            {
                $Locations[] = $Location."%PAGENAME%.php";
                $Locations[] = $Location."%PAGENAME%.html";
            }
            $FunctionFileName = $this->FindTemplate($Locations, "F-".$Callback);
            if ($FunctionFileName)
            {
                include_once($FunctionFileName);
            }
        }
        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();
    }

    /*@)*/ /* 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 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 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();
        }
    }

    /**
    * 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 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 Function to be called when event is signaled.  (OPTIONAL
    *       if EventsOrEventName is an array of events)
    * @param 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 EventName Name of event being signaled.
    * @param 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:
                        $ReturnValue = $Result;
                        $Parameters = $Result;
                        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;
                }
            }
        }

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

    /**
    * Report whether specified event only allows static callbacks.
    * @param 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 Function or method to call to perform task.
    * @param Parameters Array containing parameters to pass to function or
    *       method.  (OPTIONAL, pass NULL for no parameters)
    * @param Priority Priority to assign to task.  (OPTIONAL, defaults
    *       to PRIORITY_MEDIUM)
    * @param 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 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 Function or method to call to perform task.
    * @param Parameters Array containing parameters to pass to function or
    *       method.  (OPTIONAL, pass NULL for no parameters)
    * @param Priority Priority to assign to task.  (OPTIONAL, defaults
    *       to PRIORITY_MEDIUM)
    * @param Description Text description of task.  (OPTIONAL)
    * @return TRUE if task was added, otherwise FALSE.
    * @see 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 Function or method to call to perform task.
    * @param 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 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 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.
    */
    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 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.
    */
    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 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.
    */
    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);
    }

    /**
    * Move orphaned task back into queue.
    * @param Task ID.
    */
    function ReQueueOrphanedTask($TaskId)
    {
        $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));
        $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
        $this->DB->Query("UNLOCK TABLES");
    }

    /**
    * Delete orphaned task back from running task queue.
    * @param Task ID.
    */
    function DeleteOrphanedTask($TaskId)
    {
        $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
    }

    /**
    * Get/set maximum number of tasks to have running simultaneously.
    * @param 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 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())
        {
            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 */

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

    private $JumpToPage = NULL;
    private $SuppressHTML = FALSE;
    private $DefaultPage = "Home";
    private $ActiveUI = "default";
    private $HtmlCharset = "UTF-8";
    private $PostProcessingFuncs = array();
    private $EnvIncludes = array();
    private $DB;
    private $Settings;
    private $ExecutionStartTime;
    private static $ObjectDirectories = array();
    private $MaxRunningTasksToTrack = 250;
    private $RunningTask;

    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_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
                "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
                );

    private function FindTemplate($FileList, $PageName)
    {
        $FileNameFound = NULL;
        foreach ($FileList as $FileName)
        {
            $FileName = str_replace("%ACTIVEUI%", $this->ActiveUI, $FileName);
            $FileName = str_replace("%PAGENAME%", $PageName, $FileName);
            if (file_exists($FileName))
            {
                $FileNameFound = $FileName;
                break;
            }
        }
        return $FileNameFound;
    }

    private function SetUpObjectAutoloading()
    {
        function __autoload($ClassName)
        {
            ApplicationFramework::AutoloadObjects($ClassName);
        }
    }

    /** @cond */
    static function AutoloadObjects($ClassName)
    {
        foreach (self::$ObjectDirectories as $Location => $Prefix)
        {
            $FileName = $Location.$Prefix.$ClassName.".php";
            if (file_exists($FileName))
            {
                require_once($FileName);
                break;
            }
        }
    }
    /** @endcond */

    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);
        }
    }

    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()").")");
        }
    }

    private static function GetCallbackSignature($Callback)
    {
        return !is_array($Callback) ? $Callback
                : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
                        ."::".$Callback[1];
    }

    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())
        {
            # 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;
        }
    }

    private function LaunchTSR()
    {
        # set needed headers and
        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();

        # 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->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
            $this->Settings = $this->DB->FetchRow();

            # 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;
            call_user_func_array($Callback, $Parameters);
            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
            $RunningTaskCount = $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));
            }
        }
    }

    /** @cond */
    /**
    * 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
        }
    }
    /** @endcond */

    private $CommonTemplateList = array(
            "local/interface/%ACTIVEUI%/include/StdPage%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/include/StdPage%PAGENAME%.html",
            "local/interface/%ACTIVEUI%/include/%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/include/%PAGENAME%.html",
            "local/interface/%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.html",
            "local/interface/%ACTIVEUI%/include/SPT--%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/include/SPT--%PAGENAME%.html",
            "local/interface/%ACTIVEUI%/include/%PAGENAME%",
            "interface/%ACTIVEUI%/include/StdPage%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/include/StdPage%PAGENAME%.html",
            "interface/%ACTIVEUI%/include/%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/include/%PAGENAME%.html",
            "interface/%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.html",
            "interface/%ACTIVEUI%/include/SPT--%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/include/SPT--%PAGENAME%.html",
            "interface/%ACTIVEUI%/include/%PAGENAME%",
            "SPTUI--%ACTIVEUI%/include/StdPage%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/include/StdPage%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/include/%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/include/%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/include/SPT--%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/include/SPT--%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/include/%PAGENAME%",
            "%ACTIVEUI%/include/StdPage%PAGENAME%.tpl",
            "%ACTIVEUI%/include/StdPage%PAGENAME%.html",
            "%ACTIVEUI%/include/%PAGENAME%.tpl",
            "%ACTIVEUI%/include/%PAGENAME%.html",
            "%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.tpl",
            "%ACTIVEUI%/include/SPT--StandardPage%PAGENAME%.html",
            "%ACTIVEUI%/include/SPT--%PAGENAME%.tpl",
            "%ACTIVEUI%/include/SPT--%PAGENAME%.html",
            "%ACTIVEUI%/include/%PAGENAME%",
            "local/interface/default/include/StdPage%PAGENAME%.tpl",
            "local/interface/default/include/StdPage%PAGENAME%.html",
            "local/interface/default/include/%PAGENAME%.tpl",
            "local/interface/default/include/%PAGENAME%.html",
            "local/interface/default/include/SPT--StandardPage%PAGENAME%.tpl",
            "local/interface/default/include/SPT--StandardPage%PAGENAME%.html",
            "local/interface/default/include/SPT--%PAGENAME%.tpl",
            "local/interface/default/include/SPT--%PAGENAME%.html",
            "local/interface/default/include/%PAGENAME%",
            "interface/default/include/StdPage%PAGENAME%.tpl",
            "interface/default/include/StdPage%PAGENAME%.html",
            "interface/default/include/%PAGENAME%.tpl",
            "interface/default/include/%PAGENAME%.html",
            "interface/default/include/SPT--StandardPage%PAGENAME%.tpl",
            "interface/default/include/SPT--StandardPage%PAGENAME%.html",
            "interface/default/include/SPT--%PAGENAME%.tpl",
            "interface/default/include/SPT--%PAGENAME%.html",
            "interface/default/include/%PAGENAME%",
            );
    private $ContentTemplateList = array(
            "local/interface/%ACTIVEUI%/%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/%PAGENAME%.html",
            "local/interface/%ACTIVEUI%/SPT--%PAGENAME%.tpl",
            "local/interface/%ACTIVEUI%/SPT--%PAGENAME%.html",
            "interface/%ACTIVEUI%/%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/%PAGENAME%.html",
            "interface/%ACTIVEUI%/SPT--%PAGENAME%.tpl",
            "interface/%ACTIVEUI%/SPT--%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/SPT--%PAGENAME%.tpl",
            "SPTUI--%ACTIVEUI%/SPT--%PAGENAME%.html",
            "%ACTIVEUI%/%PAGENAME%.tpl",
            "%ACTIVEUI%/%PAGENAME%.html",
            "%ACTIVEUI%/SPT--%PAGENAME%.tpl",
            "%ACTIVEUI%/SPT--%PAGENAME%.html",
            "local/interface/default/%PAGENAME%.tpl",
            "local/interface/default/%PAGENAME%.html",
            "local/interface/default/SPT--%PAGENAME%.tpl",
            "local/interface/default/SPT--%PAGENAME%.html",
            "interface/default/%PAGENAME%.tpl",
            "interface/default/%PAGENAME%.html",
            "interface/default/SPT--%PAGENAME%.tpl",
            "interface/default/SPT--%PAGENAME%.html",
            );
    private $ImageFileList = array(
            "local/interface/%ACTIVEUI%/images/%PAGENAME%",
            "interface/%ACTIVEUI%/images/%PAGENAME%",
            "SPTUI--%ACTIVEUI%/images/%PAGENAME%",
            "%ACTIVEUI%/images/%PAGENAME%",
            "local/interface/default/images/%PAGENAME%",
            "interface/default/images/%PAGENAME%",
            );
    private $FunctionFileList = array(
            "local/interface/%ACTIVEUI%/include/%PAGENAME%.php",
            "local/interface/%ACTIVEUI%/include/%PAGENAME%.html",
            "interface/%ACTIVEUI%/include/%PAGENAME%.php",
            "interface/%ACTIVEUI%/include/%PAGENAME%.html",
            "SPTUI--%ACTIVEUI%/include/%PAGENAME%.php",
            "SPTUI--%ACTIVEUI%/include/%PAGENAME%.html",
            "%ACTIVEUI%/include/%PAGENAME%.php",
            "%ACTIVEUI%/include/%PAGENAME%.html",
            "local/interface/default/include/%PAGENAME%.php",
            "local/interface/default/include/%PAGENAME%.html",
            "local/include/%PAGENAME%.php",
            "local/include/%PAGENAME%.html",
            "interface/default/include/%PAGENAME%.php",
            "interface/default/include/%PAGENAME%.html",
            "include/%PAGENAME%.php",
            "include/%PAGENAME%.html",
            );

    /** @cond */
    const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
    /** @endcond */
};


?>
