<?PHP
#
#   FILE:  PluginManager.php
#
#   Part of the ScoutLib application support library
#   Copyright 2009-2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu
#

/**
* Manager to load and invoke plugins.
*/
class PluginManager {

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

    /**
    * PluginManager class constructor.
    * @param ApplicationFramework $AppFramework ApplicationFramework within
    *       which plugins should run.
    * @param array $PluginDirectories Array of names of directories
    *       containing plugins, in the order they should be searched.
    */
    function __construct($AppFramework, $PluginDirectories)
    {
        # save framework and directory list for later use
        $this->AF = $AppFramework;
        $this->DirsToSearch = $PluginDirectories;

        # get our own database handle
        $this->DB = new Database();

        # hook into events to load plugin PHP and HTML files
        $this->AF->HookEvent("EVENT_PHP_FILE_LOAD", array($this, "FindPluginPhpFile"),
                ApplicationFramework::ORDER_LAST);
        $this->AF->HookEvent("EVENT_HTML_FILE_LOAD", array($this, "FindPluginHtmlFile"),
                ApplicationFramework::ORDER_LAST);

        # tell PluginCaller helper object how to get to us
        PluginCaller::$Manager = $this;
    }

    /**
    * Load and initialize plugins.  The $ForcePluginConfigOptLoad value only
    * applies to plugins that use Plugin::SetUpConfigOptions() to set up their
    * configuration options;  plugins that set up their config options via
    * Plugin::Register() will always have them set up.
    * @param bool $ForcePluginConfigOptLoad If TRUE, configuration options are
    *       loaded for all plugins, regardless of whether they are currently
    *       enabled.  (OPTIONAL, defaults to FALSE)
    * @return TRUE if load was successful (no problems encountered), otherwise FALSE.
    */
    function LoadPlugins($ForcePluginConfigOptLoad = FALSE)
    {
        # clear any existing errors/status
        $this->ErrMsgs = array();
        $this->StatusMsgs = array();

        # load list of all base plugin files
        $this->FindPlugins($this->DirsToSearch);

        # for each plugin found
        foreach ($this->PluginNames as $PluginName)
        {
            # bring in plugin class file
            include_once($this->PluginFiles[$PluginName]);

            # if plugin class was defined by file
            if (class_exists($PluginName))
            {
                # if plugin class is a valid descendant of base plugin class
                $Plugin = new $PluginName;
                if (is_subclass_of($Plugin, "Plugin"))
                {
                    # set hooks needed for plugin to access plugin manager services
                    $Plugin->SetCfgSaveCallback(array(__CLASS__, "CfgSaveCallback"));

                    # register the plugin
                    $this->Plugins[$PluginName] = $Plugin;
                    $this->PluginEnabled[$PluginName] = TRUE;
                    $this->Plugins[$PluginName]->Register();

                    # check required plugin attributes
                    $Attribs[$PluginName] = $this->Plugins[$PluginName]->GetAttributes();
                    if (!strlen($Attribs[$PluginName]["Name"]))
                    {
                        $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
                                ." could not be loaded because it"
                                ." did not have a <i>Name</i> attribute set.";
                        unset($this->PluginEnabled[$PluginName]);
                        unset($this->Plugins[$PluginName]);
                    }
                    if (!strlen($Attribs[$PluginName]["Version"]))
                    {
                        $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
                                ." could not be loaded because it"
                                ." did not have a <i>Version</i> attribute set.";
                        unset($this->PluginEnabled[$PluginName]);
                        unset($this->Plugins[$PluginName]);
                    }
                }
                else
                {
                    $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
                            ." could not be loaded because <i>".$PluginName."</i> was"
                            ." not a subclass of base <i>Plugin</i> class";
                }
            }
            else
            {
                $this->ErrMsgs[$PluginName][] = "Expected class <i>".$PluginName
                        ."</i> not found in plugin file <i>"
                        .$this->PluginFiles[$PluginName]."</i>";
            }
        }

        # check plugin dependencies
        $this->CheckDependencies();

        # load plugin configurations
        $this->DB->Query("SELECT BaseName,Cfg FROM PluginInfo");
        $Cfgs = $this->DB->FetchColumn("Cfg", "BaseName");

        # for each plugin
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            # if plugin is enabled
            if ($this->PluginEnabled[$PluginName])
            {
                # if plugin has its own subdirectory
                if ($this->PluginHasDir[$PluginName])
                {
                    # if plugin has its own object directory
                    $Dir = dirname($this->PluginFiles[$PluginName]);
                    if (is_dir($Dir."/objects"))
                    {
                        # add object directory to class autoloading list
                        ApplicationFramework::AddObjectDirectory($Dir."/objects");
                    }
                    else
                    {
                        # add plugin directory to class autoloading list
                        ApplicationFramework::AddObjectDirectory($Dir);
                    }
                }

                # set configuration values if available
                if (isset($Cfgs[$PluginName]))
                {
                    $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
                }

                # install or upgrade plugins if needed
                $this->InstallPlugin($Plugin);
            }
        }

