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