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