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

/**
* Base class for all plugins.
*/
abstract class Plugin
{

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

    /**
    * Set the plugin attributes.  At minimum this method MUST set $this->Name
    * and $this->Version.  This is called when the plugin is loaded, and is
    * normally the only method called for disabled plugins (except for
    * SetUpConfigOptions(), which is called for pages within the plugin
    * configuration interface).
    */
    public abstract function Register();

    /**
    * Set up plugin configuration options.  This is called if the plugin is
    * enabled and/or when loading the plugin configuration interface.  Config
    * options must be set up using this method (rather than going into
    * Register()) whenever their setup references data from outside of the
    * plugin in any fashion.  NOTE:  This method is called after the Install()
    * or Upgrade() methods are called.
    * @return NULL if configuration setup succeeded, otherwise a string or
    *       array of strings containing error message(s) indicating why
    *       config setup failed.
    */
    public function SetUpConfigOptions()
    {
        return NULL;
    }

    /**
    * Initialize the plugin.  This is called (if the plugin is enabled) after
    * all plugins have been loaded but before any methods for this plugin
    * (other than Register()) have been called.
    * @return NULL if initialization was successful, otherwise a string or
    *       array of strings containing error message(s) indicating why
    *       initialization failed.
    */
    public function Initialize()
    {
        return NULL;
    }

    /**
    * Hook methods to be called when specific events occur.
    * For events declared by other plugins the name string should start with
    * the plugin base (class) name followed by "::" and then the event name.
    * @return Array of method names to hook indexed by the event constants
    *       or names to hook them to.
    */
    public function HookEvents()
    {
        return array();
    }

    /**
    * Declare events defined by this plugin.  This is used when a plugin defines
    * new events that it signals or responds to.  Names of these events should
    * begin with the plugin base name, followed by "_EVENT_" and the event name
    * in all caps (for example "MyPlugin_EVENT_MY_EVENT").
    * @return Array with event names for the index and event types for the values.
    */
    public function DeclareEvents()
    {
        return array();
    }

    /**
    * Perform any work needed when the plugin is first installed (for example,
    * creating database tables).
    * @return NULL if installation succeeded, otherwise a string containing
    *       an error message indicating why installation failed.
    */
    public function Install()
    {
        return NULL;
    }

    /**
    * Perform any work needed when the plugin is upgraded to a new version
    * (for example, adding fields to database tables).
    * @param string $PreviousVersion The version number of this plugin that was
    *       previously installed.
    * @return NULL if upgrade succeeded, otherwise a string containing
    *       an error message indicating why upgrade failed.
    */
    public function Upgrade($PreviousVersion)
    {
        return NULL;
    }

    /**
    * Perform any work needed when the plugin is uninstalled.
    * @return NULL if uninstall succeeded, otherwise a string containing
    *       an error message indicating why uninstall failed.
    */
    public function Uninstall()
    {
        return NULL;
    }

    /**
    * Retrieve plugin information.
    * @return Array of attribute values indexed by attribute names.
    */
    public final function GetAttributes()
    {
        return array(
                "Author" => $this->Author,
                "CfgPage" => $this->CfgPage,
                "CfgSetup" => $this->CfgSetup,
                "Description" => $this->Description,
                "Email" => $this->Email,
                "EnabledByDefault" => $this->EnabledByDefault,
                "InitializeAfter" => is_array($this->InitializeAfter)
                        ? $this->InitializeAfter : array($this->InitializeAfter),
                "InitializeBefore" => is_array($this->InitializeBefore)
                        ? $this->InitializeBefore : array($this->InitializeBefore),
                "Instructions" => $this->Instructions,
                "Name" => $this->Name,
                "Requires" => $this->Requires,
                "Url" => $this->Url,
                "Version" => $this->Version,
                );
    }

    /**
    * Get plugin base name.
    * @return string Base name.
    */
    public function GetBaseName()
    {
        return get_class($this);
    }

