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 # 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 ?>