PluginManager.php
Go to the documentation of this file.
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 00225 function GetPluginAttributes() 00226 { 00227 $Info = array(); 00228 foreach ($this->Plugins as $PluginName => $Plugin) 00229 { 00230 $Info[$PluginName] = $Plugin->GetAttributes(); 00231 $Info[$PluginName]["Enabled"] = 00232 isset($this->PluginInfo[$PluginName]["Enabled"]) 00233 ? $this->PluginInfo[$PluginName]["Enabled"] : FALSE; 00234 } 00235 return $Info; 00236 } 00237 00242 public function GetActivePluginList() 00243 { 00244 return array_keys($this->PluginEnabled, 1); 00245 } 00246 00253 function PluginEnabled($PluginName, $NewValue = NULL) 00254 { 00255 if ($NewValue !== NULL) 00256 { 00257 $this->DB->Query("UPDATE PluginInfo" 00258 ." SET Enabled = ".($NewValue ? "1" : "0") 00259 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00260 $this->PluginEnabled[$PluginName] = $NewValue; 00261 $this->PluginInfo[$PluginName]["Enabled"] = $NewValue; 00262 } 00263 return $this->PluginEnabled[$PluginName]; 00264 } 00265 00266 00267 # ---- PRIVATE INTERFACE ------------------------------------------------- 00268 00269 private $Plugins = array(); 00270 private $PluginFiles = array(); 00271 private $PluginNames = array(); 00272 private $PluginDirs = array(); 00273 private $PluginInfo = array(); 00274 private $PluginEnabled = array(); 00275 private $AF; 00276 private $DirsToSearch; 00277 private $ErrMsgs = array(); 00278 private $DB; 00279 00280 private function FindPlugins($DirsToSearch) 00281 { 00282 # for each directory 00283 $PluginFiles = array(); 00284 foreach ($DirsToSearch as $Dir) 00285 { 00286 # if directory exists 00287 if (is_dir($Dir)) 00288 { 00289 # for each file in directory 00290 $FileNames = scandir($Dir); 00291 foreach ($FileNames as $FileName) 00292 { 00293 # if file looks like base plugin file 00294 if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*\.php$/", $FileName)) 00295 { 00296 # add file to list 00297 $PluginName = preg_replace("/\.php$/", "", $FileName); 00298 $this->PluginNames[$PluginName] = $PluginName; 00299 $this->PluginFiles[$PluginName] = $Dir."/".$FileName; 00300 } 00301 # else if file looks like plugin directory 00302 elseif (is_dir($Dir."/".$FileName) 00303 && preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*/", $FileName)) 00304 { 00305 # if there is a base plugin file in the directory 00306 $PossibleFile = $Dir."/".$FileName."/".$FileName.".php"; 00307 if (file_exists($PossibleFile)) 00308 { 00309 # add plugin and directory to lists 00310 $this->PluginNames[$FileName] = $FileName; 00311 $this->PluginFiles[$FileName] = $PossibleFile; 00312 $this->PluginDirs[$FileName] = $Dir."/".$FileName; 00313 } 00314 else 00315 { 00316 $this->ErrMsgs[$FileName][] = 00317 "Expected plugin file <i>".$FileName.".php</i> not" 00318 ." found in plugin subdirectory <i>" 00319 .$Dir."/".$FileName."</i>"; 00320 } 00321 } 00322 } 00323 } 00324 } 00325 00326 # return list of base plugin files to caller 00327 return $PluginFiles; 00328 } 00329 00330 private function InstallPlugin($Plugin) 00331 { 00332 # cache all plugin info from database 00333 if (count($this->PluginInfo) == 0) 00334 { 00335 $this->DB->Query("SELECT * FROM PluginInfo"); 00336 while ($Row = $this->DB->FetchRow()) 00337 { 00338 $this->PluginInfo[$Row["BaseName"]] = $Row; 00339 } 00340 } 00341 00342 # if plugin was not found in database 00343 $PluginName = get_class($Plugin); 00344 $Attribs = $Plugin->GetAttributes(); 00345 if (!isset($this->PluginInfo[$PluginName])) 00346 { 00347 # add plugin to database 00348 $this->DB->Query("INSERT INTO PluginInfo" 00349 ." (BaseName, Version, Installed, Enabled)" 00350 ." VALUES ('".addslashes($PluginName)."', " 00351 ." '".addslashes($Attribs["Version"])."', " 00352 ."0, " 00353 ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")"); 00354 00355 # read plugin settings back in 00356 $this->DB->Query("SELECT * FROM PluginInfo" 00357 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00358 $this->PluginInfo[$PluginName] = $this->DB->FetchRow(); 00359 } 00360 00361 # store plugin settings for later use 00362 $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"]; 00363 00364 # if plugin is enabled 00365 if ($this->PluginEnabled[$PluginName]) 00366 { 00367 # if plugin has not been installed 00368 if (!$this->PluginInfo[$PluginName]["Installed"]) 00369 { 00370 # install plugin 00371 $ErrMsg = $Plugin->Install(); 00372 00373 # if install succeeded 00374 if ($ErrMsg == NULL) 00375 { 00376 # mark plugin as installed 00377 $this->DB->Query("UPDATE PluginInfo SET Installed = 1" 00378 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00379 $this->PluginInfo[$PluginName]["Installed"] = 1; 00380 } 00381 else 00382 { 00383 # disable plugin 00384 $this->PluginEnabled[$PluginName] = FALSE; 00385 00386 # record error message about installation failure 00387 $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>" 00388 .$PluginName."</b> failed: <i>".$ErrMsg."</i>";; 00389 } 00390 } 00391 else 00392 { 00393 # if plugin version is newer than version in database 00394 if (version_compare($Attribs["Version"], 00395 $this->PluginInfo[$PluginName]["Version"]) == 1) 00396 { 00397 # upgrade plugin 00398 $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName]["Version"]); 00399 00400 # if upgrade succeeded 00401 if ($ErrMsg == NULL) 00402 { 00403 # update plugin version in database 00404 $Attribs = $Plugin->GetAttributes(); 00405 $this->DB->Query("UPDATE PluginInfo" 00406 ." SET Version = '".addslashes($Attribs["Version"])."'" 00407 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00408 $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"]; 00409 } 00410 else 00411 { 00412 # disable plugin 00413 $this->PluginEnabled[$PluginName] = FALSE; 00414 00415 # record error message about upgrade failure 00416 $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>" 00417 .$PluginName."</b> from version <i>" 00418 .addslashes($this->PluginInfo[$PluginName]["Version"]) 00419 ."</i> to version <i>" 00420 .addslashes($Attribs["Version"])."</i> failed: <i>" 00421 .$ErrMsg."</i>"; 00422 } 00423 } 00424 # else if plugin version is older than version in database 00425 elseif (version_compare($Attribs["Version"], 00426 $this->PluginInfo[$PluginName]["Version"]) == -1) 00427 { 00428 # disable plugin 00429 $this->PluginEnabled[$PluginName] = FALSE; 00430 00431 # record error message about version conflict 00432 $this->ErrMsgs[$PluginName][] = "Plugin <b>" 00433 .$PluginName."</b> is older (<i>" 00434 .addslashes($Attribs["Version"]) 00435 ."</i>) than previously-installed version (<i>" 00436 .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>)."; 00437 } 00438 } 00439 } 00440 } 00441 00442 private function CheckDependencies() 00443 { 00444 # look until all enabled plugins check out okay 00445 do 00446 { 00447 # start out assuming all plugins are okay 00448 $AllOkay = TRUE; 00449 00450 # for each plugin 00451 foreach ($this->Plugins as $PluginName => $Plugin) 00452 { 00453 # if plugin is currently enabled 00454 if ($this->PluginEnabled[$PluginName]) 00455 { 00456 # load plugin attributes 00457 if (!isset($Attribs[$PluginName])) 00458 { 00459 $Attribs[$PluginName] = 00460 $this->Plugins[$PluginName]->GetAttributes(); 00461 } 00462 00463 # for each dependency for this plugin 00464 foreach ($Attribs[$PluginName]["Requires"] 00465 as $ReqName => $ReqVersion) 00466 { 00467 # handle PHP version requirements 00468 if ($ReqName == "PHP") 00469 { 00470 if (version_compare($ReqVersion, phpversion(), ">")) 00471 { 00472 $this->ErrMsgs[$PluginName][] = "PHP version " 00473 ."<i>".$ReqVersion."</i>" 00474 ." required by <b>".$PluginName."</b>" 00475 ." was not available. (Current PHP version" 00476 ." is <i>".phpversion()."</i>.)"; 00477 } 00478 } 00479 # handle PHP extension requirements 00480 elseif (preg_match("/^PHPX_/", $ReqName)) 00481 { 00482 list($Dummy, $ExtensionName) = split("_", $ReqName, 2); 00483 if (!extension_loaded($ExtensionName)) 00484 { 00485 $this->ErrMsgs[$PluginName][] = "PHP extension " 00486 ."<i>".$ExtensionName."</i>" 00487 ." required by <b>".$PluginName."</b>" 00488 ." was not available."; 00489 } 00490 } 00491 # handle dependencies on other plugins 00492 else 00493 { 00494 # load plugin attributes if not already loaded 00495 if (isset($this->Plugins[$ReqName]) 00496 && !isset($Attribs[$ReqName])) 00497 { 00498 $Attribs[$ReqName] = 00499 $this->Plugins[$ReqName]->GetAttributes(); 00500 } 00501 00502 # if target plugin is not present or is disabled or is too old 00503 if (!isset($this->PluginEnabled[$ReqName]) 00504 || !$this->PluginEnabled[$ReqName] 00505 || (version_compare($ReqVersion, 00506 $Attribs[$ReqName]["Version"], ">"))) 00507 { 00508 # add error message and disable plugin 00509 $this->ErrMsgs[$PluginName][] = "Plugin <i>" 00510 .$ReqName." ".$ReqVersion."</i>" 00511 ." required by <b>".$PluginName."</b>" 00512 ." was not available."; 00513 $this->PluginEnabled[$PluginName] = FALSE; 00514 00515 # set flag indicating plugin did not check out 00516 $AllOkay = FALSE; 00517 } 00518 } 00519 } 00520 } 00521 } 00522 } while ($AllOkay == FALSE); 00523 } 00524 00526 function FindPluginPhpFile($PageName) 00527 { 00528 return $this->FindPluginPageFile($PageName, "php"); 00529 } 00533 function FindPluginHtmlFile($PageName) 00534 { 00535 return $this->FindPluginPageFile($PageName, "html"); 00536 } 00539 private function FindPluginPageFile($PageName, $Suffix) 00540 { 00541 # set up return value assuming we will not find plugin page file 00542 $ReturnValue["PageName"] = $PageName; 00543 00544 # look for plugin name and plugin page name in base page name 00545 preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches); 00546 00547 # if base page name contained name of existing plugin with its own subdirectory 00548 if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]])) 00549 { 00550 # if PHP file with specified name exists in plugin subdirectory 00551 $PageFile = $this->PluginDirs[$Matches[1]]."/".$Matches[2].".".$Suffix; 00552 if (file_exists($PageFile)) 00553 { 00554 # set return value to contain full plugin PHP file name 00555 $ReturnValue["PageName"] = $PageFile; 00556 } 00557 } 00558 00559 # return array containing page name or page file name to caller 00560 return $ReturnValue; 00561 } 00562 00564 static function CfgSaveCallback($BaseName, $Cfg) 00565 { 00566 $DB = new Database(); 00567 $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg)) 00568 ."' WHERE BaseName = '".addslashes($BaseName)."'"); 00569 } 00571 } 00572 00584 class PluginCaller { 00585 00586 function __construct($PluginName, $MethodName) 00587 { 00588 $this->PluginName = $PluginName; 00589 $this->MethodName = $MethodName; 00590 } 00591 00592 function CallPluginMethod() 00593 { 00594 $Args = func_get_args(); 00595 $Plugin = self::$Manager->GetPlugin($this->PluginName); 00596 return call_user_func_array(array($Plugin, $this->MethodName), $Args); 00597 } 00598 00599 function GetCallbackAsText() 00600 { 00601 return $this->PluginName."::".$this->MethodName; 00602 } 00603 00604 function __sleep() 00605 { 00606 return array("PluginName", "MethodName"); 00607 } 00608 00609 static public $Manager; 00610 00611 private $PluginName; 00612 private $MethodName; 00613 } 00616 ?>