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 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 # cache all plugin info from database 00324 if (count($this->PluginInfo) == 0) 00325 { 00326 $this->DB->Query("SELECT * FROM PluginInfo"); 00327 while ($Row = $this->DB->FetchRow()) 00328 { 00329 $this->PluginInfo[$Row["BaseName"]] = $Row; 00330 } 00331 } 00332 00333 # if plugin was not found in database 00334 $PluginName = get_class($Plugin); 00335 $Attribs = $Plugin->GetAttributes(); 00336 if (!isset($this->PluginInfo[$PluginName])) 00337 { 00338 # add plugin to database 00339 $this->DB->Query("INSERT INTO PluginInfo" 00340 ." (BaseName, Version, Installed, Enabled)" 00341 ." VALUES ('".addslashes($PluginName)."', " 00342 ." '".addslashes($Attribs["Version"])."', " 00343 ."0, " 00344 ." ".($Attribs["EnabledByDefault"] ? 1 : 0).")"); 00345 00346 # read plugin settings back in 00347 $this->DB->Query("SELECT * FROM PluginInfo" 00348 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00349 $this->PluginInfo[$PluginName] = $this->DB->FetchRow(); 00350 } 00351 00352 # store plugin settings for later use 00353 $this->PluginEnabled[$PluginName] = $this->PluginInfo[$PluginName]["Enabled"]; 00354 00355 # if plugin is enabled 00356 if ($this->PluginEnabled[$PluginName]) 00357 { 00358 # if plugin has not been installed 00359 if (!$this->PluginInfo[$PluginName]["Installed"]) 00360 { 00361 # install plugin 00362 $ErrMsg = $Plugin->Install(); 00363 00364 # if install succeeded 00365 if ($ErrMsg == NULL) 00366 { 00367 # mark plugin as installed 00368 $this->DB->Query("UPDATE PluginInfo SET Installed = 1" 00369 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00370 $this->PluginInfo[$PluginName]["Installed"] = 1; 00371 } 00372 else 00373 { 00374 # disable plugin 00375 $this->PluginEnabled[$PluginName] = FALSE; 00376 00377 # record error message about installation failure 00378 $this->ErrMsgs[$PluginName][] = "Installation of plugin <b>" 00379 .$PluginName."</b> failed: <i>".$ErrMsg."</i>";; 00380 } 00381 } 00382 else 00383 { 00384 # if plugin version is newer than version in database 00385 if (version_compare($Attribs["Version"], 00386 $this->PluginInfo[$PluginName]["Version"]) == 1) 00387 { 00388 # upgrade plugin 00389 $ErrMsg = $Plugin->Upgrade($this->PluginInfo[$PluginName]["Version"]); 00390 00391 # if upgrade succeeded 00392 if ($ErrMsg == NULL) 00393 { 00394 # update plugin version in database 00395 $Attribs = $Plugin->GetAttributes(); 00396 $this->DB->Query("UPDATE PluginInfo" 00397 ." SET Version = '".addslashes($Attribs["Version"])."'" 00398 ." WHERE BaseName = '".addslashes($PluginName)."'"); 00399 $this->PluginInfo[$PluginName]["Version"] = $Attribs["Version"]; 00400 } 00401 else 00402 { 00403 # disable plugin 00404 $this->PluginEnabled[$PluginName] = FALSE; 00405 00406 # record error message about upgrade failure 00407 $this->ErrMsgs[$PluginName][] = "Upgrade of plugin <b>" 00408 .$PluginName."</b> from version <i>" 00409 .addslashes($this->PluginInfo[$PluginName]["Version"]) 00410 ."</i> to version <i>" 00411 .addslashes($Attribs["Version"])."</i> failed: <i>" 00412 .$ErrMsg."</i>"; 00413 } 00414 } 00415 # else if plugin version is older than version in database 00416 elseif (version_compare($Attribs["Version"], 00417 $this->PluginInfo[$PluginName]["Version"]) == -1) 00418 { 00419 # disable plugin 00420 $this->PluginEnabled[$PluginName] = FALSE; 00421 00422 # record error message about version conflict 00423 $this->ErrMsgs[$PluginName][] = "Plugin <b>" 00424 .$PluginName."</b> is older (<i>" 00425 .addslashes($Attribs["Version"]) 00426 ."</i>) than previously-installed version (<i>" 00427 .addslashes($this->PluginInfo[$PluginName]["Version"])."</i>)."; 00428 } 00429 } 00430 } 00431 } 00432 00433 private function CheckDependencies() 00434 { 00435 # look until all enabled plugins check out okay 00436 do 00437 { 00438 # start out assuming all plugins are okay 00439 $AllOkay = TRUE; 00440 00441 # for each plugin 00442 foreach ($this->Plugins as $PluginName => $Plugin) 00443 { 00444 # if plugin is currently enabled 00445 if ($this->PluginEnabled[$PluginName]) 00446 { 00447 # load plugin attributes 00448 if (!isset($Attribs[$PluginName])) 00449 { 00450 $Attribs[$PluginName] = 00451 $this->Plugins[$PluginName]->GetAttributes(); 00452 } 00453 00454 # for each dependency for this plugin 00455 foreach ($Attribs[$PluginName]["Requires"] 00456 as $ReqName => $ReqVersion) 00457 { 00458 # handle PHP version requirements 00459 if ($ReqName == "PHP") 00460 { 00461 if (version_compare($ReqVersion, phpversion(), ">")) 00462 { 00463 $this->ErrMsgs[$PluginName][] = "PHP version " 00464 ."<i>".$ReqVersion."</i>" 00465 ." required by <b>".$PluginName."</b>" 00466 ." was not available. (Current PHP version" 00467 ." is <i>".phpversion()."</i>.)"; 00468 } 00469 } 00470 # handle PHP extension requirements 00471 elseif (preg_match("/^PHPX_/", $ReqName)) 00472 { 00473 list($Dummy, $ExtensionName) = split("_", $ReqName, 2); 00474 if (!extension_loaded($ExtensionName)) 00475 { 00476 $this->ErrMsgs[$PluginName][] = "PHP extension " 00477 ."<i>".$ExtensionName."</i>" 00478 ." required by <b>".$PluginName."</b>" 00479 ." was not available."; 00480 } 00481 } 00482 # handle dependencies on other plugins 00483 else 00484 { 00485 # load plugin attributes if not already loaded 00486 if (isset($this->Plugins[$ReqName]) 00487 && !isset($Attribs[$ReqName])) 00488 { 00489 $Attribs[$ReqName] = 00490 $this->Plugins[$ReqName]->GetAttributes(); 00491 } 00492 00493 # if target plugin is not present or is disabled or is too old 00494 if (!isset($this->PluginEnabled[$ReqName]) 00495 || !$this->PluginEnabled[$ReqName] 00496 || (version_compare($ReqVersion, 00497 $Attribs[$ReqName]["Version"], ">"))) 00498 { 00499 # add error message and disable plugin 00500 $this->ErrMsgs[$PluginName][] = "Plugin <i>" 00501 .$ReqName." ".$ReqVersion."</i>" 00502 ." required by <b>".$PluginName."</b>" 00503 ." was not available."; 00504 $this->PluginEnabled[$PluginName] = FALSE; 00505 00506 # set flag indicating plugin did not check out 00507 $AllOkay = FALSE; 00508 } 00509 } 00510 } 00511 } 00512 } 00513 } while ($AllOkay == FALSE); 00514 } 00515 00517 function FindPluginPhpFile($PageName) 00518 { 00519 return $this->FindPluginPageFile($PageName, "php"); 00520 } 00524 function FindPluginHtmlFile($PageName) 00525 { 00526 return $this->FindPluginPageFile($PageName, "html"); 00527 } 00530 private function FindPluginPageFile($PageName, $Suffix) 00531 { 00532 # set up return value assuming we will not find plugin page file 00533 $ReturnValue["PageName"] = $PageName; 00534 00535 # look for plugin name and plugin page name in base page name 00536 preg_match("/P_([A-Za-z].[A-Za-z0-9]*)_([A-Za-z0-9_-]+)/", $PageName, $Matches); 00537 00538 # if base page name contained name of existing plugin with its own subdirectory 00539 if ((count($Matches) == 3) && isset($this->PluginDirs[$Matches[1]])) 00540 { 00541 # if PHP file with specified name exists in plugin subdirectory 00542 $PageFile = $this->PluginDirs[$Matches[1]]."/".$Matches[2].".".$Suffix; 00543 if (file_exists($PageFile)) 00544 { 00545 # set return value to contain full plugin PHP file name 00546 $ReturnValue["PageName"] = $PageFile; 00547 } 00548 } 00549 00550 # return array containing page name or page file name to caller 00551 return $ReturnValue; 00552 } 00553 00555 static function CfgSaveCallback($BaseName, $Cfg) 00556 { 00557 $DB = new Database(); 00558 $DB->Query("UPDATE PluginInfo SET Cfg = '".addslashes(serialize($Cfg)) 00559 ."' WHERE BaseName = '".addslashes($BaseName)."'"); 00560 } 00562 } 00563 00575 class PluginCaller { 00576 00577 function __construct($PluginName, $MethodName) 00578 { 00579 $this->PluginName = $PluginName; 00580 $this->MethodName = $MethodName; 00581 } 00582 00583 function CallPluginMethod() 00584 { 00585 $Args = func_get_args(); 00586 $Plugin = self::$Manager->GetPlugin($this->PluginName); 00587 return call_user_func_array(array($Plugin, $this->MethodName), $Args); 00588 } 00589 00590 function GetCallbackAsText() 00591 { 00592 return $this->PluginName."::".$this->MethodName; 00593 } 00594 00595 function __sleep() 00596 { 00597 return array("PluginName", "MethodName"); 00598 } 00599 00600 static public $Manager; 00601 00602 private $PluginName; 00603 private $MethodName; 00604 } 00607 ?>