CWIS Developer Documentation
PluginManager.php
Go to the documentation of this file.
1 <?PHP
2 
6 class PluginManager {
7 
8  # ---- PUBLIC INTERFACE --------------------------------------------------
9 
15  function __construct($AppFramework, $PluginDirectories)
16  {
17  # save framework and directory list for later use
18  $this->AF = $AppFramework;
19  $this->DirsToSearch = $PluginDirectories;
20 
21  # get our own database handle
22  $this->DB = new Database();
23 
24  # hook into events to load plugin PHP and HTML files
25  $this->AF->HookEvent("EVENT_PHP_FILE_LOAD", array($this, "FindPluginPhpFile"),
27  $this->AF->HookEvent("EVENT_HTML_FILE_LOAD", array($this, "FindPluginHtmlFile"),
29 
30  # tell PluginCaller helper object how to get to us
31  PluginCaller::$Manager = $this;
32  }
33 
38  function LoadPlugins()
39  {
40  # clear any existing errors
41  $this->ErrMsgs = array();
42 
43  # load list of all base plugin files
44  $this->FindPlugins($this->DirsToSearch);
45 
46  # for each plugin found
47  foreach ($this->PluginNames as $PluginName)
48  {
49  # bring in plugin class file
50  include_once($this->PluginFiles[$PluginName]);
51 
52  # if plugin class was defined by file
53  if (class_exists($PluginName))
54  {
55  # if plugin class is a valid descendant of base plugin class
56  $Plugin = new $PluginName;
57  if (is_subclass_of($Plugin, "Plugin"))
58  {
59  # set hooks needed for plugin to access plugin manager services
60  $Plugin->SetCfgSaveCallback(array(__CLASS__, "CfgSaveCallback"));
61 
62  # register the plugin
63  $this->Plugins[$PluginName] = $Plugin;
64  $this->PluginEnabled[$PluginName] = TRUE;
65  $this->Plugins[$PluginName]->Register();
66 
67  # check required plugin attributes
68  $Attribs[$PluginName] = $this->Plugins[$PluginName]->GetAttributes();
69  if (!strlen($Attribs[$PluginName]["Name"]))
70  {
71  $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
72  ." could not be loaded because it"
73  ." did not have a <i>Name</i> attribute set.";
74  unset($this->PluginEnabled[$PluginName]);
75  unset($this->Plugins[$PluginName]);
76  }
77  if (!strlen($Attribs[$PluginName]["Version"]))
78  {
79  $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
80  ." could not be loaded because it"
81  ." did not have a <i>Version</i> attribute set.";
82  unset($this->PluginEnabled[$PluginName]);
83  unset($this->Plugins[$PluginName]);
84  }
85  }
86  else
87  {
88  $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
89  ." could not be loaded because <i>".$PluginName."</i> was"
90  ." not a subclass of base <i>Plugin</i> class";
91  }
92  }
93  else
94  {
95  $this->ErrMsgs[$PluginName][] = "Expected class <i>".$PluginName
96  ."</i> not found in plugin file <i>"
97  .$this->PluginFiles[$PluginName]."</i>";
98  }
99  }
100 
101  # check plugin dependencies
102  $this->CheckDependencies();
103 
104  # load plugin configurations
105  $this->DB->Query("SELECT BaseName,Cfg FROM PluginInfo");
106  $Cfgs = $this->DB->FetchColumn("Cfg", "BaseName");
107 
108  foreach ($this->Plugins as $PluginName => $Plugin)
109  {
110  if ($this->PluginEnabled[$PluginName])
111  {
112  # add plugin directory (if it exists) to class autoloading list
113  if (array_key_exists($PluginName, $this->PluginDirs))
114  {
116  $this->PluginDirs[$PluginName]);
117  }
118 
119  # set configuration values if available
120  if (isset($Cfgs[$PluginName]))
121  {
122  $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
123  }
124 
125  # install or upgrade plugins if needed
126  $this->InstallPlugin($Plugin);
127  }
128  }
129 
130  # check plugin dependencies again in case an install or upgrade failed
131  $this->CheckDependencies();
132 
133  # initialize enabled plugins
134  foreach ($this->Plugins as $PluginName => $Plugin)
135  {
136  if ($this->PluginEnabled[$PluginName])
137  {
138  $ErrMsg = $Plugin->Initialize();
139  if ($ErrMsg !== NULL)
140  {
141  $this->ErrMsgs[$PluginName][] = "Initialization failed for"
142  ." plugin <b>".$PluginName."</b>: <i>".$ErrMsg."</i>";
143  $this->PluginEnabled[$PluginName] = FALSE;
144  }
145  }
146  }
147 
148  # register any events declared by enabled plugins
149  foreach ($this->Plugins as $PluginName => $Plugin)
150  {
151  if ($this->PluginEnabled[$PluginName])
152  {
153  $Events = $Plugin->DeclareEvents();
154  if (count($Events)) { $this->AF->RegisterEvent($Events); }
155  }
156  }
157 
158  # hook enabled plugins to events
159  foreach ($this->Plugins as $PluginName => $Plugin)
160  {
161  if ($this->PluginEnabled[$PluginName])
162  {
163  $EventsToHook = $Plugin->HookEvents();
164  if (count($EventsToHook))
165  {
166  foreach ($EventsToHook as $EventName => $PluginMethod)
167  {
168  if ($this->AF->IsStaticOnlyEvent($EventName))
169  {
170  $Caller = new PluginCaller($PluginName, $PluginMethod);
171  $Result = $this->AF->HookEvent(
172  $EventName, array($Caller, "CallPluginMethod"));
173  }
174  else
175  {
176  $Result = $this->AF->HookEvent(
177  $EventName, array($Plugin, $PluginMethod));
178  }
179  if ($Result === FALSE)
180  {
181  $this->ErrMsgs[$PluginName][] =
182  "Unable to hook requested event <i>"
183  .$EventName."</i> for plugin <b>".$PluginName."</b>";
184  }
185  }
186  }
187  }
188  }
189 
190  # limit plugin directory list to only active plugins
191  foreach ($this->PluginEnabled as $PluginName => $Enabled)
192  {
193  if (isset($this->PluginDirs[$PluginName]) && !$Enabled)
194  {
195  unset($this->PluginDirs[$PluginName]);
196  }
197  }
198 
199  # report to caller whether any problems were encountered
200  return count($this->ErrMsgs) ? FALSE : TRUE;
201  }
202 
207  function GetErrorMessages()
208  {
209  return $this->ErrMsgs;
210  }
211 
217  function GetPlugin($PluginName)
218  {
219  return isset($this->Plugins[$PluginName])
220  ? $this->Plugins[$PluginName] : NULL;
221  }
222 
231  {
232  return $this->GetPlugin($this->PageFilePlugin);
233  }
234 
240  {
241  $Info = array();
242  foreach ($this->Plugins as $PluginName => $Plugin)
243  {
244  $Info[$PluginName] = $Plugin->GetAttributes();
245  $Info[$PluginName]["Enabled"] =
246  isset($this->PluginInfo[$PluginName]["Enabled"])
247  ? $this->PluginInfo[$PluginName]["Enabled"] : FALSE;
248  $Info[$PluginName]["Installed"] =
249  isset($this->PluginInfo[$PluginName]["Installed"])
250  ? $this->PluginInfo[$PluginName]["Installed"] : FALSE;
251  }
252  return $Info;
253  }
254 
260  function GetDependents($PluginName)
261  {
262  $Dependents = array();
263  $AllAttribs = $this->GetPluginAttributes();
264  foreach ($AllAttribs as $Name => $Attribs)
265  {
266  if (array_key_exists($PluginName, $Attribs["Requires"]))
267  {
268  $Dependents[] = $Name;
269  $SubDependents = $this->GetDependents($Name);
270  $Dependents = array_merge($Dependents, $SubDependents);
271  }
272  }
273  return $Dependents;
274  }
275 
281  {
282  return array_keys($this->PluginEnabled, 1);
283  }
284 
291  function PluginEnabled($PluginName, $NewValue = NULL)
292  {
293  if ($NewValue !== NULL)
294  {
295  $this->DB->Query("UPDATE PluginInfo"
296  ." SET Enabled = ".($NewValue ? "1" : "0")
297  ." WHERE BaseName = '".addslashes($PluginName)."'");
298  $this->PluginEnabled[$PluginName] = $NewValue;
299  $this->PluginInfo[$PluginName]["Enabled"] = $NewValue;
300  }
301  return $this->PluginEnabled[$PluginName];
302  }
303 
309  function UninstallPlugin($PluginName)
310  {
311  # assume success
312  $Result = NULL;
313 
314  # if plugin is installed
315  if ($this->PluginInfo[$PluginName]["Installed"])
316  {
317  # call uninstall method for plugin
318  $Result = $this->Plugins[$PluginName]->Uninstall();
319 
320  # if plugin uninstall method succeeded
321  if ($Result === NULL)
322  {
323  # remove plugin info from database
324  $this->DB->Query("DELETE FROM PluginInfo"
325  ." WHERE BaseName = '".addslashes($PluginName)."'");
326 
327  # drop our data for the plugin
328  unset($this->Plugins[$PluginName]);
329  unset($this->PluginInfo[$PluginName]);
330  unset($this->PluginEnabled[$PluginName]);
331  unset($this->PluginNames[$PluginName]);
332  unset($this->PluginFiles[$PluginName]);
333  }
334  }
335 
336  # report results (if any) to caller
337  return $Result;
338  }
339 
340 
341  # ---- PRIVATE INTERFACE -------------------------------------------------
342 
343  private $Plugins = array();
344  private $PluginFiles = array();
345  private $PluginNames = array();
346  private $PluginDirs = array();
347  private $PluginInfo = array();
348  private $PluginEnabled = array();
349  private $PageFilePlugin = NULL;
350  private $AF;
351  private $DirsToSearch;
352  private $ErrMsgs = array();
353  private $DB;
354 
355  private function FindPlugins($DirsToSearch)
356  {
357  # for each directory
358  $PluginFiles = array();
359  foreach ($DirsToSearch as $Dir)
360  {
361  # if directory exists
362  if (is_dir($Dir))
363  {
364  # for each file in directory
365  $FileNames = scandir($Dir);
366  foreach ($FileNames as $FileName)
367  {
368  # if file looks like base plugin file
369  if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
370  {
371  # add file to list
372  $PluginName = preg_replace("/\.php$/", "", $FileName);
373  $this->PluginNames[$PluginName] = $PluginName;
374  $this->PluginFiles[$PluginName] = $Dir."/".$FileName;
375  }
376  # else if file looks like plugin directory
377  elseif (is_dir($Dir."/".$FileName)
378  && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
379  {
380  # if there is a base plugin file in the directory
381  $PossibleFile = $Dir."/".$FileName."/".$FileName.".php";
382  if (file_exists($PossibleFile))
383  {
384  # add plugin and directory to lists
385  $this->PluginNames[$FileName] = $FileName;
386  $this->PluginFiles[$FileName] = $PossibleFile;
387  $this->PluginDirs[$FileName] = $Dir."/".$FileName;
388  }
389  else
390  {
391  $this->ErrMsgs[$FileName][] =
392  "Expected plugin file <i>".$FileName.".php</i> not"
393  ." found in plugin subdirectory <i>"
394  .$Dir."/".$FileName."</i>";
395  }
396  }
397  }
398  }
399  }
400 
401  # return list of base plugin files to caller
402  return $PluginFiles;
403  }
404 
405  private function InstallPlugin($Plugin)
406  {
407  # cache all plugin info from database
408  if (count($this->PluginInfo) == 0)
409  {
410  $this->DB->Query("SELECT * FROM PluginInfo");
411  while ($Row = $this->DB->FetchRow())
412  {
413  $this->PluginInfo[$Row["BaseName"]] = $Row;
414  }
415  }
416 
417  # if plugin was not found in database
418  $PluginName = get_class($Plugin);
419  $Attribs = $Plugin->GetAttributes();
420  if (!isset($this->PluginInfo[$PluginName]))
421  {
422  # add plugin to database
423  $this->DB->Query("INSERT INTO PluginInfo"
424  ." (BaseName, Version, Installed, Enabled)"
425  ." VALUES ('".addslashes($PluginName)."', "
426  ." '".addslashes($Attribs["Version"])."', "
427  ."0, "
428  ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")");
429 
430  # read plugin settings back in
431  $this->DB->Query("SELECT * FROM PluginInfo"
432  ." WHERE BaseName = '".addslashes($PluginName)."'");
433  $this->PluginInfo[$PluginName] = $this->DB->FetchRow();
434  }
435 
436  # store plugin settings for later use
437  $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"];
438 
439  # if plugin is enabled
440  if ($this->PluginEnabled[$PluginName])
441  {
442  # if plugin has not been installed
443  if (!$this->PluginInfo[$PluginName]["Installed"])
444  {
445  # install plugin
446  $ErrMsg = $Plugin->Install();
447 
448  # if install succeeded
449  if ($ErrMsg == NULL)
450  {
451  # mark plugin as installed
452  $this->DB->Query("UPDATE PluginInfo SET Installed = 1"
453  ." WHERE BaseName = '".addslashes($PluginName)."'");
454  $this->PluginInfo[$PluginName]["Installed"] = 1;
455  }
456  else
457  {
458  # disable plugin
459  $this->PluginEnabled[$PluginName] = FALSE;
460 
461  # record error message about installation failure
462  $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>"
463  .$PluginName."</b> failed: <i>".$ErrMsg."</i>";;
464  }
465  }
466  else
467  {
468  # if plugin version is newer than version in database
469  if (version_compare($Attribs["Version"],
470  $this->PluginInfo[$PluginName]["Version"]) == 1)
471  {
472  # upgrade plugin
473  $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName]["Version"]);
474 
475  # if upgrade succeeded
476  if ($ErrMsg == NULL)
477  {
478  # update plugin version in database
479  $Attribs = $Plugin->GetAttributes();
480  $this->DB->Query("UPDATE PluginInfo"
481  ." SET Version = '".addslashes($Attribs["Version"])."'"
482  ." WHERE BaseName = '".addslashes($PluginName)."'");
483  $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"];
484  }
485  else
486  {
487  # disable plugin
488  $this->PluginEnabled[$PluginName] = FALSE;
489 
490  # record error message about upgrade failure
491  $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>"
492  .$PluginName."</b> from version <i>"
493  .addslashes($this->PluginInfo[$PluginName]["Version"])
494  ."</i> to version <i>"
495  .addslashes($Attribs["Version"])."</i> failed: <i>"
496  .$ErrMsg."</i>";
497  }
498  }
499  # else if plugin version is older than version in database
500  elseif (version_compare($Attribs["Version"],
501  $this->PluginInfo[$PluginName]["Version"]) == -1)
502  {
503  # disable plugin
504  $this->PluginEnabled[$PluginName] = FALSE;
505 
506  # record error message about version conflict
507  $this->ErrMsgs[$PluginName][] = "Plugin <b>"
508  .$PluginName."</b> is older (<i>"
509  .addslashes($Attribs["Version"])
510  ."</i>) than previously-installed version (<i>"
511  .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>).";
512  }
513  }
514  }
515  }
516 
517  private function CheckDependencies()
518  {
519  # look until all enabled plugins check out okay
520  do
521  {
522  # start out assuming all plugins are okay
523  $AllOkay = TRUE;
524 
525  # for each plugin
526  foreach ($this->Plugins as $PluginName => $Plugin)
527  {
528  # if plugin is currently enabled
529  if ($this->PluginEnabled[$PluginName])
530  {
531  # load plugin attributes
532  if (!isset($Attribs[$PluginName]))
533  {
534  $Attribs[$PluginName] =
535  $this->Plugins[$PluginName]->GetAttributes();
536  }
537 
538  # for each dependency for this plugin
539  foreach ($Attribs[$PluginName]["Requires"]
540  as $ReqName => $ReqVersion)
541  {
542  # handle PHP version requirements
543  if ($ReqName == "PHP")
544  {
545  if (version_compare($ReqVersion, phpversion(), ">"))
546  {
547  $this->ErrMsgs[$PluginName][] = "PHP version "
548  ."<i>".$ReqVersion."</i>"
549  ." required by <b>".$PluginName."</b>"
550  ." was not available. (Current PHP version"
551  ." is <i>".phpversion()."</i>.)";
552  }
553  }
554  # handle PHP extension requirements
555  elseif (preg_match("/^PHPX_/", $ReqName))
556  {
557  list($Dummy, $ExtensionName) = split("_", $ReqName, 2);
558  if (!extension_loaded($ExtensionName))
559  {
560  $this->ErrMsgs[$PluginName][] = "PHP extension "
561  ."<i>".$ExtensionName."</i>"
562  ." required by <b>".$PluginName."</b>"
563  ." was not available.";
564  }
565  }
566  # handle dependencies on other plugins
567  else
568  {
569  # load plugin attributes if not already loaded
570  if (isset($this->Plugins[$ReqName])
571  && !isset($Attribs[$ReqName]))
572  {
573  $Attribs[$ReqName] =
574  $this->Plugins[$ReqName]->GetAttributes();
575  }
576 
577  # if target plugin is not present or is disabled or is too old
578  if (!isset($this->PluginEnabled[$ReqName])
579  || !$this->PluginEnabled[$ReqName]
580  || (version_compare($ReqVersion,
581  $Attribs[$ReqName]["Version"], ">")))
582  {
583  # add error message and disable plugin
584  $this->ErrMsgs[$PluginName][] = "Plugin <i>"
585  .$ReqName." ".$ReqVersion."</i>"
586  ." required by <b>".$PluginName."</b>"
587  ." was not available.";
588  $this->PluginEnabled[$PluginName] = FALSE;
589 
590  # set flag indicating plugin did not check out
591  $AllOkay = FALSE;
592  }
593  }
594  }
595  }
596  }
597  } while ($AllOkay == FALSE);
598  }
599 
601  function FindPluginPhpFile($PageName)
602  {
603  return $this->FindPluginPageFile($PageName, "php");
604  }
608  function FindPluginHtmlFile($PageName)
609  {
610  return $this->FindPluginPageFile($PageName, "html");
611  }
614  private function FindPluginPageFile($PageName, $Suffix)
615  {
616  # set up return value assuming we will not find plugin page file
617  $ReturnValue["PageName"] = $PageName;
618 
619  # look for plugin name and plugin page name in base page name
620  preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);
621 
622  # if base page name contained name of existing plugin with its own subdirectory
623  if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]]))
624  {
625  # if PHP file with specified name exists in plugin subdirectory
626  $PageFile = $this->PluginDirs[$Matches[1]]."/".$Matches[2].".".$Suffix;
627  if (file_exists($PageFile))
628  {
629  # set return value to contain full plugin PHP file name
630  $ReturnValue["PageName"] = $PageFile;
631 
632  # save plugin name as home of current page
633  $this->PageFilePlugin = $Matches[1];
634  }
635  }
636 
637  # return array containing page name or page file name to caller
638  return $ReturnValue;
639  }
640 
642  static function CfgSaveCallback($BaseName, $Cfg)
643  {
644  $DB = new Database();
645  $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
646  ."' WHERE BaseName = '".addslashes($BaseName)."'");
647  }
649 }
650 
662 class PluginCaller {
663 
664  function __construct($PluginName, $MethodName)
665  {
666  $this->PluginName = $PluginName;
667  $this->MethodName = $MethodName;
668  }
669 
670  function CallPluginMethod()
671  {
672  $Args = func_get_args();
673  $Plugin = self::$Manager->GetPlugin($this->PluginName);
674  return call_user_func_array(array($Plugin, $this->MethodName), $Args);
675  }
676 
677  function GetCallbackAsText()
678  {
679  return $this->PluginName."::".$this->MethodName;
680  }
681 
682  function __sleep()
683  {
684  return array("PluginName", "MethodName");
685  }
686 
687  static public $Manager;
688 
689  private $PluginName;
690  private $MethodName;
691 }
694 ?>