CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

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"),
26 ApplicationFramework::ORDER_LAST);
27 $this->AF->HookEvent("EVENT_HTML_FILE_LOAD", array($this, "FindPluginHtmlFile"),
28 ApplicationFramework::ORDER_LAST);
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 {
115 ApplicationFramework::AddObjectDirectory(
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
230 function GetPluginForCurrentPage()
231 {
232 return $this->GetPlugin($this->PageFilePlugin);
233 }
234
239 function GetPluginAttributes()
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
280 function GetActivePluginList()
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 ?>