    /**
    * Get/set plugin configuration setting.  The value returned may have
    * been overridden via ConfigSettingOverride().
    * @param string $SettingName Name of configuration value.
    * @param mixed $NewValue New setting value.
    * @return Requested value, or NULL if value was not set or there was no
    *       configuration value with the specified name.
    * @see Plugin::ConfigSettingOverride()
    */
    public final function ConfigSetting($SettingName, $NewValue = NULL)
    {
        # if a new value was supplied for the setting
        if (func_num_args() > 1)
        {
            # if this setting has a filter function specified
            if (array_key_exists($SettingName, $this->CfgSetup)
                    && array_key_exists("SettingFilter",
                            $this->CfgSetup[$SettingName]))
            {
                # pass new value through filter function
                $FilterMethod = $this->CfgSetup[$SettingName]["SettingFilter"];
                $NewValue = $this->$FilterMethod($SettingName, $NewValue);
            }

            # if caller requested that setting be cleared
            if ($NewValue === NULL)
            {
                # clear setting
                unset($this->Cfg[$SettingName]);
            }
            else
            {
                # save new value for setting
                $this->Cfg[$SettingName] = $NewValue;
            }

            # save new configuration settings
            $DB = new Database();
            $DB->Query("UPDATE PluginInfo SET Cfg = '"
                    .addslashes(serialize($this->Cfg))
                    ."' WHERE BaseName = '"
                    .addslashes($this->GetBaseName())."'");
        }

        # return current value of setting to caller
        return isset($this->CfgOver[$SettingName]) ? $this->CfgOver[$SettingName]
                : (isset($this->Cfg[$SettingName]) ? $this->Cfg[$SettingName] : NULL);
    }

    /**
    * Get plugin configuration setting, ignoring any override value.
    * @param string $SettingName Name of configuration value.
    * @return Requested value, or NULL if value was not set or there was no
    *       configuration value with the specified name.
    */
    public final function GetSavedConfigSetting($SettingName)
    {
        # return current saved value of setting to caller
        return isset($this->Cfg[$SettingName]) ? $this->Cfg[$SettingName] : NULL;
    }

    /**
    * Get type of a plugin configuration setting.
    * @param string $SettingName Name of configuration value.
    * @return string Type of setting, as specified by the plugin, or NULL if
    *       no setting available by that name.
    */
    public final function GetConfigSettingType($SettingName)
    {
        return isset($this->CfgSetup[$SettingName])
                ? $this->CfgSetup[$SettingName]["Type"] : NULL;
    }

    /**
    * Get plugin configuration setting parameters.
    * @param string $SettingName Name of configuration value.
    * @return array Associative array with plugin config settings, as
    *       defined by plugin, or NULL if no setting available with the
    *       specified name.
    */
    public final function GetConfigSettingParameters($SettingName)
    {
        return isset($this->CfgSetup[$SettingName])
                ? $this->CfgSetup[$SettingName] : NULL;
    }

    /**
    * Set override for configuration setting, that will be returned
    * regardless of the current saved configuration setting value.  This
    * does not affect the saved setting value.
    * @param string $SettingName Name of configuration value.
    * @param mixed $Value New override Value.
    * @see Plugin::ConfigSetting()
    */
    public final function ConfigSettingOverride($SettingName, $Value)
    {
        # check that setting name was valid
        if (!isset($this->Cfg[$SettingName]))
        {
            throw new InvalidArgumentException(
                    "Unknown setting name (".$SettingName.").");
        }

        # save override value
        $this->CfgOver[$SettingName] = $Value;
    }

    /**
    * Get/set whether the plugin is ready for use.
    * @param bool $NewValue TRUE if plugin is ready for use, otherwise FALSE.
    *       (OPTIONAL)
    * @return bool TRUE if plugin is ready for use, otherwise FALSE.
    */
    public function IsReady($NewValue = NULL)
    {
        # if new ready status was supplied
        if ($NewValue !== NULL)
        {
            # make sure we are being called from the plugin manager
            StdLib::CheckMyCaller("PluginManager",
                "Attempt to update plugin ready status at %FILE%:%LINE%."
                ."  (Plugin ready status can only be set by PluginManager.)");

            # update plugin ready status
            $this->Ready = $NewValue ? TRUE : FALSE;
        }

        # return current ready status to caller
        return $this->Ready;
    }

