00001 <?PHP
00002
00006 class PluginManager {
00007
00008 # ---- PUBLIC INTERFACE --------------------------------------------------
00009
00015 function __construct($AppFramework, $PluginDirectories)
00016 {
00017 # save framework and directory list for later use
00018 $this->AF = $AppFramework;
00019 $this->DirsToSearch = $PluginDirectories;
00020
00021 # get our own database handle
00022 $this->DB = new Database();
00023
00024 # hook into events to load plugin PHP and HTML files
00025 $this->AF->HookEvent("EVENT_PHP_FILE_LOAD", array($this, "FindPluginPhpFile"),
00026 ApplicationFramework::ORDER_LAST);
00027 $this->AF->HookEvent("EVENT_HTML_FILE_LOAD", array($this, "FindPluginHtmlFile"),
00028 ApplicationFramework::ORDER_LAST);
00029
00030 # tell PluginCaller helper object how to get to us
00031 PluginCaller::$Manager = $this;
00032 }
00033
00038 function LoadPlugins()
00039 {
00040 # clear any existing errors
00041 $this->ErrMsgs = array();
00042
00043 # load list of all base plugin files
00044 $this->FindPlugins($this->DirsToSearch);
00045
00046 # for each plugin found
00047 foreach ($this->PluginNames as $PluginName)
00048 {
00049 # bring in plugin class file
00050 include_once($this->PluginFiles[$PluginName]);
00051
00052 # if plugin class was defined by file
00053 if (class_exists($PluginName))
00054 {
00055 # if plugin class is a valid descendant of base plugin class
00056 $Plugin = new $PluginName;
00057 if (is_subclass_of($Plugin, "Plugin"))
00058 {
00059 # set hooks needed for plugin to access plugin manager services
00060 $Plugin->SetCfgSaveCallback(array(__CLASS__, "CfgSaveCallback"));
00061
00062 # register the plugin
00063 $this->Plugins[$PluginName] = $Plugin;
00064 $this->PluginEnabled[$PluginName] = TRUE;
00065 $this->Plugins[$PluginName]->Register();
00066
00067 # check required plugin attributes
00068 $Attribs[$PluginName] = $this->Plugins[$PluginName]->GetAttributes();
00069 if (!strlen($Attribs[$PluginName]["Name"]))
00070 {
00071 $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
00072 ." could not be loaded because it"
00073 ." did not have a <i>Name</i> attribute set.";
00074 unset($this->PluginEnabled[$PluginName]);
00075 unset($this->Plugins[$PluginName]);
00076 }
00077 if (!strlen($Attribs[$PluginName]["Version"]))
00078 {
00079 $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
00080 ." could not be loaded because it"
00081 ." did not have a <i>Version</i> attribute set.";
00082 unset($this->PluginEnabled[$PluginName]);
00083 unset($this->Plugins[$PluginName]);
00084 }
00085 }
00086 else
00087 {
00088 $this->ErrMsgs[$PluginName][] = "Plugin <b>".$PluginName."</b>"
00089 ." could not be loaded because <i>".$PluginName."</i> was"
00090 ." not a subclass of base <i>Plugin</i> class";
00091 }
00092 }
00093 else
00094 {
00095 $this->ErrMsgs[$PluginName][] = "Expected class <i>".$PluginName
00096 ."</i> not found in plugin file <i>"
00097 .$this->PluginFiles[$PluginName]."</i>";
00098 }
00099 }
00100
00101 # check plugin dependencies
00102 $this->CheckDependencies();
00103
00104 # load plugin configurations
00105 $this->DB->Query("SELECT BaseName,Cfg FROM PluginInfo");
00106 $Cfgs = $this->DB->FetchColumn("Cfg", "BaseName");
00107
00108 foreach ($this->Plugins as $PluginName => $Plugin)
00109 {
00110 if ($this->PluginEnabled[$PluginName])
00111 {
00112 # set configuration values if available
00113 if (isset($Cfgs[$PluginName]))
00114 {
00115 $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
00116 }
00117
00118 # install or upgrade plugins if needed
00119 $this->InstallPlugin($Plugin);
00120 }
00121 }
00122
00123 # initialize each plugin
00124 foreach ($this->Plugins as $PluginName => $Plugin)
00125 {
00126 if ($this->PluginEnabled[$PluginName])
00127 {
00128 $ErrMsg = $Plugin->Initialize();
00129 if ($ErrMsg !== NULL)
00130 {
00131 $this->ErrMsgs[$PluginName][] = "Initialization failed for"
00132 ." plugin <b>".$PluginName."</b>: <i>".$ErrMsg."</i>";
00133 $this->PluginEnabled[$PluginName] = FALSE;
00134 }
00135 }
00136 }
00137
00138 # register any events declared by each plugin
00139 foreach ($this->Plugins as $PluginName => $Plugin)
00140 {
00141 if ($this->PluginEnabled[$PluginName])
00142 {
00143 $Events = $Plugin->DeclareEvents();
00144 if (count($Events)) { $this->AF->RegisterEvent($Events); }
00145 }
00146 }
00147
00148 # hook plugins to events
00149 foreach ($this->Plugins as $PluginName => $Plugin)
00150 {
00151 if ($this->PluginEnabled[$PluginName])
00152 {
00153 $EventsToHook = $Plugin->HookEvents();
00154 if (count($EventsToHook))
00155 {
00156 foreach ($EventsToHook as $EventName => $PluginMethod)
00157 {
00158 if ($this->AF->IsStaticOnlyEvent($EventName))
00159 {
00160 $Caller = new PluginCaller($PluginName, $PluginMethod);
00161 $Result = $this->AF->HookEvent(
00162 $EventName, array($Caller, "CallPluginMethod"));
00163 }
00164 else
00165 {
00166 $Result = $this->AF->HookEvent(
00167 $EventName, array($Plugin, $PluginMethod));
00168 }
00169 if ($Result === FALSE)
00170 {
00171 $this->ErrMsgs[$PluginName][] =
00172 "Unable to hook requested event <i>"
00173 .$EventName."</i> for plugin <b>".$PluginName."</b>";
00174 }
00175 }
00176 }
00177 }
00178 }
00179
00180 # limit plugin directory list to only active plugins
00181 foreach ($this->PluginEnabled as $PluginName => $Enabled)
00182 {
00183 if (isset($this->PluginDirs[$PluginName]) && !$Enabled)
00184 {
00185 unset($this->PluginDirs[$PluginName]);
00186 }
00187 }
00188
00189 # add plugin directories to list to be searched for object files
00190 $ObjDirs = array();
00191 foreach ($this->PluginDirs as $Dir)
00192 {
00193 $ObjDirs[$Dir] = "";
00194 }
00195 $this->AF->AddObjectDirectories($ObjDirs);
00196
00197 # report to caller whether any problems were encountered
00198 return count($this->ErrMsgs) ? FALSE : TRUE;
00199 }
00200
00205 function GetErrorMessages()
00206 {
00207 return $this->ErrMsgs;
00208 }
00209
00215 function GetPlugin($PluginName)
00216 {
00217 return isset($this->Plugins[$PluginName])
00218 ? $this->Plugins[$PluginName] : NULL;
00219 }
00220
00228 function GetPluginForCurrentPage()
00229 {
00230 return $this->GetPlugin($this->PageFilePlugin);
00231 }
00232
00237 function GetPluginAttributes()
00238 {
00239 $Info = array();
00240 foreach ($this->Plugins as $PluginName => $Plugin)
00241 {
00242 $Info[$PluginName] = $Plugin->GetAttributes();
00243 $Info[$PluginName]["Enabled"] =
00244 isset($this->PluginInfo[$PluginName]["Enabled"])
00245 ? $this->PluginInfo[$PluginName]["Enabled"] : FALSE;
00246 }
00247 return $Info;
00248 }
00249
00254 public function GetActivePluginList()
00255 {
00256 return array_keys($this->PluginEnabled, 1);
00257 }
00258
00265 function PluginEnabled($PluginName, $NewValue = NULL)
00266 {
00267 if ($NewValue !== NULL)
00268 {
00269 $this->DB->Query("UPDATE PluginInfo"
00270 ." SET Enabled = ".($NewValue ? "1" : "0")
00271 ." WHERE BaseName = '".addslashes($PluginName)."'");
00272 $this->PluginEnabled[$PluginName] = $NewValue;
00273 $this->PluginInfo[$PluginName]["Enabled"] = $NewValue;
00274 }
00275 return $this->PluginEnabled[$PluginName];
00276 }
00277
00278
00279 # ---- PRIVATE INTERFACE -------------------------------------------------
00280
00281 private $Plugins = array();
00282 private $PluginFiles = array();
00283 private $PluginNames = array();
00284 private $PluginDirs = array();
00285 private $PluginInfo = array();
00286 private $PluginEnabled = array();
00287 private $PageFilePlugin = NULL;
00288 private $AF;
00289 private $DirsToSearch;
00290 private $ErrMsgs = array();
00291 private $DB;
00292
00293 private function FindPlugins($DirsToSearch)
00294 {
00295 # for each directory
00296 $PluginFiles = array();
00297 foreach ($DirsToSearch as $Dir)
00298 {
00299 # if directory exists
00300 if (is_dir($Dir))
00301 {
00302 # for each file in directory
00303 $FileNames = scandir($Dir);
00304 foreach ($FileNames as $FileName)
00305 {
00306 # if file looks like base plugin file
00307 if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
00308 {
00309 # add file to list
00310 $PluginName = preg_replace("/\.php$/", "", $FileName);
00311 $this->PluginNames[$PluginName] = $PluginName;
00312 $this->PluginFiles[$PluginName] = $Dir."/".$FileName;
00313 }
00314 # else if file looks like plugin directory
00315 elseif (is_dir($Dir."/".$FileName)
00316 && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
00317 {
00318 # if there is a base plugin file in the directory
00319 $PossibleFile = $Dir."/".$FileName."/".$FileName.".php";
00320 if (file_exists($PossibleFile))
00321 {
00322 # add plugin and directory to lists
00323 $this->PluginNames[$FileName] = $FileName;
00324 $this->PluginFiles[$FileName] = $PossibleFile;
00325 $this->PluginDirs[$FileName] = $Dir."/".$FileName;
00326 }
00327 else
00328 {
00329 $this->ErrMsgs[$FileName][] =
00330 "Expected plugin file <i>".$FileName.".php</i> not"
00331 ." found in plugin subdirectory <i>"
00332 .$Dir."/".$FileName."</i>";
00333 }
00334 }
00335 }
00336 }
00337 }
00338
00339 # return list of base plugin files to caller
00340 return $PluginFiles;
00341 }
00342
00343 private function InstallPlugin($Plugin)
00344 {
00345 # cache all plugin info from database
00346 if (count($this->PluginInfo) == 0)
00347 {
00348 $this->DB->Query("SELECT * FROM PluginInfo");
00349 while ($Row = $this->DB->FetchRow())
00350 {
00351 $this->PluginInfo[$Row["BaseName"]] = $Row;
00352 }
00353 }
00354
00355 # if plugin was not found in database
00356 $PluginName = get_class($Plugin);
00357 $Attribs = $Plugin->GetAttributes();
00358 if (!isset($this->PluginInfo[$PluginName]))
00359 {
00360 # add plugin to database
00361 $this->DB->Query("INSERT INTO PluginInfo"
00362 ." (BaseName, Version, Installed, Enabled)"
00363 ." VALUES ('".addslashes($PluginName)."', "
00364 ." '".addslashes($Attribs["Version"])."', "
00365 ."0, "
00366 ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")");
00367
00368 # read plugin settings back in
00369 $this->DB->Query("SELECT * FROM PluginInfo"
00370 ." WHERE BaseName = '".addslashes($PluginName)."'");
00371 $this->PluginInfo[$PluginName] = $this->DB->FetchRow();
00372 }
00373
00374 # store plugin settings for later use
00375 $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"];
00376
00377 # if plugin is enabled
00378 if ($this->PluginEnabled[$PluginName])
00379 {
00380 # if plugin has not been installed
00381 if (!$this->PluginInfo[$PluginName]["Installed"])
00382 {
00383 # install plugin
00384 $ErrMsg = $Plugin->Install();
00385
00386 # if install succeeded
00387 if ($ErrMsg == NULL)
00388 {
00389 # mark plugin as installed
00390 $this->DB->Query("UPDATE PluginInfo SET Installed = 1"
00391 ." WHERE BaseName = '".addslashes($PluginName)."'");
00392 $this->PluginInfo[$PluginName]["Installed"] = 1;
00393 }
00394 else
00395 {
00396 # disable plugin
00397 $this->PluginEnabled[$PluginName] = FALSE;
00398
00399 # record error message about installation failure
00400 $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>"
00401 .$PluginName."</b> failed: <i>".$ErrMsg."</i>";;
00402 }
00403 }
00404 else
00405 {
00406 # if plugin version is newer than version in database
00407 if (version_compare($Attribs["Version"],
00408 $this->PluginInfo[$PluginName]["Version"]) == 1)
00409 {
00410 # upgrade plugin
00411 $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName]["Version"]);
00412
00413 # if upgrade succeeded
00414 if ($ErrMsg == NULL)
00415 {
00416 # update plugin version in database
00417 $Attribs = $Plugin->GetAttributes();
00418 $this->DB->Query("UPDATE PluginInfo"
00419 ." SET Version = '".addslashes($Attribs["Version"])."'"
00420 ." WHERE BaseName = '".addslashes($PluginName)."'");
00421 $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"];
00422 }
00423 else
00424 {
00425 # disable plugin
00426 $this->PluginEnabled[$PluginName] = FALSE;
00427
00428 # record error message about upgrade failure
00429 $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>"
00430 .$PluginName."</b> from version <i>"
00431 .addslashes($this->PluginInfo[$PluginName]["Version"])
00432 ."</i> to version <i>"
00433 .addslashes($Attribs["Version"])."</i> failed: <i>"
00434 .$ErrMsg."</i>";
00435 }
00436 }
00437 # else if plugin version is older than version in database
00438 elseif (version_compare($Attribs["Version"],
00439 $this->PluginInfo[$PluginName]["Version"]) == -1)
00440 {
00441 # disable plugin
00442 $this->PluginEnabled[$PluginName] = FALSE;
00443
00444 # record error message about version conflict
00445 $this->ErrMsgs[$PluginName][] = "Plugin <b>"
00446 .$PluginName."</b> is older (<i>"
00447 .addslashes($Attribs["Version"])
00448 ."</i>) than previously-installed version (<i>"
00449 .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>).";
00450 }
00451 }
00452 }
00453 }
00454
00455 private function CheckDependencies()
00456 {
00457 # look until all enabled plugins check out okay
00458 do
00459 {
00460 # start out assuming all plugins are okay
00461 $AllOkay = TRUE;
00462
00463 # for each plugin
00464 foreach ($this->Plugins as $PluginName => $Plugin)
00465 {
00466 # if plugin is currently enabled
00467 if ($this->PluginEnabled[$PluginName])
00468 {
00469 # load plugin attributes
00470 if (!isset($Attribs[$PluginName]))
00471 {
00472 $Attribs[$PluginName] =
00473 $this->Plugins[$PluginName]->GetAttributes();
00474 }
00475
00476 # for each dependency for this plugin
00477 foreach ($Attribs[$PluginName]["Requires"]
00478 as $ReqName => $ReqVersion)
00479 {
00480 # handle PHP version requirements
00481 if ($ReqName == "PHP")
00482 {
00483 if (version_compare($ReqVersion, phpversion(), ">"))
00484 {
00485 $this->ErrMsgs[$PluginName][] = "PHP version "
00486 ."<i>".$ReqVersion."</i>"
00487 ." required by <b>".$PluginName."</b>"
00488 ." was not available. (Current PHP version"
00489 ." is <i>".phpversion()."</i>.)";
00490 }
00491 }
00492 # handle PHP extension requirements
00493 elseif (preg_match("/^PHPX_/", $ReqName))
00494 {
00495 list($Dummy, $ExtensionName) = split("_", $ReqName, 2);
00496 if (!extension_loaded($ExtensionName))
00497 {
00498 $this->ErrMsgs[$PluginName][] = "PHP extension "
00499 ."<i>".$ExtensionName."</i>"
00500 ." required by <b>".$PluginName."</b>"
00501 ." was not available.";
00502 }
00503 }
00504 # handle dependencies on other plugins
00505 else
00506 {
00507 # load plugin attributes if not already loaded
00508 if (isset($this->Plugins[$ReqName])
00509 && !isset($Attribs[$ReqName]))
00510 {
00511 $Attribs[$ReqName] =
00512 $this->Plugins[$ReqName]->GetAttributes();
00513 }
00514
00515 # if target plugin is not present or is disabled or is too old
00516 if (!isset($this->PluginEnabled[$ReqName])
00517 || !$this->PluginEnabled[$ReqName]
00518 || (version_compare($ReqVersion,
00519 $Attribs[$ReqName]["Version"], ">")))
00520 {
00521 # add error message and disable plugin
00522 $this->ErrMsgs[$PluginName][] = "Plugin <i>"
00523 .$ReqName." ".$ReqVersion."</i>"
00524 ." required by <b>".$PluginName."</b>"
00525 ." was not available.";
00526 $this->PluginEnabled[$PluginName] = FALSE;
00527
00528 # set flag indicating plugin did not check out
00529 $AllOkay = FALSE;
00530 }
00531 }
00532 }
00533 }
00534 }
00535 } while ($AllOkay == FALSE);
00536 }
00537
00539 function FindPluginPhpFile($PageName)
00540 {
00541 return $this->FindPluginPageFile($PageName, "php");
00542 }
00546 function FindPluginHtmlFile($PageName)
00547 {
00548 return $this->FindPluginPageFile($PageName, "html");
00549 }
00552 private function FindPluginPageFile($PageName, $Suffix)
00553 {
00554 # set up return value assuming we will not find plugin page file
00555 $ReturnValue["PageName"] = $PageName;
00556
00557 # look for plugin name and plugin page name in base page name
00558 preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);
00559
00560 # if base page name contained name of existing plugin with its own subdirectory
00561 if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]]))
00562 {
00563 # if PHP file with specified name exists in plugin subdirectory
00564 $PageFile = $this->PluginDirs[$Matches[1]]."/".$Matches[2].".".$Suffix;
00565 if (file_exists($PageFile))
00566 {
00567 # set return value to contain full plugin PHP file name
00568 $ReturnValue["PageName"] = $PageFile;
00569
00570 # save plugin name as home of current page
00571 $this->PageFilePlugin = $Matches[1];
00572 }
00573 }
00574
00575 # return array containing page name or page file name to caller
00576 return $ReturnValue;
00577 }
00578
00580 static function CfgSaveCallback($BaseName, $Cfg)
00581 {
00582 $DB = new Database();
00583 $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
00584 ."' WHERE BaseName = '".addslashes($BaseName)."'");
00585 }
00587 }
00588
00600 class PluginCaller {
00601
00602 function __construct($PluginName, $MethodName)
00603 {
00604 $this->PluginName = $PluginName;
00605 $this->MethodName = $MethodName;
00606 }
00607
00608 function CallPluginMethod()
00609 {
00610 $Args = func_get_args();
00611 $Plugin = self::$Manager->GetPlugin($this->PluginName);
00612 return call_user_func_array(array($Plugin, $this->MethodName), $Args);
00613 }
00614
00615 function GetCallbackAsText()
00616 {
00617 return $this->PluginName."::".$this->MethodName;
00618 }
00619
00620 function __sleep()
00621 {
00622 return array("PluginName", "MethodName");
00623 }
00624
00625 static public $Manager;
00626
00627 private $PluginName;
00628 private $MethodName;
00629 }
00632 ?>