8 # ---- PUBLIC INTERFACE --------------------------------------------------
17 # save framework and directory list for later use
18 $this->AF = $AppFramework;
19 $this->DirsToSearch = $PluginDirectories;
21 # get our own database handle
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"),
30 # tell PluginCaller helper object how to get to us
31 PluginCaller::$Manager = $this;
40 # clear any existing errors
41 $this->ErrMsgs = array();
43 # load list of all base plugin files
44 $this->FindPlugins($this->DirsToSearch);
46 # for each plugin found
47 foreach ($this->PluginNames as $PluginName)
49 # bring in plugin class file
50 include_once($this->PluginFiles[$PluginName]);
52 # if plugin class was defined by file
53 if (class_exists($PluginName))
55 # if plugin class is a valid descendant of base plugin class
56 $Plugin =
new $PluginName;
57 if (is_subclass_of($Plugin,
"Plugin"))
59 # set hooks needed for plugin to access plugin manager services
60 $Plugin->SetCfgSaveCallback(array(__CLASS__,
"CfgSaveCallback"));
63 $this->Plugins[$PluginName] = $Plugin;
65 $this->Plugins[$PluginName]->Register();
67 # check required plugin attributes
68 $Attribs[$PluginName] = $this->Plugins[$PluginName]->GetAttributes();
69 if (!strlen($Attribs[$PluginName][
"Name"]))
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.";
75 unset($this->Plugins[$PluginName]);
77 if (!strlen($Attribs[$PluginName][
"Version"]))
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.";
83 unset($this->Plugins[$PluginName]);
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";
95 $this->ErrMsgs[$PluginName][] =
"Expected class <i>".$PluginName
96 .
"</i> not found in plugin file <i>"
97 .$this->PluginFiles[$PluginName].
"</i>";
101 # check plugin dependencies
102 $this->CheckDependencies();
104 # load plugin configurations
105 $this->DB->Query(
"SELECT BaseName,Cfg FROM PluginInfo");
106 $Cfgs = $this->DB->FetchColumn(
"Cfg",
"BaseName");
108 foreach ($this->Plugins as $PluginName => $Plugin)
112 # add plugin directory (if it exists) to class autoloading list
113 if (array_key_exists($PluginName, $this->PluginDirs))
116 $this->PluginDirs[$PluginName]);
119 # set configuration values if available
120 if (isset($Cfgs[$PluginName]))
122 $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
125 # install or upgrade plugins if needed
126 $this->InstallPlugin($Plugin);
130 # check plugin dependencies again in case an install or upgrade failed
131 $this->CheckDependencies();
133 # initialize enabled plugins
134 foreach ($this->Plugins as $PluginName => $Plugin)
138 $ErrMsg = $Plugin->Initialize();
139 if ($ErrMsg !== NULL)
141 $this->ErrMsgs[$PluginName][] =
"Initialization failed for"
142 .
" plugin <b>".$PluginName.
"</b>: <i>".$ErrMsg.
"</i>";
148 # register any events declared by enabled plugins
149 foreach ($this->Plugins as $PluginName => $Plugin)
153 $Events = $Plugin->DeclareEvents();
154 if (count($Events)) { $this->AF->RegisterEvent($Events); }
158 # hook enabled plugins to events
159 foreach ($this->Plugins as $PluginName => $Plugin)
163 $EventsToHook = $Plugin->HookEvents();
164 if (count($EventsToHook))
166 foreach ($EventsToHook as $EventName => $PluginMethod)
168 if ($this->AF->IsStaticOnlyEvent($EventName))
170 $Caller =
new PluginCaller($PluginName, $PluginMethod);
171 $Result = $this->AF->HookEvent(
172 $EventName, array($Caller,
"CallPluginMethod"));
176 $Result = $this->AF->HookEvent(
177 $EventName, array($Plugin, $PluginMethod));
179 if ($Result === FALSE)
181 $this->ErrMsgs[$PluginName][] =
182 "Unable to hook requested event <i>"
183 .$EventName.
"</i> for plugin <b>".$PluginName.
"</b>";
190 # limit plugin directory list to only active plugins
193 if (isset($this->PluginDirs[$PluginName]) && !$Enabled)
195 unset($this->PluginDirs[$PluginName]);
199 # report to caller whether any problems were encountered
200 return count($this->ErrMsgs) ? FALSE : TRUE;
209 return $this->ErrMsgs;
219 return isset($this->Plugins[$PluginName])
220 ? $this->Plugins[$PluginName] : NULL;
232 return $this->
GetPlugin($this->PageFilePlugin);
242 foreach ($this->Plugins as $PluginName => $Plugin)
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;
262 $Dependents = array();
264 foreach ($AllAttribs as $Name => $Attribs)
266 if (array_key_exists($PluginName, $Attribs[
"Requires"]))
268 $Dependents[] = $Name;
270 $Dependents = array_merge($Dependents, $SubDependents);
293 if ($NewValue !== NULL)
295 $this->DB->Query(
"UPDATE PluginInfo"
296 .
" SET Enabled = ".($NewValue ?
"1" :
"0")
297 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
299 $this->PluginInfo[$PluginName][
"Enabled"] = $NewValue;
314 # if plugin is installed
315 if ($this->PluginInfo[$PluginName][
"Installed"])
317 # call uninstall method for plugin
318 $Result = $this->Plugins[$PluginName]->Uninstall();
320 # if plugin uninstall method succeeded
321 if ($Result === NULL)
323 # remove plugin info from database
324 $this->DB->Query(
"DELETE FROM PluginInfo"
325 .
" WHERE BaseName = '".addslashes($PluginName).
"'");
327 # drop our data for the plugin
328 unset($this->Plugins[$PluginName]);
329 unset($this->PluginInfo[$PluginName]);
331 unset($this->PluginNames[$PluginName]);
332 unset($this->PluginFiles[$PluginName]);
336 # report results (if any) to caller
341 # ---- PRIVATE INTERFACE -------------------------------------------------
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;
351 private $DirsToSearch;
352 private $ErrMsgs = array();
355 private function FindPlugins($DirsToSearch)
358 $PluginFiles = array();
359 foreach ($DirsToSearch as $Dir)
361 # if directory exists
364 # for each file in directory
365 $FileNames = scandir($Dir);
366 foreach ($FileNames as $FileName)
368 # if file looks like base plugin file
369 if (preg_match(
"/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
372 $PluginName = preg_replace(
"/\.php$/",
"", $FileName);
373 $this->PluginNames[$PluginName] = $PluginName;
374 $this->PluginFiles[$PluginName] = $Dir.
"/".$FileName;
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))
380 # if there is a base plugin file in the directory
381 $PossibleFile = $Dir.
"/".$FileName.
"/".$FileName.
".php";
382 if (file_exists($PossibleFile))
384 # add plugin and directory to lists
385 $this->PluginNames[$FileName] = $FileName;
386 $this->PluginFiles[$FileName] = $PossibleFile;
387 $this->PluginDirs[$FileName] = $Dir.
"/".$FileName;
391 $this->ErrMsgs[$FileName][] =
392 "Expected plugin file <i>".$FileName.
".php</i> not"
393 .
" found in plugin subdirectory <i>"
394 .$Dir.
"/".$FileName.
"</i>";
401 # return list of base plugin files to caller
405 private function InstallPlugin($Plugin)
407 # cache all plugin info from database
408 if (count($this->PluginInfo) == 0)
410 $this->DB->Query(
"SELECT * FROM PluginInfo");
411 while ($Row = $this->DB->FetchRow())
413 $this->PluginInfo[$Row[
"BaseName"]] = $Row;
417 # if plugin was not found in database
418 $PluginName = get_class($Plugin);
419 $Attribs = $Plugin->GetAttributes();
420 if (!isset($this->PluginInfo[$PluginName]))
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"]).
"', "
428 .
" ".($Attribs[
"EnabledByDefault"] ? 1 : 0).
")");
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();
436 # store plugin settings for later use
437 $this->
PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName][
"Enabled"];
439 # if plugin is enabled
442 # if plugin has not been installed
443 if (!$this->PluginInfo[$PluginName][
"Installed"])
446 $ErrMsg = $Plugin->Install();
448 # if install succeeded
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;
461 # record error message about installation failure
462 $this->ErrMsgs[$PluginName][] =
"Installation of plugin <b>"
463 .$PluginName.
"</b> failed: <i>".$ErrMsg.
"</i>";;
468 # if plugin version is newer than version in database
469 if (version_compare($Attribs[
"Version"],
470 $this->PluginInfo[$PluginName][
"Version"]) == 1)
473 $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName][
"Version"]);
475 # if upgrade succeeded
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"];
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>"
499 # else if plugin version is older than version in database
500 elseif (version_compare($Attribs[
"Version"],
501 $this->PluginInfo[$PluginName][
"Version"]) == -1)
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>).";
517 private function CheckDependencies()
519 # look until all enabled plugins check out okay
522 # start out assuming all plugins are okay
526 foreach ($this->Plugins as $PluginName => $Plugin)
528 # if plugin is currently enabled
531 # load plugin attributes
532 if (!isset($Attribs[$PluginName]))
534 $Attribs[$PluginName] =
535 $this->Plugins[$PluginName]->GetAttributes();
538 # for each dependency for this plugin
539 foreach ($Attribs[$PluginName][
"Requires"]
540 as $ReqName => $ReqVersion)
542 # handle PHP version requirements
543 if ($ReqName ==
"PHP")
545 if (version_compare($ReqVersion, phpversion(),
">"))
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>.)";
554 # handle PHP extension requirements
555 elseif (preg_match(
"/^PHPX_/", $ReqName))
557 list($Dummy, $ExtensionName) = split(
"_", $ReqName, 2);
558 if (!extension_loaded($ExtensionName))
560 $this->ErrMsgs[$PluginName][] =
"PHP extension "
561 .
"<i>".$ExtensionName.
"</i>"
562 .
" required by <b>".$PluginName.
"</b>"
563 .
" was not available.";
566 # handle dependencies on other plugins
569 # load plugin attributes if not already loaded
570 if (isset($this->Plugins[$ReqName])
571 && !isset($Attribs[$ReqName]))
574 $this->Plugins[$ReqName]->GetAttributes();
577 # if target plugin is not present or is disabled or is too old
580 || (version_compare($ReqVersion,
581 $Attribs[$ReqName][
"Version"],
">")))
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.";
590 # set flag indicating plugin did not check out
597 }
while ($AllOkay == FALSE);
601 function FindPluginPhpFile($PageName)
603 return $this->FindPluginPageFile($PageName,
"php");
608 function FindPluginHtmlFile($PageName)
610 return $this->FindPluginPageFile($PageName,
"html");
614 private function FindPluginPageFile($PageName, $Suffix)
616 # set up return value assuming we will not find plugin page file
617 $ReturnValue[
"PageName"] = $PageName;
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);
622 # if base page name contained name of existing plugin with its own subdirectory
623 if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]]))
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))
629 # set return value to contain full plugin PHP file name
630 $ReturnValue[
"PageName"] = $PageFile;
632 # save plugin name as home of current page
633 $this->PageFilePlugin = $Matches[1];
637 # return array containing page name or page file name to caller
642 static function CfgSaveCallback($BaseName, $Cfg)
645 $DB->Query(
"UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
646 .
"' WHERE BaseName = '".addslashes($BaseName).
"'");
664 function __construct($PluginName, $MethodName)
666 $this->PluginName = $PluginName;
667 $this->MethodName = $MethodName;
670 function CallPluginMethod()
672 $Args = func_get_args();
673 $Plugin = self::$Manager->GetPlugin($this->PluginName);
674 return call_user_func_array(array($Plugin, $this->MethodName), $Args);
677 function GetCallbackAsText()
679 return $this->PluginName.
"::".$this->MethodName;
684 return array(
"PluginName",
"MethodName");
687 static public $Manager;