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 # install or upgrade plugins if needed
00102 foreach ($this->Plugins as $PluginName => $Plugin)
00103 {
00104 if ($this->PluginEnabled[$PluginName])
00105 {
00106 $this->InstallPlugin($Plugin);
00107 }
00108 }
00109
00110 # check plugin dependencies
00111 $this->CheckDependencies();
00112
00113 # load plugin configurations
00114 $this->DB->Query("SELECT BaseName,Cfg FROM PluginInfo");
00115 $Cfgs = $this->DB->FetchColumn("Cfg", "BaseName");
00116 foreach ($this->Plugins as $PluginName => $Plugin)
00117 {
00118 if ($this->PluginEnabled[$PluginName])
00119 {
00120 if (isset($Cfgs[$PluginName]))
00121 {
00122 $Plugin->SetAllCfg(unserialize($Cfgs[$PluginName]));
00123 }
00124 }
00125 }
00126
00127 # initialize each plugin
00128 foreach ($this->Plugins as $PluginName => $Plugin)
00129 {
00130 if ($this->PluginEnabled[$PluginName])
00131 {
00132 $ErrMsg = $Plugin->Initialize();
00133 if ($ErrMsg !== NULL)
00134 {
00135 $this->ErrMsgs[$PluginName][] = "Initialization failed for"
00136 ." plugin <b>".$PluginName."</b>: <i>".$ErrMsg."</i>";
00137 $this->PluginEnabled[$PluginName] = FALSE;
00138 }
00139 }
00140 }
00141
00142 # register any events declared by each plugin
00143 foreach ($this->Plugins as $PluginName => $Plugin)
00144 {
00145 if ($this->PluginEnabled[$PluginName])
00146 {
00147 $Events = $Plugin->DeclareEvents();
00148 if (count($Events)) { $this->AF->RegisterEvent($Events); }
00149 }
00150 }
00151
00152 # hook plugins to events
00153 foreach ($this->Plugins as $PluginName => $Plugin)
00154 {
00155 if ($this->PluginEnabled[$PluginName])
00156 {
00157 $EventsToHook = $Plugin->HookEvents();
00158 foreach ($EventsToHook as $EventName => $PluginMethod)
00159 {
00160 if ($this->AF->IsStaticOnlyEvent($EventName))
00161 {
00162 $Caller = new PluginCaller($PluginName, $PluginMethod);
00163 $Result = $this->AF->HookEvent(
00164 $EventName, array($Caller, "CallPluginMethod"));
00165 }
00166 else
00167 {
00168 $Result = $this->AF->HookEvent(
00169 $EventName, array($Plugin, $PluginMethod));
00170 }
00171 if ($Result === FALSE)
00172 {
00173 $this->ErrMsgs[$PluginName][] =
00174 "Unable to hook requested event <i>"
00175 .$EventName."</i> for plugin <b>".$PluginName."</b>";
00176 }
00177 }
00178 }
00179 }
00180
00181 # limit plugin directory list to only active plugins
00182 foreach ($this->PluginEnabled as $PluginName => $Enabled)
00183 {
00184 if (isset($this->PluginDirs[$PluginName]) && !$Enabled)
00185 {
00186 unset($this->PluginDirs[$PluginName]);
00187 }
00188 }
00189
00190 # add plugin directories to list to be searched for object files
00191 $ObjDirs = array();
00192 foreach ($this->PluginDirs as $Dir)
00193 {
00194 $ObjDirs[$Dir] = "";
00195 }
00196 $this->AF->AddObjectDirectories($ObjDirs);
00197
00198 # report to caller whether any problems were encountered
00199 return count($this->ErrMsgs) ? FALSE : TRUE;
00200 }
00201
00206 function GetErrorMessages()
00207 {
00208 return $this->ErrMsgs;
00209 }
00210
00216 function GetPlugin($PluginName)
00217 {
00218 return isset($this->Plugins[$PluginName])
00219 ? $this->Plugins[$PluginName] : NULL;
00220 }
00221
00226 function GetPluginAttributes()
00227 {
00228 $Info = array();
00229 foreach ($this->Plugins as $PluginName => $Plugin)
00230 {
00231 $Info[$PluginName] = $Plugin->GetAttributes();
00232 $Info[$PluginName]["Enabled"] =
00233 $this->PluginInfo[$PluginName]["Enabled"];
00234 }
00235 return $Info;
00236 }
00237
00244 function PluginEnabled($PluginName, $NewValue = NULL)
00245 {
00246 if ($NewValue !== NULL)
00247 {
00248 $this->DB->Query("UPDATE PluginInfo"
00249 ." SET Enabled = ".($NewValue ? "1" : "0")
00250 ." WHERE BaseName = '".addslashes($PluginName)."'");
00251 $this->PluginEnabled[$PluginName] = $NewValue;
00252 $this->PluginInfo[$PluginName]["Enabled"] = $NewValue;
00253 }
00254 return $this->PluginEnabled[$PluginName];
00255 }
00256
00257
00258 # ---- PRIVATE INTERFACE -------------------------------------------------
00259
00260 private $Plugins = array();
00261 private $PluginFiles = array();
00262 private $PluginNames = array();
00263 private $PluginDirs = array();
00264 private $PluginInfo = array();
00265 private $PluginEnabled = array();
00266 private $AF;
00267 private $DirsToSearch;
00268 private $ErrMsgs = array();
00269 private $DB;
00270
00271 private function FindPlugins($DirsToSearch)
00272 {
00273 # for each directory
00274 $PluginFiles = array();
00275 foreach ($DirsToSearch as $Dir)
00276 {
00277 # if directory exists
00278 if (is_dir($Dir))
00279 {
00280 # for each file in directory
00281 $FileNames = scandir($Dir);
00282 foreach ($FileNames as $FileName)
00283 {
00284 # if file looks like base plugin file
00285 if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName))
00286 {
00287 # add file to list
00288 $PluginName = preg_replace("/\.php$/", "", $FileName);
00289 $this->PluginNames[$PluginName] = $PluginName;
00290 $this->PluginFiles[$PluginName] = $Dir."/".$FileName;
00291 }
00292 # else if file looks like plugin directory
00293 elseif (is_dir($Dir."/".$FileName)
00294 && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName))
00295 {
00296 # if there is a base plugin file in the directory
00297 $PossibleFile = $Dir."/".$FileName."/".$FileName.".php";
00298 if (file_exists($PossibleFile))
00299 {
00300 # add plugin and directory to lists
00301 $this->PluginNames[$FileName] = $FileName;
00302 $this->PluginFiles[$FileName] = $PossibleFile;
00303 $this->PluginDirs[$FileName] = $Dir."/".$FileName;
00304 }
00305 else
00306 {
00307 $this->ErrMsgs[$FileName][] =
00308 "Expected plugin file <i>".$FileName.".php</i> not"
00309 ." found in plugin subdirectory <i>"
00310 .$Dir."/".$FileName."</i>";
00311 }
00312 }
00313 }
00314 }
00315 }
00316
00317 # return list of base plugin files to caller
00318 return $PluginFiles;
00319 }
00320
00321 private function InstallPlugin($Plugin)
00322 {
00323 # retrieve info about plugin from database
00324 $PluginName = get_class($Plugin);
00325 $this->DB->Query("SELECT * FROM PluginInfo"
00326 ." WHERE BaseName = '".addslashes($PluginName)."'");
00327
00328 # if plugin was not found in database
00329 $Attribs = $Plugin->GetAttributes();
00330 if ($this->DB->NumRowsSelected() == 0)
00331 {
00332 # add plugin to database
00333 $this->DB->Query("INSERT INTO PluginInfo"
00334 ." (BaseName, Version, Installed, Enabled)"
00335 ." VALUES ('".addslashes($PluginName)."', "
00336 ." '".addslashes($Attribs["Version"])."', "
00337 ."0, "
00338 ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")");
00339
00340 # read plugin settings back in
00341 $this->DB->Query("SELECT * FROM PluginInfo"
00342 ." WHERE BaseName = '".addslashes($PluginName)."'");
00343 }
00344
00345 # store plugin settings for later use
00346 $this->PluginInfo[$PluginName] = $this->DB->FetchRow();
00347 $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"];
00348
00349 # if plugin is enabled
00350 if ($this->PluginEnabled[$PluginName])
00351 {
00352 # if plugin has not been installed
00353 if (!$this->PluginInfo[$PluginName]["Installed"])
00354 {
00355 # install plugin
00356 $ErrMsg = $Plugin->Install();
00357
00358 # if install succeeded
00359 if ($ErrMsg == NULL)
00360 {
00361 # mark plugin as installed
00362 $this->DB->Query("UPDATE PluginInfo SET Installed = 1"
00363 ." WHERE BaseName = '".addslashes($PluginName)."'");
00364 }
00365 else
00366 {
00367 # disable plugin
00368 $this->PluginEnabled[$PluginName] = FALSE;
00369
00370 # record error message about installation failure
00371 $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>"
00372 .$PluginName."</b> failed: <i>".$ErrMsg."</i>";;
00373 }
00374 }
00375 else
00376 {
00377 # if plugin version is newer than version in database
00378 if (version_compare($Attribs["Version"],
00379 $this->PluginInfo[$PluginName]["Version"]) == 1)
00380 {
00381 # upgrade plugin
00382 $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName]["Version"]);
00383
00384 # if upgrade succeeded
00385 if ($ErrMsg == NULL)
00386 {
00387 # update plugin version in database
00388 $Attribs = $Plugin->GetAttributes();
00389 $this->DB->Query("UPDATE PluginInfo"
00390 ." SET Version = '".addslashes($Attribs["Version"])."'"
00391 ." WHERE BaseName = '".addslashes($PluginName)."'");
00392 $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"];
00393 }
00394 else
00395 {
00396 # disable plugin
00397 $this->PluginEnabled[$PluginName] = FALSE;
00398
00399 # record error message about upgrade failure
00400 $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>"
00401 .$PluginName."</b> from version <i>"
00402 .addslashes($this->PluginInfo[$PluginName]["Version"])
00403 ."</i> to version <i>"
00404 .addslashes($Attribs["Version"])."</i> failed: <i>"
00405 .$ErrMsg."</i>";
00406 }
00407 }
00408 # else if plugin version is older than version in database
00409 elseif (version_compare($Attribs["Version"],
00410 $this->PluginInfo[$PluginName]["Version"]) == -1)
00411 {
00412 # disable plugin
00413 $this->PluginEnabled[$PluginName] = FALSE;
00414
00415 # record error message about version conflict
00416 $this->ErrMsgs[$PluginName][] = "Plugin <b>"
00417 .$PluginName."</b> is older (<i>"
00418 .addslashes($Attribs["Version"])
00419 ."</i>) than previously-installed version (<i>"
00420 .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>).";
00421 }
00422 }
00423 }
00424 }
00425
00426 private function CheckDependencies()
00427 {
00428 # look until all enabled plugins check out okay
00429 do
00430 {
00431 # start out assuming all plugins are okay
00432 $AllOkay = TRUE;
00433
00434 # for each plugin
00435 foreach ($this->Plugins as $PluginName => $Plugin)
00436 {
00437 # if plugin is currently enabled
00438 if ($this->PluginEnabled[$PluginName])
00439 {
00440 # load plugin attributes
00441 if (!isset($Attribs[$PluginName]))
00442 {
00443 $Attribs[$PluginName] =
00444 $this->Plugins[$PluginName]->GetAttributes();
00445 }
00446
00447 # for each dependency for this plugin
00448 foreach ($Attribs[$PluginName]["Requires"]
00449 as $ReqName => $ReqVersion)
00450 {
00451 # handle PHP version requirements
00452 if ($ReqName == "PHP")
00453 {
00454 if (version_compare($ReqVersion, phpversion(), ">"))
00455 {
00456 $this->ErrMsgs[$PluginName][] = "PHP version "
00457 ."<i>".$ReqVersion."</i>"
00458 ." required by <b>".$PluginName."</b>"
00459 ." was not available. (Current PHP version"
00460 ." is <i>".phpversion()."</i>.)";
00461 }
00462 }
00463 # handle PHP extension requirements
00464 elseif (preg_match("/^PHPX_/", $ReqName))
00465 {
00466 list($Dummy, $ExtensionName) = split("_", $ReqName, 2);
00467 if (!extension_loaded($ExtensionName))
00468 {
00469 $this->ErrMsgs[$PluginName][] = "PHP extension "
00470 ."<i>".$ExtensionName."</i>"
00471 ." required by <b>".$PluginName."</b>"
00472 ." was not available.";
00473 }
00474 }
00475 # handle dependencies on other plugins
00476 else
00477 {
00478 # load plugin attributes if not already loaded
00479 if (isset($this->Plugins[$ReqName])
00480 && !isset($Attribs[$ReqName]))
00481 {
00482 $Attribs[$ReqName] =
00483 $this->Plugins[$ReqName]->GetAttributes();
00484 }
00485
00486 # if target plugin is not present or is disabled or is too old
00487 if (!isset($this->PluginEnabled[$ReqName])
00488 || !$this->PluginEnabled[$ReqName]
00489 || (version_compare($ReqVersion,
00490 $Attribs[$ReqName]["Version"], ">")))
00491 {
00492 # add error message and disable plugin
00493 $this->ErrMsgs[$PluginName][] = "Plugin <i>"
00494 .$ReqName." ".$ReqVersion."</i>"
00495 ." required by <b>".$PluginName."</b>"
00496 ." was not available.";
00497 $this->PluginEnabled[$PluginName] = FALSE;
00498
00499 # set flag indicating plugin did not check out
00500 $AllOkay = FALSE;
00501 }
00502 }
00503 }
00504 }
00505 }
00506 } while ($AllOkay == FALSE);
00507 }
00508
00510 function FindPluginPhpFile($PageName)
00511 {
00512 return $this->FindPluginPageFile($PageName, "php");
00513 }
00517 function FindPluginHtmlFile($PageName)
00518 {
00519 return $this->FindPluginPageFile($PageName, "html");
00520 }
00523 private function FindPluginPageFile($PageName, $Suffix)
00524 {
00525 # set up return value assuming we will not find plugin page file
00526 $ReturnValue["PageName"] = $PageName;
00527
00528 # look for plugin name and plugin page name in base page name
00529 preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches);
00530
00531 # if base page name contained name of existing plugin with its own subdirectory
00532 if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]]))
00533 {
00534 # if PHP file with specified name exists in plugin subdirectory
00535 $PageFile = $this->PluginDirs[$Matches[1]]."/".$Matches[2].".".$Suffix;
00536 if (file_exists($PageFile))
00537 {
00538 # set return value to contain full plugin PHP file name
00539 $ReturnValue["PageName"] = $PageFile;
00540 }
00541 }
00542
00543 # return array containing page name or page file name to caller
00544 return $ReturnValue;
00545 }
00546
00548 static function CfgSaveCallback($BaseName, $Cfg)
00549 {
00550 $DB = new Database();
00551 $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg))
00552 ."' WHERE BaseName = '".addslashes($BaseName)."'");
00553 }
00555 }
00556
00568 class PluginCaller {
00569
00570 function __construct($PluginName, $MethodName)
00571 {
00572 $this->PluginName = $PluginName;
00573 $this->MethodName = $MethodName;
00574 }
00575
00576 function CallPluginMethod()
00577 {
00578 $Args = func_get_args();
00579 $Plugin = self::$Manager->GetPlugin($this->PluginName);
00580 return call_user_func_array(array($Plugin, $this->MethodName), $Args);
00581 }
00582
00583 function GetCallbackAsText()
00584 {
00585 return $this->PluginName."::".$this->MethodName;
00586 }
00587
00588 function __sleep()
00589 {
00590 return array("PluginName", "MethodName");
00591 }
00592
00593 static public $Manager;
00594
00595 private $PluginName;
00596 private $MethodName;
00597 }
00600 ?>