        # check plugin dependencies again in case an install or upgrade failed
        $this->CheckDependencies();

        # set up plugin configuration options
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            if ($ForcePluginConfigOptLoad || $this->PluginEnabled[$PluginName])
            {
                $ErrMsg = $Plugin->SetUpConfigOptions();
                if ($ErrMsg !== NULL)
                {
                    $this->ErrMsgs[$PluginName][] = "Configuration option setup"
                            ." failed for plugin <b>".$PluginName."</b>: <i>"
                            .$ErrMsg."</i>";
                    $this->PluginEnabled[$PluginName] = FALSE;
                }
            }
        }

        # initialize enabled plugins
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            if ($this->PluginEnabled[$PluginName])
            {
                $ErrMsg = $Plugin->Initialize();
                if ($ErrMsg !== NULL)
                {
                    $this->ErrMsgs[$PluginName][] = "Initialization failed for"
                            ." plugin <b>".$PluginName."</b>: <i>".$ErrMsg."</i>";
                    $this->PluginEnabled[$PluginName] = FALSE;
                }
            }
        }

        # register any events declared by enabled plugins
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            if ($this->PluginEnabled[$PluginName])
            {
                $Events = $Plugin->DeclareEvents();
                if (count($Events)) {  $this->AF->RegisterEvent($Events);  }
            }
        }

        # hook enabled plugins to events
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            if ($this->PluginEnabled[$PluginName])
            {
                $EventsToHook = $Plugin->HookEvents();
                if (count($EventsToHook))
                {
                    foreach ($EventsToHook as $EventName => $PluginMethods)
                    {
                        if (!is_array($PluginMethods))
                                {  $PluginMethods = array($PluginMethods);  }

                        foreach ($PluginMethods as $PluginMethod)
                        {
                            if ($this->AF->IsStaticOnlyEvent($EventName))
                            {
                                $Caller = new PluginCaller($PluginName, $PluginMethod);
                                $Result = $this->AF->HookEvent(
                                        $EventName, array($Caller, "CallPluginMethod"));
                            }
                            else
                            {
                                $Result = $this->AF->HookEvent(
                                        $EventName, array($Plugin, $PluginMethod));
                            }
                            if ($Result === FALSE)
                            {
                                $this->ErrMsgs[$PluginName][] =
                                        "Unable to hook requested event <i>"
                                        .$EventName."</i> for plugin <b>"
                                        .$PluginName."</b>";
                            }
                        }
                    }
                }
            }
        }

        # report to caller whether any problems were encountered
        return count($this->ErrMsgs) ? FALSE : TRUE;
    }

    /**
    * Retrieve any error messages generated during plugin loading.
    * @return Array of arrays of error messages, indexed by plugin base
    *       (class) name.
    */
    function GetErrorMessages()
    {
        return $this->ErrMsgs;
    }

    /**
    * Get/set any status messages generated during plugin loading or
    * via plugin method calls.
    * @return Array of arrays of status messages, indexed by plugin base
    *       (class) name.
    */
    function GetStatusMessages()
    {
        return $this->StatusMsgs;
    }

    /**
    * Retrieve specified plugin.
    * @param string $PluginName Base name of plugin.
    * @return Plugin object or NULL if no plugin found with specified name.
    */
    function GetPlugin($PluginName)
    {
        return isset($this->Plugins[$PluginName])
                ? $this->Plugins[$PluginName] : NULL;
    }

    /**
    * Retrieve plugin for current page (if any).  This method relies on the
    * current page having been found within the plugin directory (usually via a
    * "P_" prefix on the page name) via a call to the hooked FindPluginPhpFile()
    * or FindPluginHtmlFile() methods..
    * @return Plugin object or NULL if no plugin associated with current page.
    */
    function GetPluginForCurrentPage()
    {
        return $this->GetPlugin($this->PageFilePlugin);
    }

    /**
    * Retrieve info about currently loaded plugins.
    * @return Array of arrays of plugin info, indexed by plugin base (class) name.
    */
    function GetPluginAttributes()
    {
        $Info = array();
        foreach ($this->Plugins as $PluginName => $Plugin)
        {
            $Info[$PluginName] = $Plugin->GetAttributes();
            $Info[$PluginName]["Enabled"] =
                    isset($this->PluginInfo[$PluginName]["Enabled"])
                    ? $this->PluginInfo[$PluginName]["Enabled"] : FALSE;
            $Info[$PluginName]["Installed"] =
                    isset($this->PluginInfo[$PluginName]["Installed"])
                    ? $this->PluginInfo[$PluginName]["Installed"] : FALSE;
            $Info[$PluginName]["ClassFile"] = $this->PluginFiles[$PluginName];
        }
        return $Info;
    }

    /**
    * Returns a list of plugins dependent on the specified plugin.
    * @param string $PluginName Base name of plugin.
    * @return Array of base names of dependent plugins.
    */
    function GetDependents($PluginName)
    {
        $Dependents = array();
        $AllAttribs = $this->GetPluginAttributes();
        foreach ($AllAttribs as $Name => $Attribs)
        {
            if (array_key_exists($PluginName, $Attribs["Requires"]))
            {
                $Dependents[] = $Name;
                $SubDependents = $this->GetDependents($Name);
                $Dependents = array_merge($Dependents, $SubDependents);
            }
        }
        return $Dependents;
    }

    /**
    * Get list of active (i.e. enabled) plugins.
    * @return Array of base names of active plugins.
    */
    function GetActivePluginList()
    {
        return array_keys($this->PluginEnabled, 1);
    }

    /**
    * Get/set whether specified plugin is enabled.
    * @param string $PluginName Base name of plugin.
    * @param bool $NewValue TRUE to enable, FALSE to disable.  (OPTIONAL)
    * @return TRUE if plugin is enabled, otherwise FALSE.
    */
    function PluginEnabled($PluginName, $NewValue = NULL)
    {
        # if an enabled/disabled value was supplied
        if ($NewValue !== NULL)
        {
            # if enabled/disabled status has changed for plugin
            if (($NewValue && !$this->PluginEnabled[$PluginName]["Enabled"])
                    || (!$NewValue && $this->PluginEnabled[$PluginName]["Enabled"]))
            {
                $Info = $this->Plugins[$PluginName]->GetAttributes();
                $this->DB->Query("UPDATE PluginInfo"
                        ." SET Enabled = ".($NewValue ? "1" : "0")
                        ." WHERE BaseName = '".addslashes($PluginName)."'");
                $this->PluginEnabled[$PluginName] = $NewValue;
                $this->PluginInfo[$PluginName]["Enabled"] = $NewValue;
                $this->StatusMsgs[$PluginName][] = "Plugin <i>"
                        .$Info["Name"]."</i> "
                        .($NewValue ? "enabled" : "disabled").".";
            }
        }
        return array_key_exists($PluginName, $this->PluginEnabled)
                && $this->PluginEnabled[$PluginName];
    }

    /**
    * Uninstall plugin and (optionally) delete any associated data.
    * @param string $PluginName Base name of plugin.
    * @return Error message or NULL if uninstall succeeded.
    */
    function UninstallPlugin($PluginName)
    {
        # assume success
        $Result = NULL;

        # if plugin is installed
        if ($this->PluginInfo[$PluginName]["Installed"])
        {
            # call uninstall method for plugin
            $Result = $this->Plugins[$PluginName]->Uninstall();

            # if plugin uninstall method succeeded
            if ($Result === NULL)
            {
                # remove plugin info from database
                $this->DB->Query("DELETE FROM PluginInfo"
                        ." WHERE BaseName = '".addslashes($PluginName)."'");

                # drop our data for the plugin
                unset($this->Plugins[$PluginName]);
                unset($this->PluginInfo[$PluginName]);
                unset($this->PluginEnabled[$PluginName]);
                unset($this->PluginNames[$PluginName]);
                unset($this->PluginFiles[$PluginName]);
            }
        }

        # report results (if any) to caller
        return $Result;
    }


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

    private $Plugins = array();
    private $PluginFiles = array();
    private $PluginNames = array();
    private $PluginHasDir = array();
    private $PluginInfo = array();
    private $PluginEnabled = array();
    private $PageFilePlugin = NULL;
    private $AF;
    private $DirsToSearch;
    private $ErrMsgs = array();
    private $StatusMsgs = array();
    private $DB;

    /**
    * Load list of plugins.
    * @param array $DirsToSearch Array of strings containing names of
    *       directories in which to look for plugin files.
    */
    private function FindPlugins($DirsToSearch)
    {
        # for each directory
        foreach ($DirsToSearch as $Dir)
        {
            # if directory exists
            if (is_dir($Dir))
            {
                # for each file in directory
                $FileNames = scandir($Dir);
                foreach ($FileNames as $FileName)
                {
                    # if file looks like base plugin file
                    if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
                    {
                        # add file to list
                        $PluginName = preg_replace("/\.php$/", "", $FileName);
                        $this->PluginNames[$PluginName] = $PluginName;
                        $this->PluginFiles[$PluginName] = $Dir."/".$FileName;
                        $this->PluginHasDir[$PluginName] = FALSE;
                    }
                    # else if file looks like plugin directory
                    elseif (is_dir($Dir."/".$FileName)
                            && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
                    {
                        # if there is a base plugin file in the directory
                        $PluginName = $FileName;
                        $PluginFile = $Dir."/".$PluginName."/".$PluginName.".php";
                        if (file_exists($PluginFile))
                        {
                            # add plugin and directory to lists
                            $this->PluginNames[$PluginName] = $PluginName;
                            $this->PluginFiles[$PluginName] = $PluginFile;
                            $this->PluginHasDir[$PluginName] = TRUE;
                        }
                        # else if we don't already have a base plugin file
                        elseif (!array_key_exists($PluginName, $this->PluginFiles))
                        {
                            $this->ErrMsgs[$PluginName][] =
                                    "Expected plugin file <i>".$PluginName.".php</i> not"
                                    ." found in plugin subdirectory <i>"
                                    .$Dir."/".$PluginName."</i>";
                        }
                    }
                }
            }
        }
    }

    /**
    * Install or upgrade specified plugin if needed.  Any errors encountered
    * cause entries to be added to the $this->ErrMsgs array.
    * @param object $Plugin Plugin to install.
    */
    private function InstallPlugin($Plugin)
    {
        # cache all plugin info from database
        if (count($this->PluginInfo) == 0)
        {
            $this->DB->Query("SELECT * FROM PluginInfo");
            while ($Row = $this->DB->FetchRow())
            {
                $this->PluginInfo[$Row["BaseName"]] = $Row;
            }
        }

        # if plugin was not found in database
        $PluginName = get_class($Plugin);
        $Attribs = $Plugin->GetAttributes();
        if (!isset($this->PluginInfo[$PluginName]))
        {
            # add plugin to database
            $this->DB->Query("INSERT INTO PluginInfo"
                    ." (BaseName, Version, Installed, Enabled)"
                    ." VALUES ('".addslashes($PluginName)."', "
                    ." '".addslashes($Attribs["Version"])."', "
                    ."0, "
                    ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")");

            # read plugin settings back in
            $this->DB->Query("SELECT * FROM PluginInfo"
                 ." WHERE BaseName = '".addslashes($PluginName)."'");
            $this->PluginInfo[$PluginName] = $this->DB->FetchRow();
        }

        # store plugin settings for later use
        $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"];

        # if plugin is enabled
        if ($this->PluginEnabled[$PluginName])
        {
            # if plugin has not been installed
            if (!$this->PluginInfo[$PluginName]["Installed"])
            {
                # set default values if present
                $Attribs = $Plugin->GetAttributes();
                if (isset($Attribs["CfgSetup"]))
                {
                    foreach ($Attribs["CfgSetup"] as $CfgValName => $CfgSetup)
                    {
                        if (isset($CfgSetup["Default"]))
                        {
                            $Plugin->ConfigSetting($CfgValName,
                                    $CfgSetup["Default"]);
                        }
                    }
                }

                # install plugin
                $ErrMsg = $Plugin->Install();

                # if install succeeded
                if ($ErrMsg == NULL)
                {
                    # mark plugin as installed
                    $this->DB->Query("UPDATE PluginInfo SET Installed = 1"
                            ." WHERE BaseName = '".addslashes($PluginName)."'");
                    $this->PluginInfo[$PluginName]["Installed"] = 1;
                }
                else
                {
                    # disable plugin
                    $this->PluginEnabled[$PluginName] = FALSE;

                    # record error message about installation failure
                    $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>"
                            .$PluginName."</b> failed: <i>".$ErrMsg."</i>";;
                }
            }
            else
            {
                # if plugin version is newer than version in database
                if (version_compare($Attribs["Version"],
                        $this->PluginInfo[$PluginName]["Version"]) == 1)
                {
                    # set default values for any new configuration settings
                    $Attribs = $Plugin->GetAttributes();
                    if (isset($Attribs["CfgSetup"]))
                    {
                        foreach ($Attribs["CfgSetup"] as $CfgValName => $CfgSetup)
                        {
                            if (isset($CfgSetup["Default"])
                                    && ($Plugin->ConfigSetting(
                                            $CfgValName) === NULL))
                            {
                                $Plugin->ConfigSetting($CfgValName,
                                        $CfgSetup["Default"]);
                            }
                        }
                    }

                    # upgrade plugin
                    $ErrMsg = $Plugin->Upgrade(
                            $this->PluginInfo[$PluginName]["Version"]);

                    # if upgrade succeeded
                    if ($ErrMsg == NULL)
                    {
                        # update plugin version in database
                        $this->DB->Query("UPDATE PluginInfo"
                                ." SET Version = '".addslashes($Attribs["Version"])."'"
                                ." WHERE BaseName = '".addslashes($PluginName)."'");
                        $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"];
                    }
                    else
                    {
                        # disable plugin
                        $this->PluginEnabled[$PluginName] = FALSE;

                        # record error message about upgrade failure
                        $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>"
                                .$PluginName."</b> from version <i>"
                                .addslashes($this->PluginInfo[$PluginName]["Version"])
                                ."</i> to version <i>"
                                .addslashes($Attribs["Version"])."</i> failed: <i>"
                                .$ErrMsg."</i>";
                    }
                }
                # else if plugin version is older than version in database
                elseif (version_compare($Attribs["Version"],
                        $this->PluginInfo[$PluginName]["Version"]) == -1)
                {
                    # disable plugin
                    $this->PluginEnabled[$PluginName] = FALSE;

                    # record error message about version conflict
                    $this->ErrMsgs[$PluginName][] = "Plugin <b>"
                            .$PluginName."</b> is older (<i>"
                            .addslashes($Attribs["Version"])
                            ."</i>) than previously-installed version (<i>"
                            .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>).";
                }
            }
        }
    }

    /**
    * Check plugin dependencies, disabling plugins whose dependencies
    * are not met, and add messages to $this->ErrMsgs as appropriate.
    */
    private function CheckDependencies()
    {
        # look until all enabled plugins check out okay
        do
        {
            # start out assuming all plugins are okay
            $AllOkay = TRUE;

            # for each plugin
            foreach ($this->Plugins as $PluginName => $Plugin)
            {
                # if plugin is currently enabled
                if ($this->PluginEnabled[$PluginName])
                {
                    # load plugin attributes
                    if (!isset($Attribs[$PluginName]))
                    {
                        $Attribs[$PluginName] =
                                $this->Plugins[$PluginName]->GetAttributes();
                    }

                    # for each dependency for this plugin
                    foreach ($Attribs[$PluginName]["Requires"]
                            as $ReqName => $ReqVersion)
                    {
                        # handle PHP version requirements
                        if ($ReqName ==  "PHP")
                        {
                            if (version_compare($ReqVersion, phpversion(), ">"))
                            {
                                $this->ErrMsgs[$PluginName][] = "PHP version "
                                    ."<i>".$ReqVersion."</i>"
                                    ." required by <b>".$PluginName."</b>"
                                    ." was not available.  (Current PHP version"
                                    ." is <i>".phpversion()."</i>.)";
                            }
                        }
                        # handle PHP extension requirements
                        elseif (preg_match("/^PHPX_/", $ReqName))
                        {
                            list($Dummy, $ExtensionName) = explode("_", $ReqName, 2);
                            if (!extension_loaded($ExtensionName))
                            {
                                $this->ErrMsgs[$PluginName][] = "PHP extension "
                                    ."<i>".$ExtensionName."</i>"
                                    ." required by <b>".$PluginName."</b>"
                                    ." was not available.";
                            }
                        }
                        # handle dependencies on other plugins
                        else
                        {
                            # load plugin attributes if not already loaded
                            if (isset($this->Plugins[$ReqName])
                                    && !isset($Attribs[$ReqName]))
                            {
                                $Attribs[$ReqName] =
                                        $this->Plugins[$ReqName]->GetAttributes();
                            }

                            # if target plugin is not present or is disabled or is too old
                            if (!isset($this->PluginEnabled[$ReqName])
                                    || !$this->PluginEnabled[$ReqName]
                                    || (version_compare($ReqVersion,
                                            $Attribs[$ReqName]["Version"], ">")))
                            {
                                # add error message and disable plugin
                                $this->ErrMsgs[$PluginName][] = "Plugin <i>"
                                        .$ReqName." ".$ReqVersion."</i>"
                                        ." required by <b>".$PluginName."</b>"
                                        ." was not available.";
                                $this->PluginEnabled[$PluginName] = FALSE;

                                # set flag indicating plugin did not check out
                                $AllOkay = FALSE;
                            }
                        }
                    }
                }
            }
        } while ($AllOkay == FALSE);
    }

    /** @cond */
    /**
    * Method hooked to EVENT_PHP_FILE_LOAD to find the appropriate PHP file
    * when a plugin page is to be loaded.  (This method is not meant to be
    * called directly.)
    * @param string $PageName Current page name.
    * @return string Parameter array with updated page name (if appropriate).
    */
    function FindPluginPhpFile($PageName)
    {
        # build list of possible locations for file
        $Locations = array(
                "local/plugins/%PLUGIN%/pages/%PAGE%.php",
                "plugins/%PLUGIN%/pages/%PAGE%.php",
                "local/plugins/%PLUGIN%/%PAGE%.php",
                "plugins/%PLUGIN%/%PAGE%.php",
                );

        # look for file and return (possibly) updated page to caller
        return $this->FindPluginPageFile($PageName, $Locations);
    }
    /** @endcond */

    /** @cond */
    /**
    * Method hooked to EVENT_HTML_FILE_LOAD to find the appropriate HTML file
    * when a plugin page is to be loaded.  (This method is not meant to be
    * called directly.)
    * @param string $PageName Current page name.
    * @return string Parameter array with updated page name (if appropriate).
    */
    function FindPluginHtmlFile($PageName)
    {
        # build list of possible locations for file
        $Locations = array(
                "local/plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
                "plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
                "local/plugins/%PLUGIN%/interface/default/%PAGE%.html",
                "plugins/%PLUGIN%/interface/default/%PAGE%.html",
                "local/plugins/%PLUGIN%/%PAGE%.html",
                "plugins/%PLUGIN%/%PAGE%.html",
                );

        # find HTML file
        $Params = $this->FindPluginPageFile($PageName, $Locations);

        # if plugin HTML file was found
        if ($Params["PageName"] != $PageName)
        {
            # add subdirectories for plugin to search paths
            $Dir = preg_replace("%^local/%", "", dirname($Params["PageName"]));
            $GLOBALS["AF"]->AddImageDirectories(array(
                    "local/".$Dir."/images",
                    $Dir."/images",
                    ));
            $GLOBALS["AF"]->AddIncludeDirectories(array(
                    "local/".$Dir."/include",
                    $Dir."/include",
                    ));
            $GLOBALS["AF"]->AddFunctionDirectories(array(
                    "local/".$Dir."/include",
                    $Dir."/include",
                    ));
        }

        # return possibly revised HTML file name to caller
        return $Params;
    }
    /** @endcond */

    /**
    * Find the plugin page file with the specified suffix, based on the
    * "P_PluginName_" convention for indicating plugin pages.
    * @param string $PageName Current page name.
    * @param array $Locations Array of strings giving possible locations for
    *       file, with %ACTIVEUI%, %PLUGIN%, and %PAGE% used as appropriate.
    * @return string Parameter array with page name (updated if appropriate).
    */
    private function FindPluginPageFile($PageName, $Locations)
    {
        # set up return value assuming we will not find plugin page file
        $ReturnValue["PageName"] = $PageName;

        # look for plugin name and plugin page name in base page name
        preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);

        # if base page name contained name of enabled plugin with its own subdirectory
        if ((count($Matches) == 3)
                && $this->PluginEnabled[$Matches[1]]
                && $this->PluginHasDir[$Matches[1]])
        {
            # for each possible location
            $ActiveUI = $GLOBALS["AF"]->ActiveUserInterface();
            $PluginName = $Matches[1];
            $PageName = $Matches[2];
            foreach ($Locations as $Loc)
            {
                # make any needed substitutions into path
                $FileName = str_replace(array("%ACTIVEUI%", "%PLUGIN%", "%PAGE%"),
                        array($ActiveUI, $PluginName, $PageName), $Loc);

                # if file exists in this location
                if (file_exists($FileName))
                {
                    # set return value to contain full plugin page file name
                    $ReturnValue["PageName"] = $FileName;

                    # save plugin name as home of current page
                    $this->PageFilePlugin = $PluginName;

                    # set G_Plugin to plugin associated with current page
                    $GLOBALS["G_Plugin"] = $this->GetPluginForCurrentPage();

                    # stop looking
                    break;
                }
            }
        }

        # return array containing page name or page file name to caller
        return $ReturnValue;
    }

    /** @cond */
    /**
    * Method installed in plugins to allow them to save their configuration
    * information.
    * @param string $BaseName Plugin base name.
    * @param array $Cfg Array of configuration values.
    */
    static function CfgSaveCallback($BaseName, $Cfg)
    {
        $DB = new Database();
        $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
                ."' WHERE BaseName = '".addslashes($BaseName)."'");
    }
    /** @endcond */
}

