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