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