3 # FILE: PluginManager.php
5 # Part of the ScoutLib application support library
6 # Copyright 2009-2013 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
15 # ---- PUBLIC INTERFACE --------------------------------------------------
26 # save framework and directory list for later use
27 $this->AF = $AppFramework;
28 $this->DirsToSearch = $PluginDirectories;
30 # get our own database handle
33 # hook into events to load plugin PHP and HTML files
34 $this->AF->HookEvent(
"EVENT_PHP_FILE_LOAD", array($this,
"FindPluginPhpFile"),
36 $this->AF->HookEvent(
"EVENT_HTML_FILE_LOAD", array($this,
"FindPluginHtmlFile"),
39 # tell PluginCaller helper object how to get to us
55 # clear any existing errors/status
56 $this->ErrMsgs = array();
57 $this->StatusMsgs = array();
59 # load list of all base plugin files
60 $this->FindPlugins($this->DirsToSearch);
62 # for each plugin found
63 foreach ($this->PluginNames as $PluginName)
65 # bring in plugin class file
66 include_once($this->PluginFiles[$PluginName]);
68 # if plugin class was defined by file
69 if (class_exists($PluginName))
71 # if plugin class is a valid descendant of base plugin class
72 $Plugin =
new $PluginName;
73 if (is_subclass_of($Plugin,
"Plugin"))
75 # set hooks needed for plugin to access plugin manager services
76 $Plugin->SetCfgSaveCallback(array(__CLASS__,
"CfgSaveCallback"));
79 $this->Plugins[$PluginName] = $Plugin;
81 $this->Plugins[$PluginName]->Register();
83 # check required plugin attributes
84 $Attribs[$PluginName] = $this->Plugins[$PluginName]->GetAttributes();
85 if (!strlen($Attribs[$PluginName][
"Name"]))
87 $this->ErrMsgs[$PluginName][] =
"Plugin <b>".$PluginName.
"</b>"
88 .
" could not be loaded because it"
89 .
" did not have a <i>Name</i> attribute set.";
91 unset($this->Plugins[$PluginName]);
93 if (!strlen($Attribs[$PluginName][
"Version"]))
95 $this->ErrMsgs[$PluginName][] =
"Plugin <b>".$PluginName.
"</b>"
96 .
" could not be loaded because it"
97 .
" did not have a <i>Version</i> attribute set.";
99 unset($this->Plugins[$PluginName]);
104 $this->ErrMsgs[$PluginName][] =
"Plugin <b>".$PluginName.
"</b>"
105 .
" could not be loaded because <i>".$PluginName.
"</i> was"
106 .
" not a subclass of base <i>Plugin</i> class";
111 $this->ErrMsgs[$PluginName][] =
"Expected class <i>".$PluginName
112 .
"</i> not found in plugin file <i>"
113 .$this->PluginFiles[$PluginName].
"</i>";
117 # check plugin dependencies
118 $this->CheckDependencies();
120 # load plugin configurations
121 $this->DB->Query(
"SELECT BaseName,Cfg FROM PluginInfo");
122 $Cfgs = $this->DB->FetchColumn(
"Cfg",
"BaseName");
125 foreach ($this->Plugins as $PluginName => $Plugin)
127 # if plugin is enabled
130 # if plugin has its own subdirectory
131 if ($this->PluginHasDir[$PluginName])
133 # if plugin has its own object directory
134 $Dir = dirname($this->PluginFiles[$PluginName]);
135 if (is_dir($Dir.
"/objects"))
137 # add object directory to class autoloading list
142 # add plugin directory to class autoloading list
147 # set configuration values if available
148 if (isset($Cfgs[$PluginName]))
150 $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
153 # install or upgrade plugins if needed
154 $this->InstallPlugin($Plugin);
158 # check plugin dependencies again in case an install or upgrade failed
159 $this->CheckDependencies();
161 # set up plugin configuration options
162 foreach ($this->Plugins as $PluginName => $Plugin)
164 if ($ForcePluginConfigOptLoad || $this->
PluginEnabled[$PluginName])
166 $ErrMsg = $Plugin->SetUpConfigOptions();
167 if ($ErrMsg !== NULL)
169 $this->ErrMsgs[$PluginName][] =
"Configuration option setup"
170 .
" failed for plugin <b>".$PluginName.
"</b>: <i>"
177 # initialize enabled plugins
178 foreach ($this->Plugins as $PluginName => $Plugin)
182 $ErrMsg = $Plugin->Initialize();
183 if ($ErrMsg !== NULL)
185 $this->ErrMsgs[$PluginName][] =
"Initialization failed for"
186 .
" plugin <b>".$PluginName.
"</b>: <i>".$ErrMsg.
"</i>";
192 # register any events declared by enabled plugins
193 foreach ($this->Plugins as $PluginName => $Plugin)
197 $Events = $Plugin->DeclareEvents();
198 if (count($Events)) { $this->AF->RegisterEvent($Events); }
202 # hook enabled plugins to events
203 foreach ($this->Plugins as $PluginName => $Plugin)
207 $EventsToHook = $Plugin->HookEvents();
208 if (count($EventsToHook))
210 foreach ($EventsToHook as $EventName => $PluginMethods)
212 if (!is_array($PluginMethods))
213 { $PluginMethods = array($PluginMethods); }
215 foreach ($PluginMethods as $PluginMethod)
217 if ($this->AF->IsStaticOnlyEvent($EventName))
220 $Result = $this->AF->HookEvent(
221 $EventName, array($Caller,
"CallPluginMethod"));
225 $Result = $this->AF->HookEvent(
226 $EventName, array($Plugin, $PluginMethod));
228 if ($Result === FALSE)
230 $this->ErrMsgs[$PluginName][] =
231 "Unable to hook requested event <i>"
232 .$EventName.
"</i> for plugin <b>"
241 # report to caller whether any problems were encountered
242 return count($this->ErrMsgs) ? FALSE : TRUE;
252 return $this->ErrMsgs;
263 return $this->StatusMsgs;
273 return isset($this->Plugins[$PluginName])
274 ? $this->Plugins[$PluginName] : NULL;
286 return $this->
GetPlugin($this->PageFilePlugin);
296 foreach ($this->Plugins as $PluginName => $Plugin)
298 $Info[$PluginName] = $Plugin->GetAttributes();
299 $Info[$PluginName][
"Enabled"] =
300 isset($this->PluginInfo[$PluginName][
"Enabled"])
301 ? $this->PluginInfo[$PluginName][
"Enabled"] : FALSE;
302 $Info[$PluginName][
"Installed"] =
303 isset($this->PluginInfo[$PluginName][
"Installed"])
304 ? $this->PluginInfo[$PluginName][
"Installed"] : FALSE;
305 $Info[$PluginName][
"ClassFile"] = $this->PluginFiles[$PluginName];
317 $Dependents = array();
319 foreach ($AllAttribs as $Name => $Attribs)
321 if (array_key_exists($PluginName, $Attribs[
"Requires"]))
323 $Dependents[] = $Name;
325 $Dependents = array_merge($Dependents, $SubDependents);
348 # if an enabled/disabled value was supplied
349 if ($NewValue !== NULL)
351 # if enabled/disabled status has changed for plugin
352 if (($NewValue && !$this->
PluginEnabled[$PluginName][
"Enabled"])
353 || (!$NewValue && $this->
PluginEnabled[$PluginName][
"Enabled"]))
355 $Info = $this->Plugins[$PluginName]->GetAttributes();
356 $this->DB->Query(
"UPDATE PluginInfo"
357 .
" SET Enabled = ".($NewValue ?
"1" :
"0")
358 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
360 $this->PluginInfo[$PluginName][
"Enabled"] = $NewValue;
361 $this->StatusMsgs[$PluginName][] =
"Plugin <i>"
362 .$Info[
"Name"].
"</i> "
363 .($NewValue ?
"enabled" :
"disabled").
".";
380 # if plugin is installed
381 if ($this->PluginInfo[$PluginName][
"Installed"])
383 # call uninstall method for plugin
384 $Result = $this->Plugins[$PluginName]->Uninstall();
386 # if plugin uninstall method succeeded
387 if ($Result === NULL)
389 # remove plugin info from database
390 $this->DB->Query(
"DELETE FROM PluginInfo"
391 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
393 # drop our data for the plugin
394 unset($this->Plugins[$PluginName]);
395 unset($this->PluginInfo[$PluginName]);
397 unset($this->PluginNames[$PluginName]);
398 unset($this->PluginFiles[$PluginName]);
402 # report results (if any) to caller
407 # ---- PRIVATE INTERFACE -------------------------------------------------
409 private $Plugins = array();
410 private $PluginFiles = array();
411 private $PluginNames = array();
412 private $PluginHasDir = array();
413 private $PluginInfo = array();
414 private $PluginEnabled = array();
415 private $PageFilePlugin = NULL;
417 private $DirsToSearch;
418 private $ErrMsgs = array();
419 private $StatusMsgs = array();
427 private function FindPlugins($DirsToSearch)
430 foreach ($DirsToSearch as $Dir)
432 # if directory exists
435 # for each file in directory
436 $FileNames = scandir($Dir);
437 foreach ($FileNames as $FileName)
439 # if file looks like base plugin file
440 if (preg_match(
"/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
443 $PluginName = preg_replace(
"/\.php$/",
"", $FileName);
444 $this->PluginNames[$PluginName] = $PluginName;
445 $this->PluginFiles[$PluginName] = $Dir.
"/".$FileName;
446 $this->PluginHasDir[$PluginName] = FALSE;
448 # else if file looks like plugin directory
449 elseif (is_dir($Dir.
"/".$FileName)
450 && preg_match(
"/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
452 # if there is a base plugin file in the directory
453 $PluginName = $FileName;
454 $PluginFile = $Dir.
"/".$PluginName.
"/".$PluginName.
".php";
455 if (file_exists($PluginFile))
457 # add plugin and directory to lists
458 $this->PluginNames[$PluginName] = $PluginName;
459 $this->PluginFiles[$PluginName] = $PluginFile;
460 $this->PluginHasDir[$PluginName] = TRUE;
462 # else if we don't already have a base plugin file
463 elseif (!array_key_exists($PluginName, $this->PluginFiles))
465 $this->ErrMsgs[$PluginName][] =
466 "Expected plugin file <i>".$PluginName.
".php</i> not"
467 .
" found in plugin subdirectory <i>"
468 .$Dir.
"/".$PluginName.
"</i>";
481 private function InstallPlugin($Plugin)
483 # cache all plugin info from database
484 if (count($this->PluginInfo) == 0)
486 $this->DB->Query(
"SELECT * FROM PluginInfo");
487 while ($Row = $this->DB->FetchRow())
489 $this->PluginInfo[$Row[
"BaseName"]] = $Row;
493 # if plugin was not found in database
494 $PluginName = get_class($Plugin);
495 $Attribs = $Plugin->GetAttributes();
496 if (!isset($this->PluginInfo[$PluginName]))
498 # add plugin to database
499 $this->DB->Query(
"INSERT INTO PluginInfo"
500 .
" (BaseName, Version, Installed, Enabled)"
501 .
" VALUES ('".addslashes($PluginName).
"', "
502 .
" '".addslashes($Attribs[
"Version"]).
"', "
504 .
" ".($Attribs[
"EnabledByDefault"] ? 1 : 0).
")");
506 # read plugin settings back in
507 $this->DB->Query(
"SELECT * FROM PluginInfo"
508 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
509 $this->PluginInfo[$PluginName] = $this->DB->FetchRow();
512 # store plugin settings for later use
513 $this->
PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName][
"Enabled"];
515 # if plugin is enabled
518 # if plugin has not been installed
519 if (!$this->PluginInfo[$PluginName][
"Installed"])
521 # set default values if present
522 $Attribs = $Plugin->GetAttributes();
523 if (isset($Attribs[
"CfgSetup"]))
525 foreach ($Attribs[
"CfgSetup"] as $CfgValName => $CfgSetup)
527 if (isset($CfgSetup[
"Default"]))
529 $Plugin->ConfigSetting($CfgValName,
530 $CfgSetup[
"Default"]);
536 $ErrMsg = $Plugin->Install();
538 # if install succeeded
541 # mark plugin as installed
542 $this->DB->Query(
"UPDATE PluginInfo SET Installed = 1"
543 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
544 $this->PluginInfo[$PluginName][
"Installed"] = 1;
551 # record error message about installation failure
552 $this->ErrMsgs[$PluginName][] =
"Installation of plugin <b>"
553 .$PluginName.
"</b> failed: <i>".$ErrMsg.
"</i>";;
558 # if plugin version is newer than version in database
559 if (version_compare($Attribs[
"Version"],
560 $this->PluginInfo[$PluginName][
"Version"]) == 1)
562 # set default values for any new configuration settings
563 $Attribs = $Plugin->GetAttributes();
564 if (isset($Attribs[
"CfgSetup"]))
566 foreach ($Attribs[
"CfgSetup"] as $CfgValName => $CfgSetup)
568 if (isset($CfgSetup[
"Default"])
569 && ($Plugin->ConfigSetting(
570 $CfgValName) === NULL))
572 $Plugin->ConfigSetting($CfgValName,
573 $CfgSetup[
"Default"]);
579 $ErrMsg = $Plugin->Upgrade(
580 $this->PluginInfo[$PluginName][
"Version"]);
582 # if upgrade succeeded
585 # update plugin version in database
586 $this->DB->Query(
"UPDATE PluginInfo"
587 .
" SET Version = '".addslashes($Attribs[
"Version"]).
"'"
588 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
589 $this->PluginInfo[$PluginName][
"Version"] = $Attribs[
"Version"];
596 # record error message about upgrade failure
597 $this->ErrMsgs[$PluginName][] =
"Upgrade of plugin <b>"
598 .$PluginName.
"</b> from version <i>"
599 .addslashes($this->PluginInfo[$PluginName][
"Version"])
600 .
"</i> to version <i>"
601 .addslashes($Attribs[
"Version"]).
"</i> failed: <i>"
605 # else if plugin version is older than version in database
606 elseif (version_compare($Attribs[
"Version"],
607 $this->PluginInfo[$PluginName][
"Version"]) == -1)
612 # record error message about version conflict
613 $this->ErrMsgs[$PluginName][] =
"Plugin <b>"
614 .$PluginName.
"</b> is older (<i>"
615 .addslashes($Attribs[
"Version"])
616 .
"</i>) than previously-installed version (<i>"
617 .addslashes($this->PluginInfo[$PluginName][
"Version"]).
"</i>).";
627 private function CheckDependencies()
629 # look until all enabled plugins check out okay
632 # start out assuming all plugins are okay
636 foreach ($this->Plugins as $PluginName => $Plugin)
638 # if plugin is currently enabled
641 # load plugin attributes
642 if (!isset($Attribs[$PluginName]))
644 $Attribs[$PluginName] =
645 $this->Plugins[$PluginName]->GetAttributes();
648 # for each dependency for this plugin
649 foreach ($Attribs[$PluginName][
"Requires"]
650 as $ReqName => $ReqVersion)
652 # handle PHP version requirements
653 if ($ReqName ==
"PHP")
655 if (version_compare($ReqVersion, phpversion(),
">"))
657 $this->ErrMsgs[$PluginName][] =
"PHP version "
658 .
"<i>".$ReqVersion.
"</i>"
659 .
" required by <b>".$PluginName.
"</b>"
660 .
" was not available. (Current PHP version"
661 .
" is <i>".phpversion().
"</i>.)";
664 # handle PHP extension requirements
665 elseif (preg_match(
"/^PHPX_/", $ReqName))
667 list($Dummy, $ExtensionName) = explode(
"_", $ReqName, 2);
668 if (!extension_loaded($ExtensionName))
670 $this->ErrMsgs[$PluginName][] =
"PHP extension "
671 .
"<i>".$ExtensionName.
"</i>"
672 .
" required by <b>".$PluginName.
"</b>"
673 .
" was not available.";
676 # handle dependencies on other plugins
679 # load plugin attributes if not already loaded
680 if (isset($this->Plugins[$ReqName])
681 && !isset($Attribs[$ReqName]))
684 $this->Plugins[$ReqName]->GetAttributes();
687 # if target plugin is not present or is disabled or is too old
690 || (version_compare($ReqVersion,
691 $Attribs[$ReqName][
"Version"],
">")))
693 # add error message and disable plugin
694 $this->ErrMsgs[$PluginName][] =
"Plugin <i>"
695 .$ReqName.
" ".$ReqVersion.
"</i>"
696 .
" required by <b>".$PluginName.
"</b>"
697 .
" was not available.";
700 # set flag indicating plugin did not check out
707 }
while ($AllOkay == FALSE);
720 # build list of possible locations for file
722 "local/plugins/%PLUGIN%/pages/%PAGE%.php",
723 "plugins/%PLUGIN%/pages/%PAGE%.php",
724 "local/plugins/%PLUGIN%/%PAGE%.php",
725 "plugins/%PLUGIN%/%PAGE%.php",
728 # look for file and return (possibly) updated page to caller
729 return $this->FindPluginPageFile($PageName, $Locations);
743 # build list of possible locations for file
745 "local/plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
746 "plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
747 "local/plugins/%PLUGIN%/interface/default/%PAGE%.html",
748 "plugins/%PLUGIN%/interface/default/%PAGE%.html",
749 "local/plugins/%PLUGIN%/%PAGE%.html",
750 "plugins/%PLUGIN%/%PAGE%.html",
754 $Params = $this->FindPluginPageFile($PageName, $Locations);
756 # if plugin HTML file was found
757 if ($Params[
"PageName"] != $PageName)
759 # add subdirectories for plugin to search paths
760 $Dir = preg_replace(
"%^local/%",
"", dirname($Params[
"PageName"]));
761 $GLOBALS[
"AF"]->AddImageDirectories(array(
762 "local/".$Dir.
"/images",
765 $GLOBALS[
"AF"]->AddIncludeDirectories(array(
766 "local/".$Dir.
"/include",
769 $GLOBALS[
"AF"]->AddFunctionDirectories(array(
770 "local/".$Dir.
"/include",
775 # return possibly revised HTML file name to caller
788 private function FindPluginPageFile($PageName, $Locations)
790 # set up return value assuming we will not find plugin page file
791 $ReturnValue[
"PageName"] = $PageName;
793 # look for plugin name and plugin page name in base page name
794 preg_match(
"/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);
796 # if base page name contained name of enabled plugin with its own subdirectory
797 if ((count($Matches) == 3)
799 && $this->PluginHasDir[$Matches[1]])
801 # for each possible location
802 $ActiveUI = $GLOBALS[
"AF"]->ActiveUserInterface();
803 $PluginName = $Matches[1];
804 $PageName = $Matches[2];
805 foreach ($Locations as $Loc)
807 # make any needed substitutions into path
808 $FileName = str_replace(array(
"%ACTIVEUI%",
"%PLUGIN%",
"%PAGE%"),
809 array($ActiveUI, $PluginName, $PageName), $Loc);
811 # if file exists in this location
812 if (file_exists($FileName))
814 # set return value to contain full plugin page file name
815 $ReturnValue[
"PageName"] = $FileName;
817 # save plugin name as home of current page
818 $this->PageFilePlugin = $PluginName;
820 # set G_Plugin to plugin associated with current page
829 # return array containing page name or page file name to caller
843 $DB->Query(
"UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
844 .
"' WHERE BaseName = '".addslashes($BaseName).
"'");
870 $this->PluginName = $PluginName;
871 $this->MethodName = $MethodName;
881 $Args = func_get_args();
882 $Plugin = self::$Manager->GetPlugin($this->PluginName);
883 return call_user_func_array(array($Plugin, $this->MethodName), $Args);
892 return $this->PluginName.
"::".$this->MethodName;
902 return array(
"PluginName",
"MethodName");
LoadPlugins($ForcePluginConfigOptLoad=FALSE)
Load and initialize plugins.
GetErrorMessages()
Retrieve any error messages generated during plugin loading.
__sleep()
Sleep method, specifying which values are to be saved when we are serialized.
__construct($PluginName, $MethodName)
Class constructor, which stores the plugin name and the name of the method to be called.
Manager to load and invoke plugins.
static CfgSaveCallback($BaseName, $Cfg)
SQL database abstraction object with smart query caching.
UninstallPlugin($PluginName)
Uninstall plugin and (optionally) delete any associated data.
GetDependents($PluginName)
Returns a list of plugins dependent on the specified plugin.
FindPluginPhpFile($PageName)
static AddObjectDirectory($Dir, $Prefix="", $ClassPattern=NULL, $ClassReplacement=NULL)
Add directory to be searched for object files when autoloading.
GetActivePluginList()
Get list of active (i.e.
FindPluginHtmlFile($PageName)
GetPluginAttributes()
Retrieve info about currently loaded plugins.
GetPlugin($PluginName)
Retrieve specified plugin.
PluginEnabled($PluginName, $NewValue=NULL)
Get/set whether specified plugin is enabled.
__construct($AppFramework, $PluginDirectories)
PluginManager class constructor.
GetPluginForCurrentPage()
Retrieve plugin for current page (if any).
GetCallbackAsText()
Get full method name as a text string.
const ORDER_LAST
Run hooked function last (i.e.
GetStatusMessages()
Get/set any status messages generated during plugin loading or via plugin method calls.
static $Manager
PluginManager to use to retrieve appropriate plugins.
CallPluginMethod()
Call the method that was specified in our constructor.