/** @cond */
/**
* Helper class for internal use by PluginManager.  This class is used to
* allow plugin methods to be triggered by events that only allow serialized
* callbacks (e.g. periodic events).
* The plugin name and the method to be called are set and then the
* PluginCaller object is serialized out.  When the PluginCaller object is
* unserialized, it retrieves the appropriate plugin object from the
* PluginManager (pointer to PluginManager is set in PluginManager
* constructor) and calls the specified method.
*/
class PluginCaller {

    /**
    * Class constructor, which stores the plugin name and the name of the
    * method to be called.
    * @param string $PluginName Name of plugin.
    * @param string $MethodName Name of method to be called.
    */
    function __construct($PluginName, $MethodName)
    {
        $this->PluginName = $PluginName;
        $this->MethodName = $MethodName;
    }

    /**
    * Call the method that was specified in our constructor.  This method
    * accept whatever arguments are appropriate for the specified method
    * and returns values as appropriate for the specified method.
    */
    function CallPluginMethod()
    {
        $Args = func_get_args();
        $Plugin = self::$Manager->GetPlugin($this->PluginName);
        return call_user_func_array(array($Plugin, $this->MethodName), $Args);
    }

    /**
    * Get full method name as a text string.
    * @return string Method name, including plugin class name.
    */
    function GetCallbackAsText()
    {
        return $this->PluginName."::".$this->MethodName;
    }

    /**
    * Sleep method, specifying which values are to be saved when we are
    * serialized.
    * @return array Array of names of variables to be saved.
    */
    function __sleep()
    {
        return array("PluginName", "MethodName");
    }

    /** PluginManager to use to retrieve appropriate plugins. */
    static public $Manager;

    private $PluginName;
    private $MethodName;
}
/** @endcond */

?>
