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