    /**
    * Get/set whether the plugin is enabled.  (This is the persistent setting
    * for enabling/disabling, not whether the plugin is currently working.)
    * @param bool $NewValue TRUE to enable, or FALSE to disable.  (OPTIONAL)
    * @param bool $Persistent TRUE to make new setting persistent, or FALSE
    *       for new setting to apply only to this page load.  (OPTIONAL,
    *       defaults to TRUE)
    * @return bool TRUE if plugin is enabled, otherwise FALSE.
    */
    public function IsEnabled($NewValue = NULL, $Persistent = TRUE)
    {
        # if new enabled status was suppled
        if ($NewValue !== NULL)
        {
            # save new status locally
            $this->Enabled = $NewValue ? TRUE : FALSE;

            # update enabled status in database if appropriate
            if ($Persistent)
            {
                $DB = new Database();
                $DB->Query("UPDATE PluginInfo"
                        ." SET Enabled = ".($NewValue ? "1" : "0")
                        ." WHERE BaseName = '".addslashes($this->GetBaseName())."'");
            }
        }

        # return current enabled status to caller
        return $this->Enabled;
    }

    /**
    * Get/set whether the plugin is installed.  This should only be set by
    * the plugin manager.
    * @param bool $NewValue TRUE to mark as installed, or FALSE to mark as
    *       not installed.  (OPTIONAL)
    * @return bool TRUE if plugin is installed, otherwise FALSE.
    */
    public function IsInstalled($NewValue = NULL)
    {
        # if new install status was supplied
        if ($NewValue !== NULL)
        {
            # make sure we are being called from the plugin manager
            StdLib::CheckMyCaller("PluginManager",
                "Attempt to update plugin install status at %FILE%:%LINE%."
                ."  (Plugin install status can only be set by PluginManager.)");

            # update installation setting in database
            $this->Installed = $NewValue ? TRUE : FALSE;
            $DB = new Database();
            $DB->Query("UPDATE PluginInfo"
                    ." SET Installed = ".($NewValue ? "1" : "0")
                    ." WHERE BaseName = '".addslashes($this->GetBaseName())."'");
        }

        # return installed status to caller
        return $this->Installed;
    }

    /**
    * Get/set the last version recorded as installed.  This should only be
    * set by the plugin manager.
    * @param string $NewValue New installed version.  (OPTIONAL)
    * @return string Current installed version.
    */
    public function InstalledVersion($NewValue = NULL)
    {
        # if new version was supplied
        if ($NewValue !== NULL)
        {
            # make sure we are being called from the plugin manager
            StdLib::CheckMyCaller("PluginManager",
                "Attempt to set installed version of plugin at %FILE%:%LINE%."
                ."  (Plugin installed version can only be set by PluginManager.)");

            # update version in database
            $this->InstalledVersion = $NewValue;
            $DB = new Database();
            $DB->Query("UPDATE PluginInfo"
                    ." SET Version = '".addslashes($NewValue)."'"
                    ." WHERE BaseName = '".addslashes($this->GetBaseName())."'");
        }

        # return current installed version to caller
        return $this->InstalledVersion;
    }

    /**
    * Get full name of plugin.
    * @return string Name.
    */
    public function GetName()
    {
        return $this->Name;
    }

    /**
    * Get list of plugins upon which this plugin depends (if any).
    * @return array Versions of required plugins with base names
    *       for the index.
    */
    public function GetDependencies()
    {
        return $this->Requires;
    }

