CWIS Developer Documentation
PluginManager.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: PluginManager.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2009-2013 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 {
15 
16  # ---- PUBLIC INTERFACE --------------------------------------------------
17 
25  public function __construct($AppFramework, $PluginDirectories)
26  {
27  # save framework and directory list for later use
28  $this->AF = $AppFramework;
29  Plugin::SetApplicationFramework($AppFramework);
30  $this->DirsToSearch = $PluginDirectories;
31 
32  # get our own database handle
33  $this->DB = new Database();
34 
35  # hook into events to load plugin PHP and HTML files
36  $this->AF->HookEvent("EVENT_PHP_FILE_LOAD", array($this, "FindPluginPhpFile"),
37  ApplicationFramework::ORDER_LAST);
38  $this->AF->HookEvent("EVENT_HTML_FILE_LOAD", array($this, "FindPluginHtmlFile"),
39  ApplicationFramework::ORDER_LAST);
40 
41  # tell PluginCaller helper object how to get to us
42  PluginCaller::$Manager = $this;
43  }
44 
49  public function LoadPlugins()
50  {
51  $ErrMsgs = array();
52 
53  # look for plugin files
54  $PluginFiles = $this->FindPlugins($this->DirsToSearch);
55 
56  # for each plugin found
57  foreach ($PluginFiles as $PluginName => $PluginFileName)
58  {
59  # attempt to load plugin
60  $Result = $this->LoadPlugin($PluginName, $PluginFileName);
61 
62  # if errors were encountered during loading
63  if (is_array($Result))
64  {
65  # save errors
66  $ErrMsgs[$PluginName][] = $Result;
67  }
68  else
69  {
70  # add plugin to list of loaded plugins
71  $this->Plugins[$PluginName] = $Result;
72  }
73  }
74 
75  # check dependencies and drop any plugins with failed dependencies
76  $DepErrMsgs = $this->CheckDependencies($this->Plugins);
77  $DisabledPlugins = array();
78  foreach ($DepErrMsgs as $PluginName => $Msgs)
79  {
80  $DisabledPlugins[] = $PluginName;
81  foreach ($Msgs as $Msg)
82  {
83  $ErrMsgs[$PluginName][] = $Msg;
84  }
85  }
86 
87  # sort plugins according to any loading order requests
88  $this->Plugins = $this->SortPluginsByInitializationPrecedence(
89  $this->Plugins);
90 
91  # for each plugin
92  foreach ($this->Plugins as $PluginName => $Plugin)
93  {
94  # if plugin is loaded and enabled
95  if (!in_array($PluginName, $DisabledPlugins)
96  && $Plugin->IsEnabled())
97  {
98  # attempt to make plugin ready
99  try
100  {
101  $Result = $this->ReadyPlugin($Plugin);
102  }
103  catch (Exception $Except)
104  {
105  $Result = array("Uncaught Exception: ".$Except->getMessage());
106  }
107 
108  # if making plugin ready failed
109  if ($Result !== NULL)
110  {
111  # save error messages
112  foreach ($Result as $Msg)
113  {
114  $ErrMsgs[$PluginName][] = $Msg;
115  }
116  }
117  else
118  {
119  # mark plugin as ready
120  $Plugin->IsReady(TRUE);
121  }
122  }
123  }
124 
125  # check plugin dependencies again in case an install or upgrade failed
126  $DepErrMsgs = $this->CheckDependencies($this->Plugins, TRUE);
127 
128  # for any plugins that were disabled because of dependencies
129  foreach ($DepErrMsgs as $PluginName => $Msgs)
130  {
131  # make sure all plugin hooks are undone
132  $this->UnhookPlugin($this->Plugins[$PluginName]);
133 
134  # mark the plugin as unready
135  $this->Plugins[$PluginName]->IsReady(FALSE);
136 
137  # record any errors that were reported
138  foreach ($Msgs as $Msg)
139  {
140  $ErrMsgs[$PluginName][] = $Msg;
141  }
142  }
143 
144  # save plugin files names and any error messages for later use
145  $this->PluginFiles = $PluginFiles;
146  $this->ErrMsgs = $ErrMsgs;
147 
148  # report to caller whether any problems were encountered
149  return count($ErrMsgs) ? FALSE : TRUE;
150  }
151 
157  public function GetErrorMessages()
158  {
159  return $this->ErrMsgs;
160  }
161 
170  public function GetPlugin($PluginName, $EvenIfNotReady = FALSE)
171  {
172  if (!$EvenIfNotReady && array_key_exists($PluginName, $this->Plugins)
173  && !$this->Plugins[$PluginName]->IsReady())
174  {
175  $Trace = debug_backtrace();
176  $Caller = basename($Trace[0]["file"]).":".$Trace[0]["line"];
177  throw new Exception("Attempt to access uninitialized plugin "
178  .$PluginName." from ".$Caller);
179  }
180  return isset($this->Plugins[$PluginName])
181  ? $this->Plugins[$PluginName] : NULL;
182  }
183 
188  public function GetPlugins()
189  {
190  return $this->Plugins;
191  }
192 
200  public function GetPluginForCurrentPage()
201  {
202  return $this->GetPlugin($this->PageFilePlugin);
203  }
204 
210  public function GetPluginAttributes()
211  {
212  # for each loaded plugin
213  $Info = array();
214  foreach ($this->Plugins as $PluginName => $Plugin)
215  {
216  # retrieve plugin attributes
217  $Info[$PluginName] = $Plugin->GetAttributes();
218 
219  # add in other values to attributes
220  $Info[$PluginName]["Enabled"] = $Plugin->IsEnabled();
221  $Info[$PluginName]["Installed"] = $Plugin->IsInstalled();
222  $Info[$PluginName]["ClassFile"] = $this->PluginFiles[$PluginName];
223  }
224 
225  # sort plugins by name
226  uasort($Info, function ($A, $B) {
227  $AName = strtoupper($A["Name"]);
228  $BName = strtoupper($B["Name"]);
229  return ($AName == $BName) ? 0
230  : ($AName < $BName) ? -1 : 1;
231  });
232 
233  # return plugin info to caller
234  return $Info;
235  }
236 
242  public function GetDependents($PluginName)
243  {
244  $Dependents = array();
245  $AllAttribs = $this->GetPluginAttributes();
246  foreach ($AllAttribs as $Name => $Attribs)
247  {
248  if (array_key_exists($PluginName, $Attribs["Requires"]))
249  {
250  $Dependents[] = $Name;
251  $SubDependents = $this->GetDependents($Name);
252  $Dependents = array_merge($Dependents, $SubDependents);
253  }
254  }
255  return $Dependents;
256  }
257 
262  public function GetActivePluginList()
263  {
264  $ActivePluginNames = array();
265  foreach ($this->Plugins as $PluginName => $Plugin)
266  {
267  if ($Plugin->IsReady())
268  {
269  $ActivePluginNames[] = $PluginName;
270  }
271  }
272  return $ActivePluginNames;
273  }
274 
281  public function PluginEnabled($PluginName, $NewValue = NULL)
282  {
283  return !isset($this->Plugins[$PluginName]) ? FALSE
284  : $this->Plugins[$PluginName]->IsEnabled($NewValue);
285  }
286 
292  public function UninstallPlugin($PluginName)
293  {
294  # assume success
295  $Result = NULL;
296 
297  # if plugin is installed
298  if ($this->Plugins[$PluginName]->IsInstalled())
299  {
300  # call uninstall method for plugin
301  $Result = $this->Plugins[$PluginName]->Uninstall();
302 
303  # if plugin uninstall method succeeded
304  if ($Result === NULL)
305  {
306  # remove plugin info from database
307  $this->DB->Query("DELETE FROM PluginInfo"
308  ." WHERE BaseName = '".addslashes($PluginName)."'");
309 
310  # drop our data for the plugin
311  unset($this->Plugins[$PluginName]);
312  unset($this->PluginFiles[$PluginName]);
313  }
314  }
315 
316  # report results (if any) to caller
317  return $Result;
318  }
319 
325  static public function SetConfigValueLoader($Func)
326  {
327  if (!is_callable($Func))
328  {
329  throw new InvalidArgumentException(
330  "Invalid configuration value loading function supplied.");
331  }
332  self::$CfgValueLoader = $Func;
333  }
334 
335 
336  # ---- PRIVATE INTERFACE -------------------------------------------------
337 
338  private $AF;
339  private $DB;
340  private $DirsToSearch;
341  private $ErrMsgs = array();
342  private $PageFilePlugin = NULL;
343  private $Plugins = array();
344  private $PluginFiles = array();
345  private $PluginHasDir = array();
346 
347  static private $CfgValueLoader;
348 
358  private function FindPlugins($DirsToSearch)
359  {
360  # for each directory
361  $PluginFiles = array();
362  foreach ($DirsToSearch as $Dir)
363  {
364  # if directory exists
365  if (is_dir($Dir))
366  {
367  # for each file in directory
368  $FileNames = scandir($Dir);
369  foreach ($FileNames as $FileName)
370  {
371  # if file looks like base plugin file
372  if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
373  {
374  # if we do not already have a plugin with that name
375  $PluginName = substr($FileName, 0, -4);
376  if (!isset($PluginFiles[$PluginName]))
377  {
378  # add file to list
379  $PluginFiles[$PluginName] = $Dir."/".$FileName;
380  }
381  }
382  # else if file looks like plugin directory
383  elseif (is_dir($Dir."/".$FileName)
384  && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
385  {
386  # if there is a base plugin file in the directory
387  $PluginName = $FileName;
388  $PluginFile = $Dir."/".$PluginName."/".$PluginName.".php";
389  if (file_exists($PluginFile))
390  {
391  # add file to list
392  $PluginFiles[$PluginName] = $PluginFile;
393  }
394  else
395  {
396  # record error
397  $this->ErrMsgs[$PluginName][] =
398  "Expected plugin file <i>".$PluginName.".php</i> not"
399  ." found in plugin subdirectory <i>"
400  .$Dir."/".$PluginName."</i>";
401  }
402  }
403  }
404  }
405  }
406 
407  # return info about found plugins to caller
408  return $PluginFiles;
409  }
410 
418  private function LoadPlugin($PluginName, $PluginFileName)
419  {
420  # bring in plugin class file
421  include_once($PluginFileName);
422 
423  # if plugin class was not defined by file
424  if (!class_exists($PluginName))
425  {
426  $ErrMsgs[] = "Expected class <i>".$PluginName
427  ."</i> not found in plugin file <i>"
428  .$PluginFileName."</i>";
429  }
430  else
431  {
432  # if plugin class is not a valid descendant of base plugin class
433  if (!is_subclass_of($PluginName, "Plugin"))
434  {
435  $ErrMsgs[] = "Plugin <b>".$PluginName."</b>"
436  ." could not be loaded because <i>".$PluginName."</i> class"
437  ." was not a subclass of base <i>Plugin</i> class";
438  }
439  else
440  {
441  # instantiate and register the plugin
442  $Plugin = new $PluginName($PluginName);
443 
444  # check required plugin attributes
445  $RequiredAttribs = array("Name", "Version");
446  $Attribs = $Plugin->GetAttributes();
447  foreach ($RequiredAttribs as $AttribName)
448  {
449  if (!strlen($Attribs[$AttribName]))
450  {
451  $ErrMsgs[] = "Plugin <b>".$PluginName."</b>"
452  ." could not be loaded because it"
453  ." did not have a <i>"
454  .$AttribName."</i> attribute set.";
455  }
456  }
457 
458  # if all required attributes were found
459  if (!isset($ErrMsgs))
460  {
461  # if plugin has its own subdirectory
462  $this->PluginHasDir[$PluginName] = preg_match(
463  "%/".$PluginName."/".$PluginName.".php\$%",
464  $PluginFileName) ? TRUE : FALSE;
465  if ($this->PluginHasDir[$PluginName])
466  {
467  # if plugin has its own object directory
468  $Dir = dirname($PluginFileName);
469  if (is_dir($Dir."/objects"))
470  {
471  # add object directory to class autoloading list
472  ApplicationFramework::AddObjectDirectory($Dir."/objects");
473  }
474  else
475  {
476  # add plugin directory to class autoloading list
477  ApplicationFramework::AddObjectDirectory($Dir);
478  }
479 
480  # if plugin has its own interface directory
481  $InterfaceDir = $Dir."/interface";
482  if (is_dir($InterfaceDir))
483  {
484  # scan contents of interface directory for
485  # entries other than the default default
486  $InterfaceSubdirsFound = FALSE;
487  foreach (scandir($InterfaceDir) as $Entry)
488  {
489  if (($Entry != "default") && ($Entry[0] != "."))
490  {
491  $InterfaceSubdirsFound = TRUE;
492  break;
493  }
494  }
495 
496  # if entries found other than the default default
497  if ($InterfaceSubdirsFound)
498  {
499  # add directory to those scanned for interfaces
500  $this->AF->AddInterfaceDirectories(
501  array($InterfaceDir."/%ACTIVEUI%/",
502  $InterfaceDir."/%DEFAULTUI%/"));
503  }
504 
505  # add plugin interface object directories if present
506  $ActiveUI = $this->AF->ActiveUserInterface();
507  $DefaultUI = $this->AF->DefaultUserInterface();
508  if (is_dir($InterfaceDir."/".$ActiveUI."/objects"))
509  {
510  ApplicationFramework::AddObjectDirectory(
511  $InterfaceDir."/%ACTIVEUI%/objects");
512  }
513  if (is_dir($InterfaceDir."/".$DefaultUI."/objects"))
514  {
515  ApplicationFramework::AddObjectDirectory(
516  $InterfaceDir."/%DEFAULTUI%/objects");
517  }
518 
519  # add plugin interface include directories if present
520  if (is_dir($InterfaceDir."/".$DefaultUI."/include"))
521  {
522  $this->AF->AddIncludeDirectories(
523  $InterfaceDir."/%DEFAULTUI%/include");
524  }
525  if (is_dir($InterfaceDir."/".$ActiveUI."/include"))
526  {
527  $this->AF->AddIncludeDirectories(
528  $InterfaceDir."/%ACTIVEUI%/include");
529  }
530  }
531  }
532  }
533  }
534  }
535 
536  # return loaded plugin or error messages, as appropriate
537  return isset($ErrMsgs) ? $ErrMsgs : $Plugin;
538  }
539 
545  private function ReadyPlugin(&$Plugin)
546  {
547  # install or upgrade plugin if needed
548  $PluginInstalled = $this->InstallPlugin($Plugin);
549 
550  # if install/upgrade failed
551  if (is_string($PluginInstalled))
552  {
553  # report errors to caller
554  return array($PluginInstalled);
555  }
556 
557  # set up plugin configuration options
558  $ErrMsgs = $Plugin->SetUpConfigOptions();
559 
560  # if plugin configuration setup failed
561  if ($ErrMsgs !== NULL)
562  {
563  # report errors to caller
564  return is_array($ErrMsgs) ? $ErrMsgs : array($ErrMsgs);
565  }
566 
567  # set default configuration values if necessary
568  if ($PluginInstalled)
569  {
570  $this->SetPluginDefaultConfigValues($Plugin);
571  }
572 
573  # initialize the plugin
574  $ErrMsgs = $Plugin->Initialize();
575 
576  # if initialization failed
577  if ($ErrMsgs !== NULL)
578  {
579  # report errors to caller
580  return is_array($ErrMsgs) ? $ErrMsgs : array($ErrMsgs);
581  }
582 
583  # register and hook any events for plugin
584  $ErrMsgs = $this->HookPlugin($Plugin);
585 
586  # make sure all hooks are undone if hooking failed
587  if ($ErrMsgs !== NULL)
588  {
589  $this->UnhookPlugin($Plugin);
590  }
591 
592  # report result to caller
593  return $ErrMsgs;
594  }
595 
601  private function HookPlugin(&$Plugin)
602  {
603  # register any events declared by plugin
604  $Events = $Plugin->DeclareEvents();
605  if (count($Events)) { $this->AF->RegisterEvent($Events); }
606 
607  # if plugin has events that need to be hooked
608  $EventsToHook = $Plugin->HookEvents();
609  if (count($EventsToHook))
610  {
611  # for each event
612  $ErrMsgs = array();
613  foreach ($EventsToHook as $EventName => $PluginMethods)
614  {
615  # for each method to hook for the event
616  if (!is_array($PluginMethods))
617  { $PluginMethods = array($PluginMethods); }
618  foreach ($PluginMethods as $PluginMethod)
619  {
620  # if the event only allows static callbacks
621  if ($this->AF->IsStaticOnlyEvent($EventName))
622  {
623  # hook event with shell for static callback
624  $Caller = new PluginCaller(
625  $Plugin->GetBaseName(), $PluginMethod);
626  $Result = $this->AF->HookEvent(
627  $EventName,
628  array($Caller, "CallPluginMethod"));
629  }
630  else
631  {
632  # hook event
633  $Result = $this->AF->HookEvent(
634  $EventName, array($Plugin, $PluginMethod));
635  }
636 
637  # record any errors
638  if ($Result === FALSE)
639  {
640  $ErrMsgs[] = "Unable to hook requested event <i>"
641  .$EventName."</i> for plugin <b>"
642  .$Plugin->GetBaseName()."</b>";
643  }
644  }
645  }
646 
647  # if event hook setup failed
648  if (count($ErrMsgs))
649  {
650  # report errors to caller
651  return $ErrMsgs;
652  }
653  }
654 
655  # report success to caller
656  return NULL;
657  }
658 
663  private function UnhookPlugin(&$Plugin)
664  {
665  # if plugin had events to hook
666  $EventsToHook = $Plugin->HookEvents();
667  if (count($EventsToHook))
668  {
669  # for each event
670  $ErrMsgs = array();
671  foreach ($EventsToHook as $EventName => $PluginMethods)
672  {
673  # for each method to hook for the event
674  if (!is_array($PluginMethods))
675  { $PluginMethods = array($PluginMethods); }
676  foreach ($PluginMethods as $PluginMethod)
677  {
678  # if the event only allows static callbacks
679  if ($this->AF->IsStaticOnlyEvent($EventName))
680  {
681  # unhook event with shell for static callback
682  $Caller = new PluginCaller(
683  $Plugin->GetBaseName(), $PluginMethod);
684  $this->AF->UnhookEvent($EventName,
685  array($Caller, "CallPluginMethod"));
686  }
687  else
688  {
689  # unhook event
690  $this->AF->UnhookEvent(
691  $EventName, array($Plugin, $PluginMethod));
692  }
693  }
694  }
695  }
696  }
697 
705  private function InstallPlugin(&$Plugin)
706  {
707  # if plugin has not been installed
708  $InstallOrUpgradePerformed = FALSE;
709  $PluginName = $Plugin->GetBaseName();
710  $Attribs = $Plugin->GetAttributes();
711  $LockName = __CLASS__.":Install:".$PluginName;
712  if (!$Plugin->IsInstalled())
713  {
714  # set default values if present
715  $this->SetPluginDefaultConfigValues($Plugin, TRUE);
716 
717  # try to get lock to prevent anyone else from trying to run
718  # install or upgrade at the same time
719  $GotLock = $this->AF->GetLock($LockName, FALSE);
720 
721  # if could not get lock
722  if (!$GotLock)
723  {
724  # return error
725  return "Installation of plugin <b>"
726  .$PluginName."</b> in progress.";
727  }
728 
729  # install plugin
730  $ErrMsg = $Plugin->Install();
731  $InstallOrUpgradePerformed = TRUE;
732 
733  # if install succeeded
734  if ($ErrMsg == NULL)
735  {
736  # mark plugin as installed
737  $Plugin->IsInstalled(TRUE);
738 
739  # release lock
740  $this->AF->ReleaseLock($LockName);
741  }
742  else
743  {
744  # release lock
745  $this->AF->ReleaseLock($LockName);
746 
747  # return error message about installation failure
748  return "Installation of plugin <b>"
749  .$PluginName."</b> failed: <i>".$ErrMsg."</i>";
750  }
751  }
752  else
753  {
754  # if plugin version is newer than version in database
755  if (version_compare($Attribs["Version"],
756  $Plugin->InstalledVersion()) == 1)
757  {
758  # set default values for any new configuration settings
759  $this->SetPluginDefaultConfigValues($Plugin);
760 
761  # try to get lock to prevent anyone else from trying to run
762  # upgrade or install at the same time
763  $GotLock = $this->AF->GetLock($LockName, FALSE);
764 
765  # if could not get lock
766  if (!$GotLock)
767  {
768  # return error
769  return "Upgrade of plugin <b>"
770  .$PluginName."</b> in progress.";
771  }
772 
773  # upgrade plugin
774  $ErrMsg = $Plugin->Upgrade($Plugin->InstalledVersion());
775  $InstallOrUpgradePerformed = TRUE;
776 
777  # if upgrade succeeded
778  if ($ErrMsg === NULL)
779  {
780  # update plugin version in database
781  $Plugin->InstalledVersion($Attribs["Version"]);
782 
783  # release lock
784  $this->AF->ReleaseLock($LockName);
785  }
786  else
787  {
788  # release lock
789  $this->AF->ReleaseLock($LockName);
790 
791  # report error message about upgrade failure
792  return "Upgrade of plugin <b>"
793  .$PluginName."</b> from version <i>"
794  .addslashes($Plugin->InstalledVersion())
795  ."</i> to version <i>"
796  .addslashes($Attribs["Version"])."</i> failed: <i>"
797  .$ErrMsg."</i>";
798  }
799  }
800  # else if plugin version is older than version in database
801  elseif (version_compare($Attribs["Version"],
802  $Plugin->InstalledVersion()) == -1)
803  {
804  # return error message about version conflict
805  return "Plugin <b>"
806  .$PluginName."</b> is older (<i>"
807  .addslashes($Attribs["Version"])
808  ."</i>) than previously-installed version (<i>"
809  .addslashes($Plugin->InstalledVersion())
810  ."</i>).";
811  }
812  }
813 
814  # report result to caller
815  return $InstallOrUpgradePerformed;
816  }
817 
825  private function SetPluginDefaultConfigValues($Plugin, $Overwrite = FALSE)
826  {
827  # if plugin has configuration info
828  $Attribs = $Plugin->GetAttributes();
829  if (isset($Attribs["CfgSetup"]))
830  {
831  foreach ($Attribs["CfgSetup"] as $CfgValName => $CfgSetup)
832  {
833  if (isset($CfgSetup["Default"]) && ($Overwrite
834  || ($Plugin->ConfigSetting($CfgValName) === NULL)))
835  {
836  if (isset(self::$CfgValueLoader))
837  {
838  $Plugin->ConfigSetting($CfgValName,
839  call_user_func(self::$CfgValueLoader,
840  $CfgSetup["Type"], $CfgSetup["Default"]));
841  }
842  else
843  {
844  $Plugin->ConfigSetting($CfgValName, $CfgSetup["Default"]);
845  }
846  }
847  }
848  }
849  }
850 
858  private function CheckDependencies($Plugins, $CheckReady = FALSE)
859  {
860  # look until all enabled plugins check out okay
861  $ErrMsgs = array();
862  do
863  {
864  # start out assuming all plugins are okay
865  $AllOkay = TRUE;
866 
867  # for each plugin
868  foreach ($Plugins as $PluginName => $Plugin)
869  {
870  # if plugin is enabled and not checking for ready
871  # or plugin is ready
872  if ($Plugin->IsEnabled() && (!$CheckReady || $Plugin->IsReady()))
873  {
874  # load plugin attributes
875  if (!isset($Attribs[$PluginName]))
876  {
877  $Attribs[$PluginName] = $Plugin->GetAttributes();
878  }
879 
880  # for each dependency for this plugin
881  foreach ($Attribs[$PluginName]["Requires"]
882  as $ReqName => $ReqVersion)
883  {
884  # handle PHP version requirements
885  if ($ReqName == "PHP")
886  {
887  if (version_compare($ReqVersion, phpversion(), ">"))
888  {
889  $ErrMsgs[$PluginName][] = "PHP version "
890  ."<i>".$ReqVersion."</i>"
891  ." required by <b>".$PluginName."</b>"
892  ." was not available. (Current PHP version"
893  ." is <i>".phpversion()."</i>.)";
894  }
895  }
896  # handle PHP extension requirements
897  elseif (preg_match("/^PHPX_/", $ReqName))
898  {
899  list($Dummy, $ExtensionName) = explode("_", $ReqName, 2);
900  if (!extension_loaded($ExtensionName))
901  {
902  $ErrMsgs[$PluginName][] = "PHP extension "
903  ."<i>".$ExtensionName."</i>"
904  ." required by <b>".$PluginName."</b>"
905  ." was not available.";
906  }
907  elseif (($ReqVersion !== TRUE)
908  && (phpversion($ExtensionName) !== FALSE)
909  && version_compare($ReqVersion,
910  phpversion($ExtensionName), ">"))
911  {
912  $ErrMsgs[$PluginName][] = "PHP extension "
913  ."<i>".$ExtensionName."</i>"
914  ." version <i>".$ReqVersion."</i>"
915  ." required by <b>".$PluginName."</b>"
916  ." was not available. (Current version"
917  ." of extension <i>".$ExtensionName."</i>"
918  ." is <i>".phpversion($ExtensionName)."</i>.)";
919  }
920  }
921  # handle dependencies on other plugins
922  else
923  {
924  # load plugin attributes if not already loaded
925  if (isset($Plugins[$ReqName])
926  && !isset($Attribs[$ReqName]))
927  {
928  $Attribs[$ReqName] =
929  $Plugins[$ReqName]->GetAttributes();
930  }
931 
932  # if target plugin is not present or is too old
933  # or is not enabled
934  # or (if appropriate) is not ready
935  if (!isset($Plugins[$ReqName])
936  || version_compare($ReqVersion,
937  $Attribs[$ReqName]["Version"], ">")
938  || !$Plugins[$ReqName]->IsEnabled()
939  || ($CheckReady
940  && !$Plugins[$ReqName]->IsReady()))
941  {
942  # add error message
943  $ErrMsgs[$PluginName][] = "Plugin <i>"
944  .$ReqName." ".$ReqVersion."</i>"
945  ." required by <b>".$PluginName."</b>"
946  ." was not available.";
947  }
948  }
949 
950  # if problem was found with plugin
951  if (isset($ErrMsgs[$PluginName]))
952  {
953  # remove plugin from our list
954  unset($Plugins[$PluginName]);
955 
956  # set flag to indicate a plugin had to be dropped
957  $AllOkay = FALSE;
958  }
959  }
960  }
961  }
962  } while ($AllOkay == FALSE);
963 
964  # return messages about any dropped plugins back to caller
965  return $ErrMsgs;
966  }
967 
976  private function SortPluginsByInitializationPrecedence($Plugins)
977  {
978  # load plugin attributes
979  foreach ($Plugins as $PluginName => $Plugin)
980  {
981  $PluginAttribs[$PluginName] = $Plugin->GetAttributes();
982  }
983 
984  # determine initialization order
985  $PluginsAfterUs = array();
986  foreach ($PluginAttribs as $PluginName => $Attribs)
987  {
988  foreach ($Attribs["InitializeBefore"] as $OtherPluginName)
989  {
990  $PluginsAfterUs[$PluginName][] = $OtherPluginName;
991  }
992  foreach ($Attribs["InitializeAfter"] as $OtherPluginName)
993  {
994  $PluginsAfterUs[$OtherPluginName][] = $PluginName;
995  }
996  }
997 
998  # infer other initialization order cues from lists of required plugins
999  foreach ($PluginAttribs as $PluginName => $Attribs)
1000  {
1001  # for each required plugin
1002  foreach ($Attribs["Requires"]
1003  as $RequiredPluginName => $RequiredPluginVersion)
1004  {
1005  # skip the requirement if it it not for another known plugin
1006  if (!isset($PluginAttribs[$RequiredPluginName]))
1007  {
1008  continue;
1009  }
1010 
1011  # if there is not a requirement in the opposite direction
1012  if (!array_key_exists($PluginName,
1013  $PluginAttribs[$RequiredPluginName]["Requires"]))
1014  {
1015  # if the required plugin is not scheduled to be after us
1016  if (!array_key_exists($PluginName, $PluginsAfterUs)
1017  || !in_array($RequiredPluginName,
1018  $PluginsAfterUs[$PluginName]))
1019  {
1020  # if we are not already scheduled to be after the required plugin
1021  if (!array_key_exists($PluginName, $PluginsAfterUs)
1022  || !in_array($RequiredPluginName,
1023  $PluginsAfterUs[$PluginName]))
1024  {
1025  # schedule us to be after the required plugin
1026  $PluginsAfterUs[$RequiredPluginName][] =
1027  $PluginName;
1028  }
1029  }
1030  }
1031  }
1032  }
1033 
1034  # keep track of those plugins we have yet to do and those that are done
1035  $UnsortedPlugins = array_keys($Plugins);
1036  $PluginsProcessed = array();
1037 
1038  # limit the number of iterations of the plugin ordering loop
1039  # to 10 times the number of plugins we have
1040  $MaxIterations = 10 * count($UnsortedPlugins);
1041  $IterationCount = 0;
1042 
1043  # iterate through all the plugins that need processing
1044  while (($NextPlugin = array_shift($UnsortedPlugins)) !== NULL)
1045  {
1046  # check to be sure that we're not looping forever
1047  $IterationCount++;
1048  if ($IterationCount > $MaxIterations)
1049  {
1050  throw new Exception(
1051  "Max iteration count exceeded trying to determine plugin"
1052  ." loading order. Is there a dependency loop?");
1053  }
1054 
1055  # if no plugins require this one, it can go last
1056  if (!isset($PluginsAfterUs[$NextPlugin]))
1057  {
1058  $PluginsProcessed[$NextPlugin] = $MaxIterations;
1059  }
1060  else
1061  {
1062  # for plugins that are required by others
1063  $Index = $MaxIterations;
1064  foreach ($PluginsAfterUs[$NextPlugin] as $GoBefore)
1065  {
1066  if (!isset($PluginsProcessed[$GoBefore]))
1067  {
1068  # if there is something that requires us which hasn't
1069  # yet been assigned an order, then we can't determine
1070  # our own place on this iteration
1071  array_push($UnsortedPlugins, $NextPlugin);
1072  continue 2;
1073  }
1074  else
1075  {
1076  # otherwise, make sure that we're loaded
1077  # before the earliest of the things that require us
1078  $Index = min($Index, $PluginsProcessed[$GoBefore] - 1);
1079  }
1080  }
1081  $PluginsProcessed[$NextPlugin] = $Index;
1082  }
1083  }
1084 
1085  # arrange plugins according to our ordering
1086  asort($PluginsProcessed, SORT_NUMERIC);
1087  $SortedPlugins = array();
1088  foreach ($PluginsProcessed as $PluginName => $SortOrder)
1089  {
1090  $SortedPlugins[$PluginName] = $Plugins[$PluginName];
1091  }
1092 
1093  # return sorted list to caller
1094  return $SortedPlugins;
1095  }
1096 
1105  public function FindPluginPhpFile($PageName)
1106  {
1107  # build list of possible locations for file
1108  $Locations = array(
1109  "local/plugins/%PLUGIN%/pages/%PAGE%.php",
1110  "plugins/%PLUGIN%/pages/%PAGE%.php",
1111  "local/plugins/%PLUGIN%/%PAGE%.php",
1112  "plugins/%PLUGIN%/%PAGE%.php",
1113  );
1114 
1115  # look for file and return (possibly) updated page to caller
1116  return $this->FindPluginPageFile($PageName, $Locations);
1117  }
1128  public function FindPluginHtmlFile($PageName)
1129  {
1130  # build list of possible locations for file
1131  $Locations = array(
1132  "local/plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
1133  "plugins/%PLUGIN%/interface/%ACTIVEUI%/%PAGE%.html",
1134  "local/plugins/%PLUGIN%/interface/%DEFAULTUI%/%PAGE%.html",
1135  "plugins/%PLUGIN%/interface/%DEFAULTUI%/%PAGE%.html",
1136  "local/plugins/%PLUGIN%/%PAGE%.html",
1137  "plugins/%PLUGIN%/%PAGE%.html",
1138  );
1139 
1140  # find HTML file
1141  $Params = $this->FindPluginPageFile($PageName, $Locations);
1142 
1143  # if plugin HTML file was found
1144  if ($Params["PageName"] != $PageName)
1145  {
1146  # add subdirectories for plugin to search paths
1147  $Dir = preg_replace("%^local/%", "", dirname($Params["PageName"]));
1148  $this->AF->AddImageDirectories(array(
1149  "local/".$Dir."/images",
1150  $Dir."/images",
1151  ));
1152  $this->AF->AddIncludeDirectories(array(
1153  "local/".$Dir."/include",
1154  $Dir."/include",
1155  ));
1156  $this->AF->AddFunctionDirectories(array(
1157  "local/".$Dir."/include",
1158  $Dir."/include",
1159  ));
1160  }
1161 
1162  # return possibly revised HTML file name to caller
1163  return $Params;
1164  }
1177  private function FindPluginPageFile($PageName, $Locations)
1178  {
1179  # set up return value assuming we will not find plugin page file
1180  $ReturnValue["PageName"] = $PageName;
1181 
1182  # look for plugin name and plugin page name in base page name
1183  preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);
1184 
1185  # if plugin name and plugin page name were found and plugin name is valid
1186  if (count($Matches) == 3)
1187  {
1188  # if plugin is valid and enabled and has its own subdirectory
1189  $PluginName = $Matches[1];
1190  if (isset($this->Plugins[$PluginName])
1191  && $this->PluginHasDir[$PluginName]
1192  && $this->Plugins[$PluginName]->IsEnabled())
1193  {
1194  # for each possible location
1195  $PageName = $Matches[2];
1196  $ActiveUI = $this->AF->ActiveUserInterface();
1197  $DefaultUI = $this->AF->DefaultUserInterface();
1198  foreach ($Locations as $Loc)
1199  {
1200  # make any needed substitutions into path
1201  $FileName = str_replace(
1202  array("%DEFAULTUI%", "%ACTIVEUI%", "%PLUGIN%", "%PAGE%"),
1203  array($DefaultUI, $ActiveUI, $PluginName, $PageName),
1204  $Loc);
1205 
1206  # if file exists in this location
1207  if (file_exists($FileName))
1208  {
1209  # set return value to contain full plugin page file name
1210  $ReturnValue["PageName"] = $FileName;
1211 
1212  # save plugin name as home of current page
1213  $this->PageFilePlugin = $PluginName;
1214 
1215  # set G_Plugin to plugin associated with current page
1216  $GLOBALS["G_Plugin"] = $this->GetPluginForCurrentPage();
1217 
1218  # stop looking
1219  break;
1220  }
1221  }
1222  }
1223  }
1224 
1225  # return array containing page name or page file name to caller
1226  return $ReturnValue;
1227  }
1228 }
1229 
1241 class PluginCaller
1242 {
1243 
1250  public function __construct($PluginName, $MethodName)
1251  {
1252  $this->PluginName = $PluginName;
1253  $this->MethodName = $MethodName;
1254  }
1255 
1261  public function CallPluginMethod()
1262  {
1263  $Args = func_get_args();
1264  $Plugin = self::$Manager->GetPlugin($this->PluginName);
1265  return call_user_func_array(array($Plugin, $this->MethodName), $Args);
1266  }
1267 
1272  public function GetCallbackAsText()
1273  {
1274  return $this->PluginName."::".$this->MethodName;
1275  }
1276 
1282  public function __sleep()
1283  {
1284  return array("PluginName", "MethodName");
1285  }
1286 
1288  static public $Manager;
1289 
1290  private $PluginName;
1291  private $MethodName;
1292 }
GetErrorMessages()
Retrieve any error messages generated during plugin loading.
Manager to load and invoke plugins.
GetPlugin($PluginName, $EvenIfNotReady=FALSE)
Retrieve specified plugin.
SQL database abstraction object with smart query caching.
Definition: Database.php:22
UninstallPlugin($PluginName)
Uninstall plugin and (optionally) delete any associated data.
GetDependents($PluginName)
Returns a list of plugins dependent on the specified plugin.
GetActivePluginList()
Get list of active (i.e.
GetPluginAttributes()
Retrieve info about currently loaded plugins.
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).
GetPlugins()
Retrieve all loaded plugins.
static SetApplicationFramework($AF)
Set the application framework to be referenced within plugins.
Definition: Plugin.php:447
static SetConfigValueLoader($Func)
Set function to load plugin configuration values from data.
LoadPlugins()
Load and initialize plugins.