    /**
    * Class constructor -- FOR PLUGIN MANAGER USE ONLY.  Plugins should
    * always be retrieved via PluginManager::GetPlugin(), rather than
    * instantiated directly.  Plugin child classes should perform any
    * needed setup in Initialize(), rather than using a constructor.
    */
    final public function __construct()
    {
        # make sure we are being called from the plugin manager
        StdLib::CheckMyCaller("PluginManager",
                "Attempt to create plugin object at %FILE%:%LINE%."
                ."  (Plugins can only be instantiated by PluginManager.)");

        # register plugin
        $this->Register();

        # load plugin info from database if necessary
        if (!isset(self::$PluginInfoCache))
        {
            $DB = new Database();
            $DB->Query("SELECT * FROM PluginInfo");
            while ($Row = $DB->FetchRow())
            {
                self::$PluginInfoCache[$Row["BaseName"]] = $Row;
            }
        }

        # add plugin to database if not already in there
        $BaseName = get_class($this);
        if (!isset(self::$PluginInfoCache[$BaseName]))
        {
            if (!isset($DB))
            {
                $DB = new Database();
            }
            $Attribs = $this->GetAttributes();
            $DB->Query("INSERT INTO PluginInfo"
                    ." (BaseName, Version, Enabled)"
                    ." VALUES ('".addslashes($BaseName)."', "
                    ." '".addslashes(
                    $Attribs["Version"])."', "
                    ." ".($Attribs["EnabledByDefault"]
                    ? 1 : 0).")");
            $DB->Query("SELECT * FROM PluginInfo WHERE BaseName = '"
                    .addslashes($BaseName)."'");
            self::$PluginInfoCache[$BaseName] = $DB->FetchRow();
        }

        # set internal value
        $Info = self::$PluginInfoCache[$BaseName];
        $this->Enabled = $Info["Enabled"];
        $this->Installed = $Info["Installed"];
        $this->InstalledVersion = $Info["Version"];
        $this->Cfg = unserialize($Info["Cfg"]);
    }

    /**
    * Set the application framework to be referenced within plugins.
    * (This is set by the plugin manager.)
    * @param object $AF ApplicationFramework object.
    */
    static final public function SetApplicationFramework($AF)
    {
        self::$AF = $AF;
    }


    # ----- PROTECTED INTERFACE ----------------------------------------------

    /** Name of the plugin's author. */
    protected $Author = NULL;
    /** Text description of the plugin. */
    protected $Description = NULL;
    /** Contact email for the plugin's author. */
    protected $Email = NULL;
    /** Whether the plugin should be enabled by default when installed. */
    protected $EnabledByDefault = FALSE;
    /** Plugins that should be initialized after us. */
    protected $InitializeBefore = array();
    /** Plugins that should be initialized before us. */
    protected $InitializeAfter = array();
    /** Instructions for configuring the plugin (displayed on the
            automatically-generated configuration page if configuration
            values are supplied). */
    protected $Instructions = NULL;
    /** Proper (human-readable) name of plugin. */
    protected $Name = NULL;
    /** Version number of plugin in the format X.X.X (for example: 1.2.12). */
    protected $Version = NULL;
    /** Web address for more information about the plugin. */
    protected $Url = NULL;

    /** Application framework. */
    static protected $AF;

    /**
    * Array with plugin base (class) names for the index and minimum version
    * numbers for the values.  Special indexes of "PHP" may be used to
    * specify a minimum required PHP version or "PHPX_xxx" to specify a required
    * PHP extension, where "xxx" is the extension name (e.g. "PHPX_GD").  The
    * version number value is ignored for PHP extensions.
    */
    protected $Requires = array();

    /**
    * Associative array describing the configuration values for the plugin.
    * The first index is the name of the configuration setting, and the second
    * indicates the type of information about that setting.  For more information
    * please see <a href="implementingplugins.html">Implementing
    * CWIS Plugins</a>.
    */
    protected $CfgSetup = array();

    /**
    * Name of configuration page for plugin.
    */
    protected $CfgPage = NULL;


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

    /** Plugin configuration values. */
    private $Cfg;
    /** Plugin configuration override values. */
    private $CfgOver;
    /** Whether the plugin is enabled. */
    private $Enabled = FALSE;
    /** Whether the plugin is installed. */
    private $Installed = FALSE;
    /** Version that was last installed. */
    private $InstalledVersion = FALSE;
    /** Whether the plugin is currently ready for use. */
    private $Ready = FALSE;

    /** Cache of setting values from database. */
    private static $PluginInfoCache;

    /** @cond */
    /**
    * Set all configuration values (only for use by PluginManager).
    * @param array $NewValues Array of new configuration values.
    */
    final public function SetAllCfg($NewValues)
    {
        $this->Cfg = $NewValues;
    }
    /** @endcond */
}


