b2evolution PHP Cross Reference Blogging Systems

Source: /inc/plugins/model/_plugins_admin.class.php - 1563 lines - 53060 bytes - Summary - Text - Print

Description: This file implements the {@link Plugins_admin} class, which gets used for administrative handling of the {@link Plugin Plugins}. This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}. See also {@link http://sourceforge.net/projects/evocms/}.

   1  <?php
   2  /**
   3   * This file implements the {@link Plugins_admin} class, which gets used for administrative
   4   * handling of the {@link Plugin Plugins}.
   5   *
   6   * This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}.
   7   * See also {@link http://sourceforge.net/projects/evocms/}.
   8   *
   9   * @copyright (c)2003-2014 by Francois Planque - {@link http://fplanque.com/}.
  10   * Parts of this file are copyright (c)2006 by Daniel HAHLER - {@link http://daniel.hahler.de/}.
  11   *
  12   * @license http://b2evolution.net/about/license.html GNU General Public License (GPL)
  13   *
  14   * {@internal Open Source relicensing agreement:
  15   * Daniel HAHLER grants Francois PLANQUE the right to license
  16   * Daniel HAHLER's contributions to this file and the b2evolution project
  17   * under any OSI approved OSS license (http://www.opensource.org/licenses/).
  18   * }}
  19   *
  20   * @package plugins
  21   *
  22   * @author blueyed: Daniel HAHLER
  23   *
  24   * @version $Id: _plugins_admin.class.php 6136 2014-03-08 07:59:48Z manuel $
  25   */
  26  if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  27  
  28  
  29  load_class( 'plugins/model/_plugins.class.php', 'Plugins' );
  30  
  31  
  32  /**
  33   * A Plugins object that loads all Plugins, not just the enabled ones.
  34   * This is needed for the backoffice plugin management.
  35   *
  36   * This extends the basic Plugins by adding all the functionnality needed for administering plugins
  37   * in addition to just using the already enabled plugins.
  38   *
  39   * @package plugins
  40   */
  41  class Plugins_admin extends Plugins
  42  {
  43      /**
  44       * Load all plugins (not just enabled ones).
  45       */
  46      var $sql_load_plugins_table = '
  47              SELECT plug_ID, plug_priority, plug_classname, plug_code, plug_name, plug_shortdesc, plug_status, plug_version, plug_spam_weight
  48                FROM T_plugins
  49               ORDER BY plug_priority, plug_classname';
  50  
  51  
  52      /**
  53       * Get the list of all events/hooks supported by the plugin framework.
  54       *
  55       * Also puts in additional events provided by plugins.
  56       * fp> please provide an example/use case
  57       *
  58       * Additional to the returned event methods (which can be disabled), there are internal
  59       * ones which just get called on the plugin (and get not remembered in T_pluginevents), e.g.:
  60       *  - AfterInstall
  61       *  - BeforeEnable
  62       *  - BeforeDisable
  63       *  - BeforeInstall
  64       *  - BeforeUninstall
  65       *  - BeforeUninstallPayload
  66       *  - DisplaySkin (called on a skin from {@link GetProvidedSkins()})
  67       *  - ExecCronJob
  68       *  - GetDefaultSettings
  69       *  - GetDefaultUserSettings
  70       *  - GetExtraEvents
  71       *  - GetHtsrvMethods
  72       *  - PluginInit
  73       *  - PluginSettingsUpdateAction (Called as action before updating the plugin's settings)
  74       *  - PluginSettingsEditAction (Called as action before editing the plugin's settings)
  75       *  - PluginSettingsEditDisplayAfter (Called after standard plugin settings are displayed for editing)
  76       *  - PluginSettingsValidateSet (Called before setting a plugin's setting in the backoffice)
  77       *  - PluginUserSettingsUpdateAction (Called as action before updating the plugin's user settings)
  78       *  - PluginUserSettingsEditDisplayAfter (Called after displaying normal user settings)
  79       *  - PluginUserSettingsValidateSet (Called before setting a plugin's user setting in the backoffice)
  80       *  - PluginVersionChanged (Called when we detect a version change)
  81       *  - PluginCollSettingsUpdateAction (Called as action before updating the collection/blog's settings)
  82       *
  83       *  The max length of event names is 40 chars (T_pluginevents.pevt_event).
  84       *
  85       * @internal When adding a new event, please make sure to add a description as well.
  86       *           Please also add a new well-documented method to the "Plugin" class.
  87       *
  88       * @return array Name of event (key) => description (value)
  89       */
  90  	function get_supported_events()
  91      {
  92          static $supported_events;
  93  
  94          if( empty( $supported_events ) )
  95          {
  96              $supported_events = array(
  97                  'AdminAfterPageFooter' => 'This gets called after the backoffice HTML footer has been displayed.',
  98                  'AdminDisplayEditorButton' => 'Display action buttons on the edit screens in the back-office',
  99                  'DisplayEditorButton' => 'Display action buttons on the edit screen in the front-office',
 100                  'AdminDisplayToolbar' => 'Display a toolbar on the edit screens',
 101                  'AdminDisplayCommentFormFieldset' => 'Display form fieldsets on the backoffice comment editing form',
 102                  'AdminDisplayItemFormFieldset' => 'Display form fieldsets on the backoffice item editing screen(s)',
 103                  'DisplayItemFormFieldset' => 'Display form fieldsets on the frontoffice item editing screen(s)',
 104                  'AdminEndHtmlHead' => 'This gets called at the end of the HTML HEAD section in backoffice skins',
 105                  'AdminAfterEvobarInit' => 'This gets called after the Evobar menu has been initialized.',
 106                  'AdminAfterMenuInit' => 'This gets called after the backoffice menu has been initialized.',
 107                  'AdminTabAction' => 'This gets called before AdminTabPayload when the Tools tab for the plugin is selected; no output allowed!',
 108                  'AdminTabPayload' => 'This gets called when the Tools tab for the plugin is selected and content should be displayed.',
 109                  'AdminToolAction' => '',
 110                  'AdminToolPayload' => 'This gets called when the plugin\'s block in the Tools menu should be displayed.',
 111  
 112                  'AdminBeforeItemEditCreate' => 'This gets called before a new item gets created from the backoffice.',
 113                  'AdminBeforeItemEditUpdate' => 'This gets called before an existing item gets updated from the backoffice.',
 114                  'AdminBeforeItemEditDelete' => 'This gets called before an existing item gets deleted from the backoffice.',
 115  
 116                  'AdminBeginPayload' => 'This gets called before the main payload in the backoffice is displayed.',
 117  
 118                  'CacheObjects' => 'Cache data objects.',
 119                  'CachePageContent' => 'Cache page content.',
 120                  'CacheIsCollectingContent' => 'Gets asked for if we are generating cached content.',
 121  
 122                  'AfterCommentDelete' => 'Gets called after a comment has been deleted from the database.',
 123                  'AfterCommentInsert' => 'Gets called after a comment has been inserted into the database.',
 124                  'AfterCommentUpdate' => 'Gets called after a comment has been updated in the database.',
 125  
 126                  'AfterCollectionDelete' => 'Gets called after a blog has been deleted from the database.',
 127                  'AfterCollectionInsert' => 'Gets called after a blog has been inserted into the database.',
 128                  'AfterCollectionUpdate' => 'Gets called after a blog has been updated in the database.',
 129  
 130                  'AfterObjectDelete' => 'Gets called after a data object has been deleted from the database.',
 131                  'AfterObjectInsert' => 'Gets called after a data object has been inserted into the database.',
 132                  'AfterObjectUpdate' => 'Gets called after a data object has been updated in the database.',
 133  
 134                  'GetCollectionKinds' => 'Defines blog kinds, their names and description.',
 135                  'InitCollectionKinds' => 'Defines blog settings by its kind.',
 136  
 137                  'AfterItemDelete' => 'This gets called after an item has been deleted from the database.',
 138                  'PrependItemInsertTransact' => 'This gets called before an item is inserted into the database.',
 139                  'AfterItemInsert' => 'This gets called after an item has been inserted into the database.',
 140                  'PrependItemUpdateTransact' => 'This gets called before an item gets updated in the database..',
 141                  'AfterItemUpdate' => 'This gets called after an item has been updated in the database.',
 142                  'AppendItemPreviewTransact' => 'This gets called when instantiating an item for preview.',
 143  
 144                   'FilterItemContents' => 'Filters the content of a post/item right after input.',
 145                   'UnfilterItemContents' => 'Unfilters the content of a post/item right before editing.',
 146  
 147                   // fp> rename to "PreRender"
 148                  'RenderItemAsHtml' => 'Renders content when generated as HTML.',
 149                  'RenderItemAsXml' => 'Renders content when generated as XML.',
 150                  'RenderItemAsText' => 'Renders content when generated as plain text.',
 151                  'RenderItemAttachment' => 'Renders item attachment.',
 152                  'RenderCommentAttachment' => 'Renders comment attachment.',
 153  
 154  
 155                  // fp> rename to "DispRender"
 156                  // dh> TODO: those do not get called anymore!
 157                  'DisplayItemAsHtml' => 'Called on an item when it gets displayed as HTML.',
 158                  'DisplayItemAsXml' => 'Called on an item when it gets displayed as XML.',
 159                  'DisplayItemAsText' => 'Called on an item when it gets displayed as text.',
 160  
 161                  // fp> These is actually RENDERing, right?
 162                  // TODO: Rename to "DispRender"
 163                  'FilterCommentAuthor' => 'Filters the comment author.',
 164                  'FilterCommentAuthorUrl' => 'Filters the URL of the comment author.',
 165                  'FilterCommentContent' => 'Filters the content of a comment.',
 166  
 167                  'AfterUserDelete' => 'This gets called after an user has been deleted from the database.',
 168                  'AfterUserInsert' => 'This gets called after an user has been inserted into the database.',
 169                  'AfterUserUpdate' => 'This gets called after an user has been updated in the database.',
 170  
 171                  // fp> This is actually RENDERing, right?
 172                  // TODO: Rename to "DispRender"
 173                  'FilterIpAddress' => 'Called when displaying an IP address.',
 174  
 175                  'ItemApplyAsRenderer' => 'Asks the plugin if it wants to apply as a renderer for an item.',
 176                  'ItemCanComment' => 'Asks the plugin if an item can receive comments/feedback.',
 177                  'ItemSendPing' => 'Send a ping to a service about new items.',
 178                  'ItemViewsIncreased' => 'Called when the view counter of an item got increased.',
 179  
 180                  'SkinTag' => 'This method gets invoked when a plugin is called by its code. Providing this method causes the plugin to be listed as a widget.',
 181  
 182                  'AppendHitLog' => 'Called when a hit gets logged, but before it gets recorded.',
 183  
 184                  'BeforeThumbCreate' => 'This gets called before an image thumbnail gets created.',
 185                  'AfterFileUpload' => 'Called before an uploaded file gets saved on server.',
 186  
 187                  'DisplayCommentToolbar' => 'Display a toolbar on the public feedback form',
 188                  'DisplayCommentFormButton' => 'Called in the submit button section of the frontend comment form.',
 189                  'DisplayCommentFormFieldset' => 'Called at the end of the frontend comment form.',
 190                  'DisplayMessageFormButton' => 'Called in the submit button section of the frontend message form.',
 191                  'DisplayMessageFormFieldset' => 'Called at the end of the frontend message form.',
 192                  'DisplayLoginFormFieldset' => 'Called when displaying the "Login" form.',
 193                  'DisplayRegisterFormBefore' => 'Called when displaying the "Register" form.',
 194                  'DisplayRegisterFormFieldset' => 'Called when displaying the "Register" form.',
 195                  'DisplayValidateAccountFormFieldset' => 'Called when displaying the "Validate account" form.',
 196                  'DisplayProfileFormFieldset' => 'Called when displaying the "User profile" form.',
 197  
 198                  'ProfileFormSent' => 'Called when a private profile form has been sent and gets received.',
 199                  'CommentFormSent' => 'Called when a public comment form has been sent and gets received.',
 200                  'BeforeCommentFormInsert' => 'Called before a comment gets recorded through the public comment form.',
 201                  'AfterCommentFormInsert' => 'Called after a comment has been added through public form.',
 202  
 203                  'BeforeTrackbackInsert' => 'Gets called before a trackback gets recorded.',
 204                  'AfterTrackbackInsert' => 'Gets called after a trackback has been recorded.',
 205  
 206                  'LoginAttempt' => 'Called when a user tries to login.',
 207                  'LoginAttemptNeedsRawPassword' => 'A plugin has to return true here, if it needs a raw (un-hashed) password in LoginAttempt.',
 208                  'AlternateAuthentication' => 'Called at the end of the login process, if the user did not try to login, the session has no user attached or only the username and no password is given (see Plugin::AlternateAuthentication() for more info).',
 209                  'MessageFormSent' => 'Called when the "Message to user" form has been submitted.',
 210                  'MessageFormSentCleanup' => 'Called after a email message has been sent through public form.',
 211                  'Logout' => 'Called when a user logs out.',
 212  
 213                  'GetSpamKarmaForComment' => 'Asks plugin for the spam karma of a comment/trackback.',
 214  
 215                  // Other Plugins can use this:
 216                  'CaptchaValidated' => 'Validate the test from CaptchaPayload to detect humans.',
 217                  'CaptchaValidatedCleanup' => 'Cleanup data used for CaptchaValidated.',
 218                  'CaptchaPayload' => 'Provide a turing test to detect humans.',
 219  
 220                  'RegisterFormSent' => 'Called when the "Register" form has been submitted.',
 221                  'ValidateAccountFormSent' => 'Called when the "Validate account" form has been submitted.',
 222                  'AppendUserRegistrTransact' => 'Gets appended to the transaction that creates a new user on registration.',
 223                  'AfterUserRegistration' => 'Gets called after a new user has registered.',
 224  
 225                  'AfterPluginsInit' => 'Gets called after $Plugins is initialized, this is the earliest event.',
 226                  'AfterMainInit' => 'Called at the end of _main.inc.php, this is the the latest event called before blog initialization.',
 227                  'SessionLoaded' => 'Gets called after $Session is initialized, quite early.',
 228                  'BeforeSessionsDelete' => 'Gets called when sessions are being pruned to enable plugin house cleaning, plugin might change the sess_lastseen timestamp of any sessions they want to keep',
 229  
 230                  'AfterLoginAnonymousUser' => 'Gets called at the end of the login procedure for anonymous visitors.',
 231                  'AfterLoginRegisteredUser' => 'Gets called at the end of the login procedure for registered users.',
 232  
 233                  'BeforeBlogDisplay' => 'Gets called before a (part of the blog) gets displayed.',
 234                  'InitMainList' => 'Initializes the MainList object. Use this method to create your own MainList, see init_MainList()',
 235                  'SkinBeginHtmlHead' => 'Gets called at the top of the HTML HEAD section in a skin.',
 236                  'SkinEndHtmlBody' => 'Gets called at the end of the skin\'s HTML BODY section.',
 237                  'DisplayTrackbackAddr' => 'Called to display the trackback URL for an item.',
 238  
 239                  'GetCronJobs' => 'Gets a list of implemented cron jobs.',
 240                  'GetProvidedSkins' => 'Get a list of "skins" handled by the plugin.',
 241  
 242                  // sam2kb> This hook is not used anywhere
 243                  // TODO: remove it
 244                  'PluginUserSettingsEditAction' => 'Called as action before editing a user\'s settings.',
 245  
 246                  // allow plugins to handle $disp modes
 247                  'GetHandledDispModes' => 'Called when building possible $disp list',
 248                  'HandleDispMode' => 'Called when displaying $disp',
 249  
 250                  'GetAdditionalColumnsTable' => 'Called to add columns for Results object',
 251              );
 252  
 253              if( ! defined('EVO_IS_INSTALLING') || ! EVO_IS_INSTALLING )
 254              { // only call this, if we're not in the process of installation, to avoid errors from Plugins in this case!
 255  
 256                  // Let Plugins add additional events (if they trigger those events themselves):
 257                  // fp> please provide an example/use case
 258                  $this->load_plugins_table();
 259  
 260                  $rev_sorted_IDs = array_reverse( $this->sorted_IDs ); // so higher priority overwrites lower (just for desc)
 261  
 262                  foreach( $rev_sorted_IDs as $plugin_ID )
 263                  {
 264                      $Plugin = & $this->get_by_ID( $plugin_ID );
 265  
 266                      if( ! $Plugin )
 267                      {
 268                          continue;
 269                      }
 270  
 271                      $extra_events = $Plugin->GetExtraEvents();
 272                      if( is_array($extra_events) )
 273                      {
 274                          $supported_events = array_merge( $supported_events, $extra_events );
 275                      }
 276                  }
 277              }
 278          }
 279  
 280          return $supported_events;
 281      }
 282  
 283  
 284      /**
 285       * Un-register a plugin, only if forced.
 286       *
 287       * This does not un-install it from DB, just from the internal indexes.
 288       *
 289       * @param Plugin
 290       * @param boolean Force unregistering
 291       * @return boolean True, if unregistered
 292       */
 293  	function unregister( & $Plugin, $force = false )
 294      {
 295          if( ! $force )
 296          {
 297              return false;
 298          }
 299  
 300          return parent::unregister($Plugin, $force);
 301      }
 302  
 303  
 304      /**
 305       * Count # of registrations of same plugin.
 306       *
 307       * Plugins with negative ID (auto-generated; not installed (yet)) will not get considered.
 308       *
 309       * @param string class name
 310       * @return int # of regs
 311       */
 312  	function count_regs( $classname )
 313      {
 314          $count = 0;
 315  
 316          foreach( $this->sorted_IDs as $plugin_ID )
 317          {
 318              $Plugin = & $this->get_by_ID( $plugin_ID );
 319              if( $Plugin && $Plugin->classname == $classname && $Plugin->ID > 0 )
 320              {
 321                  $count++;
 322              }
 323          }
 324          return $count;
 325      }
 326  
 327  
 328      /**
 329       * Discover and register all available plugins in the {@link $plugins_path} folder/subfolders.
 330       */
 331  	function discover()
 332      {
 333          global $Messages, $Debuglog, $Timer;
 334  
 335          $Timer->resume('plugins_discover');
 336  
 337          $Debuglog->add( 'Discovering plugins...', 'plugins' );
 338  
 339          // too inefficient: foreach( get_filenames( $this->plugins_path, array('inc_dirs' => false) ) as $path )
 340  
 341          $filename_params = array(
 342                  'inc_files'    => false,
 343                  'recurse'    => false,
 344              );
 345          // Get subdirs in $this->plugins_path
 346          $subdirs = array();
 347          $subdirs = get_filenames( $this->plugins_path, $filename_params );
 348  
 349          if( empty($subdirs) )
 350              return;
 351  
 352          // Skip plugins which are in a directory that starts with an underscore ("_")
 353          foreach( $subdirs as $k => $v )
 354          {
 355              $v_bn = basename($v);
 356              if( substr(basename($v_bn), 0, 1) == '_' || substr($v_bn, -7) != '_plugin' )
 357              {
 358                  unset($subdirs[$k]);
 359              }
 360          }
 361          $subdirs[] = $this->plugins_path;
 362  
 363          foreach( $subdirs as $subdir )
 364          {
 365              // Some directories may be unreadable ( get_filenames returns false which is not an array )
 366              $filename_params = array(
 367                      'inc_dirs'    => false,
 368                      'recurse'    => false,
 369                  );
 370              if( !$files = get_filenames( $subdir, $filename_params ) )
 371              {
 372                  continue;
 373              }
 374  
 375              foreach( $files as $filename )
 376              {
 377                  if( ! (preg_match( '~/_([^/]+)\.plugin\.php$~', $filename, $match ) && is_file( $filename )) )
 378                  {
 379                      continue;
 380                  }
 381  
 382                  $classname = $match[1].'_plugin';
 383  
 384                  if( $this->get_by_classname($classname) )
 385                  {
 386                      $Debuglog->add( 'Skipping duplicate plugin (classname '.$classname.')!', array('error', 'plugins') );
 387                      continue;
 388                  }
 389                  $this->register( $classname, 0, -1, $filename ); // auto-generate negative ID; will return string on error.
 390              }
 391          }
 392  
 393          $Timer->pause('plugins_discover');
 394      }
 395  
 396  
 397      /**
 398       * Get the list of all possible values for apply_rendering (defines when a rendering Plugin can apply).
 399       *
 400       * @todo Add descriptions.
 401       *
 402       * @param boolean Return an associative array with description for the values?
 403       * @return array
 404       */
 405  	function get_apply_rendering_values( $with_desc = false )
 406      {
 407          static $apply_rendering_values;
 408  
 409          if( empty( $apply_rendering_values ) )
 410          {
 411              $apply_rendering_values = array(
 412                      'stealth' => 'stealth',
 413                      'always' => 'always',
 414                      'opt-out' => 'opt-out',
 415                      'opt-in' => 'opt-in',
 416                      'lazy' => 'automatic', // The plugin will automatically deside to use rendering or not
 417                      'never' => 'never',
 418                  );
 419          }
 420          if( ! $with_desc )
 421          {
 422              return array_keys( $apply_rendering_values );
 423          }
 424  
 425          return $apply_rendering_values;
 426      }
 427  
 428  
 429      /**
 430       * Discover plugin events from its source file.
 431       *
 432       * Get a list of methods that are supported as events out of the Plugin's
 433       * class definition.
 434       *
 435       * @todo Extend to get list of defined classes and global functions and check this list before sourcing/including a Plugin! (prevent fatal error)
 436       *
 437       * @return array
 438       */
 439  	function get_registered_events( $Plugin )
 440      {
 441          global $Timer, $Debuglog;
 442  
 443          $Timer->resume( 'plugins_detect_events' );
 444  
 445          $plugin_class_methods = array();
 446  
 447          if( $Plugin->group == 'rendering' )
 448          { // All Plugin from 'rendering' groups handle the FilterCommentContent
 449              $plugin_class_methods[] = 'FilterCommentContent';
 450          }
 451  
 452          if( ! function_exists('token_get_all') )
 453          {
 454              $Debuglog->add( 'get_registered_events(): PHP function token_get_all() is not available', array('plugins', 'error') );
 455              return array();
 456          }
 457  
 458          if( ! is_readable($Plugin->classfile_path) )
 459          {
 460              $Debuglog->add( 'get_registered_events(): "'.$Plugin->classfile_path.'" is not readable.', array('plugins', 'error') );
 461              return array();
 462          }
 463  
 464          if( ( $classfile_contents = @file_get_contents( $Plugin->classfile_path ) ) === false )
 465          {
 466              $Debuglog->add( 'get_registered_events(): "'.$Plugin->classfile_path.'" could not get read.', array('plugins', 'error') );
 467              return array();
 468          }
 469          $supported_events = array_keys( $this->get_supported_events() );
 470  
 471          // TODO: allow optional Plugin callback to get list of methods. Like Plugin::GetRegisteredEvents().
 472          // fp> bloated. what problem does it solve?
 473          // dh> With a captcha_base.class.php the actual plugin (extending the class) would have to define all the event methods and not just the methods to provide the tests.
 474          //     With a GetRegisteredEvents method in captcha_base.class.php this would not be required.
 475          //     The whole point of such a base class would be to simplify writing a captcha plugin and IMHO it's "bloated" to force a whole block of methods into it that do only call the parent method.
 476  
 477          // TODO: dh> only match in the relevant "class block"
 478          $had_func_token = false;
 479          $token_all = token_get_all( $classfile_contents );
 480          foreach( $token_all as $token )
 481          {
 482              if( ! is_array( $token ) )
 483              {    // Single characters does not interest us:
 484                  continue;
 485              }
 486  
 487              if( $had_func_token && $token[0] == T_STRING )
 488              {    // We got a function name...
 489                  if( ! in_array( $token[1], $plugin_class_methods )
 490                      && in_array( $token[1], $supported_events ) )
 491                  {    // ...and it is unique and matches one of our supported events:
 492                      $plugin_class_methods[] = $token[1];
 493                  }
 494  
 495                  // Search for the next "function" token:
 496                  $had_func_token = false;
 497              }
 498              elseif( ! $had_func_token && $token[0] == T_FUNCTION )
 499              {    // Begin searching for the function name which must be a T_STRING:
 500                  $had_func_token = true;
 501              }
 502          }
 503  
 504          if( ! count( $plugin_class_methods ) )
 505          {
 506              $Debuglog->add( 'No functions found in file "'.$Plugin->classfile_path.'".', array('plugins', 'error') );
 507              return array();
 508          }
 509  
 510          $Timer->pause( 'plugins_detect_events' );
 511  
 512          return $plugin_class_methods;
 513      }
 514  
 515  
 516      /**
 517       * Install a plugin into DB.
 518       *
 519       * NOTE: this won't install necessary DB changes and not trigger {@link Plugin::AfterInstall}!
 520       *
 521       * @param string Classname of the plugin to install
 522       * @param string Initial DB Status of the plugin ("enabled", "disabled", "needs_config", "broken")
 523       * @param string Optional classfile path, if not default (used for tests).
 524       * @param boolean Must the plugin exist (classfile_path and classname)?
 525       *                This is used internally to be able to unregister a non-existing plugin.
 526       * @return Plugin The installed Plugin (perhaps with $install_dep_notes set) or a string in case of error.
 527       */
 528      function & install( $classname, $plug_status = 'enabled', $classfile_path = NULL, $must_exists = true )
 529      {
 530          global $DB, $Debuglog;
 531  
 532          // Load Plugins data from T_plugins (only once), ordered by priority.
 533          $this->load_plugins_table();
 534  
 535          // Register the plugin:
 536          $Plugin = & $this->register( $classname, 0, -1, $classfile_path, $must_exists ); // Auto-generates negative ID; New ID will be set a few lines below
 537  
 538          if( is_string($Plugin) )
 539          { // return error message from register()
 540              return $Plugin;
 541          }
 542  
 543          if( isset($Plugin->number_of_installs)
 544              && ( $this->count_regs( $Plugin->classname ) >= $Plugin->number_of_installs ) )
 545          {
 546              $this->unregister( $Plugin, true );
 547              $r = T_('The plugin cannot be installed again.');
 548              return $r;
 549          }
 550  
 551          $install_return = $Plugin->BeforeInstall();
 552          if( $install_return !== true )
 553          {
 554              $this->unregister( $Plugin, true );
 555              $r = T_('The installation of the plugin failed.');
 556              if( is_string($install_return) )
 557              {
 558                  $r .= '<br />'.$install_return;
 559              }
 560              return $r;
 561          }
 562  
 563          // Dependencies:
 564          /*
 565          // We must check dependencies against installed Plugins ($Plugins)
 566          // TODO: not possible anymore.. check it..
 567          global $Plugins;
 568          $dep_msgs = $Plugins->validate_dependencies( $Plugin, 'enable' );
 569          */
 570          $dep_msgs = $this->validate_dependencies( $Plugin, 'enable' );
 571  
 572          if( ! empty( $dep_msgs['error'] ) )
 573          { // fatal errors (required dependencies):
 574              $this->unregister( $Plugin, true );
 575              $r = T_('Some plugin dependencies are not fulfilled:').' <ul><li>'.implode( '</li><li>', $dep_msgs['error'] ).'</li></ul>';
 576              return $r;
 577          }
 578  
 579          // All OK, install:
 580          if( empty($Plugin->code) )
 581          {
 582              $Plugin->code = NULL;
 583          }
 584  
 585          $Plugin->status = $plug_status;
 586  
 587          // Record into DB
 588          $DB->begin();
 589  
 590          $DB->query( '
 591                  INSERT INTO T_plugins( plug_classname, plug_priority, plug_code, plug_version, plug_status )
 592                  VALUES( "'.$classname.'", '.$Plugin->priority.', '.$DB->quote($Plugin->code).', '.$DB->quote($Plugin->version).', '.$DB->quote($Plugin->status).' ) ' );
 593  
 594          // Unset auto-generated ID info
 595          unset( $this->index_ID_Plugins[ $Plugin->ID ] );
 596          $key = array_search( $Plugin->ID, $this->sorted_IDs );
 597  
 598          // New ID:
 599          $Plugin->ID = $DB->insert_id;
 600          $this->index_ID_Plugins[ $Plugin->ID ] = & $Plugin;
 601          $this->index_ID_rows[ $Plugin->ID ] = array(
 602                  'plug_ID' => $Plugin->ID,
 603                  'plug_priority' => $Plugin->priority,
 604                  'plug_classname' => $Plugin->classname,
 605                  'plug_code' => $Plugin->code,
 606                  'plug_status' => $Plugin->status,
 607                  'plug_version' => $Plugin->version,
 608              );
 609          $this->sorted_IDs[$key] = $Plugin->ID;
 610  
 611          $this->save_events( $Plugin );
 612  
 613          $DB->commit();
 614  
 615          $Debuglog->add( 'Installed plugin: '.$Plugin->name.' ID: '.$Plugin->ID, 'plugins' );
 616  
 617          if( ! empty($dep_msgs['note']) )
 618          { // Add dependency notes
 619              $Plugin->install_dep_notes = $dep_msgs['note'];
 620          }
 621  
 622          // Do the stuff that we've skipped in register method at the beginning:
 623  
 624          $this->init_settings( $Plugin );
 625  
 626          $tmp_params = array('db_row' => $this->index_ID_rows[$Plugin->ID], 'is_installed' => false);
 627  
 628          if( $Plugin->PluginInit( $tmp_params ) === false && $this->unregister( $Plugin ) )
 629          {
 630              $Debuglog->add( 'Unregistered plugin, because PluginInit returned false.', 'plugins' );
 631              $Plugin = '';
 632          }
 633  
 634          if( ! defined('EVO_IS_INSTALLING') || ! EVO_IS_INSTALLING )
 635          { // do not sort, if we're installing/upgrading.. instantiating Plugins might cause a fatal error!
 636              $this->sort();
 637          }
 638  
 639          return $Plugin;
 640      }
 641  
 642  
 643      /**
 644       * Uninstall a plugin.
 645       *
 646       * Removes the Plugin, its Settings and Events from the database.
 647       *
 648       * @return boolean True on success
 649       */
 650  	function uninstall( $plugin_ID )
 651      {
 652          global $DB, $Debuglog;
 653  
 654          $Debuglog->add( 'Uninstalling plugin (ID '.$plugin_ID.')...', 'plugins' );
 655  
 656          $Plugin = & $this->get_by_ID( $plugin_ID ); // get the Plugin before any not loaded data might get deleted below
 657  
 658          $DB->begin();
 659  
 660          // Delete Plugin settings (constraints)
 661          $DB->query( "DELETE FROM T_pluginsettings
 662                        WHERE pset_plug_ID = $plugin_ID" );
 663  
 664          // Delete Plugin user settings (constraints)
 665          $DB->query( "DELETE FROM T_pluginusersettings
 666                        WHERE puset_plug_ID = $plugin_ID" );
 667  
 668          // Delete Plugin events (constraints)
 669          $plugin_events = $DB->get_col( '
 670                      SELECT pevt_event
 671                      FROM T_pluginevents
 672                      WHERE pevt_enabled = 1'
 673          );
 674          $plugin_events = implode( '.', $plugin_events );
 675          if( strpos( $plugin_events, 'RenderItemAs' ) !== false )
 676          { // Clear pre-rendered content cache, if RenderItemAs* events get removed:
 677              $DB->query( 'DELETE FROM T_items__prerendering WHERE 1=1' );
 678          }
 679          if( strpos( $plugin_events, 'FilterCommentContent' ) !== false )
 680          { // Clear pre-rendered comments content cache, if FilterCommentContent plugin get removed
 681              $DB->query( 'DELETE FROM T_comments__prerendering WHERE 1=1' );
 682          }
 683  
 684          // Remove plugin collection settings
 685          $DB->query( "DELETE FROM T_coll_settings
 686                        WHERE cset_name LIKE 'plugin".$plugin_ID."_%'" );
 687  
 688          $DB->query( "DELETE FROM T_pluginevents
 689                        WHERE pevt_plug_ID = $plugin_ID" );
 690  
 691          // Delete from DB
 692          $DB->query( "DELETE FROM T_plugins
 693                        WHERE plug_ID = $plugin_ID" );
 694  
 695          $DB->commit();
 696  
 697          if( $Plugin )
 698          {
 699              $this->unregister( $Plugin, true );
 700          }
 701  
 702          $Debuglog->add( 'Uninstalled plugin (ID '.$plugin_ID.').', 'plugins' );
 703          return true;
 704      }
 705  
 706  
 707      /**
 708       * (Re)load Plugin Events for enabled (normal use) or all (admin use) plugins.
 709       *
 710       * This is the same as {@link Plugins::load_events()} except that it loads all Plugins (not just enabled ones)
 711       */
 712  	function load_events()
 713      {
 714          global $Debuglog, $DB;
 715  
 716          $this->index_event_IDs = array();
 717  
 718          $Debuglog->add( 'Loading plugin events.', 'plugins' );
 719          foreach( $DB->get_results( '
 720                  SELECT pevt_plug_ID, pevt_event
 721                      FROM T_pluginevents INNER JOIN T_plugins ON pevt_plug_ID = plug_ID
 722                   WHERE pevt_enabled > 0
 723                   ORDER BY plug_priority, plug_classname', OBJECT, 'Loading plugin events' ) as $l_row )
 724          {
 725              $this->index_event_IDs[$l_row->pevt_event][] = $l_row->pevt_plug_ID;
 726          }
 727      }
 728  
 729  
 730      /**
 731       * Save the events that the plugin provides into DB, while removing obsolete
 732       * entries (that the plugin does not register anymore).
 733       *
 734       * @param Plugin Plugin to save events for
 735       * @param array List of events to save as enabled for the Plugin.
 736       *              By default all provided events get saved as enabled. Pass array() to discover only new ones.
 737       * @param array List of events to save as disabled for the Plugin.
 738       *              By default, no events get disabled. Disabling an event takes priority over enabling.
 739       * @return boolean True, if events have changed, false if not.
 740       */
 741  	function save_events( $Plugin, $enable_events = NULL, $disable_events = NULL )
 742      {
 743          global $DB, $Debuglog;
 744  
 745          $r = false;
 746  
 747          $saved_events = array();
 748          foreach( $DB->get_results( '
 749                  SELECT pevt_event, pevt_enabled
 750                    FROM T_pluginevents
 751                   WHERE pevt_plug_ID = '.$Plugin->ID ) as $l_row )
 752          {
 753              $saved_events[$l_row->pevt_event] = $l_row->pevt_enabled;
 754          }
 755  
 756          // Discover events from plugin's source file:
 757          $available_events = $this->get_registered_events( $Plugin );
 758  
 759          $obsolete_events = array_diff( array_keys($saved_events), $available_events );
 760  
 761          if( is_null( $enable_events ) )
 762          { // Enable all events:
 763              $enable_events = $available_events;
 764          }
 765          if( is_null( $disable_events ) )
 766          {
 767              $disable_events = array();
 768          }
 769          if( $disable_events )
 770          { // Remove events to be disabled from enabled ones:
 771              $enable_events = array_diff( $enable_events, $disable_events );
 772          }
 773  
 774          // New discovered events:
 775          $discovered_events = array_diff( $available_events, array_keys($saved_events), $enable_events, $disable_events );
 776  
 777  
 778          // Delete obsolete events from DB:
 779          if( $obsolete_events && $DB->query( '
 780                  DELETE FROM T_pluginevents
 781                  WHERE pevt_plug_ID = '.$Plugin->ID.'
 782                  AND pevt_event IN ( "'.implode( '", "', $obsolete_events ).'" )' ) )
 783          {
 784              $r = true;
 785          }
 786  
 787          if( $discovered_events )
 788          {
 789              $DB->query( '
 790                  INSERT INTO T_pluginevents( pevt_plug_ID, pevt_event, pevt_enabled )
 791                  VALUES ( '.$Plugin->ID.', "'.implode( '", 1 ), ('.$Plugin->ID.', "', $discovered_events ).'", 1 )' );
 792              $r = true;
 793  
 794              $Debuglog->add( 'Discovered events ['.implode( ', ', $discovered_events ).'] for Plugin '.$Plugin->name, 'plugins' );
 795          }
 796  
 797          $new_events_enabled = array();
 798          if( $enable_events )
 799          {
 800              foreach( $enable_events as $l_event )
 801              {
 802                  if( ! isset( $saved_events[$l_event] ) || ! $saved_events[$l_event] )
 803                  { // Event not saved yet or not enabled
 804                      $new_events_enabled[] = $l_event;
 805                  }
 806              }
 807              if( $new_events_enabled )
 808              {
 809                  $DB->query( '
 810                      REPLACE INTO T_pluginevents( pevt_plug_ID, pevt_event, pevt_enabled )
 811                      VALUES ( '.$Plugin->ID.', "'.implode( '", 1 ), ('.$Plugin->ID.', "', $new_events_enabled ).'", 1 )' );
 812                  $r = true;
 813              }
 814              $Debuglog->add( 'Enabled events ['.implode( ', ', $new_events_enabled ).'] for Plugin '.$Plugin->name, 'plugins' );
 815          }
 816  
 817          $new_events_disabled = array();
 818          if( $disable_events )
 819          {
 820              foreach( $disable_events as $l_event )
 821              {
 822                  if( ! isset( $saved_events[$l_event] ) || $saved_events[$l_event] )
 823                  { // Event not saved yet or enabled
 824                      $new_events_disabled[] = $l_event;
 825                  }
 826              }
 827              if( $new_events_disabled )
 828              {
 829                  $DB->query( '
 830                      REPLACE INTO T_pluginevents( pevt_plug_ID, pevt_event, pevt_enabled )
 831                      VALUES ( '.$Plugin->ID.', "'.implode( '", 0 ), ('.$Plugin->ID.', "', $new_events_disabled ).'", 0 )' );
 832                  $r = true;
 833              }
 834              $Debuglog->add( 'Disabled events ['.implode( ', ', $new_events_disabled ).'] for Plugin '.$Plugin->name, 'plugins' );
 835          }
 836  
 837          if( $r )
 838          { // Something has changed: Reload event index
 839              foreach( array_merge($obsolete_events, $discovered_events, $new_events_enabled, $new_events_disabled) as $event )
 840              {
 841                  if( strpos($event, 'RenderItemAs') === 0 )
 842                  { // Clear pre-rendered content cache, if RenderItemAs* events have been added or removed:
 843                      $DB->query( 'DELETE FROM T_items__prerendering WHERE 1=1' );
 844                      $ItemCache = & get_ItemCache();
 845                      $ItemCache->clear();
 846                      break;
 847                  }
 848              }
 849  
 850              $this->load_events();
 851          }
 852  
 853          return $r;
 854      }
 855  
 856  
 857      /**
 858       * Reload all plugins to detect changes
 859       *  - Register new events
 860       *  - Unregister obsolete events
 861       *  - Detect plugins with no code and try to have at least one plugin with the default code
 862       *
 863       * @return boolean true if plugins have been changed, false otherwise
 864       */
 865  	function reload_plugins()
 866      {
 867          $this->restart();
 868          $this->load_events();
 869          $changed = false;
 870          while( $loop_Plugin = & $this->get_next() )
 871          { // loop through in each plugin
 872              // NOTE: we don't need to handle plug_version here, because it gets handled in Plugins::register() already.
 873  
 874              // Discover new events:
 875              if( $this->save_events( $loop_Plugin, array() ) )
 876              {
 877                  $changed = true;
 878              }
 879  
 880              // Detect plugins with no code and try to have at least one plugin with the default code:
 881              if( empty($loop_Plugin->code) )
 882              { // Instantiated Plugin has no code
 883                  $default_Plugin = & $this->register($loop_Plugin->classname);
 884  
 885                  if( ! empty($default_Plugin->code) // Plugin has default code
 886                      && ! $this->get_by_code( $default_Plugin->code ) ) // Default code is not in use (anymore)
 887                  { // Set the Plugin's code to the default one
 888                      if( $this->set_code( $loop_Plugin->ID, $default_Plugin->code ) )
 889                      {
 890                          $changed = true;
 891                      }
 892                  }
 893  
 894                  $this->unregister($default_Plugin, true);
 895              }
 896          }
 897  
 898          if( $changed )
 899          { // invalidate all PageCaches
 900              invalidate_pagecaches();
 901          }
 902  
 903          return $changed;
 904      }
 905  
 906  
 907      /**
 908       * Set the code for a given Plugin ID.
 909       *
 910       * It makes sure that the index is handled and writes it to DB.
 911       *
 912       * @param string Plugin ID
 913       * @param string Code to set the plugin to
 914       * @return boolean|integer|string
 915       *   true, if already set to same value.
 916       *   string: error message (already in use, wrong format)
 917       *   1 in case of setting it into DB (number of affected rows).
 918       *   false, if invalid Plugin.
 919       */
 920  	function set_code( $plugin_ID, $code )
 921      {
 922          global $DB;
 923  
 924          if( strlen( $code ) > 32 )
 925          {
 926              return T_( 'The maximum length of a plugin code is 32 characters.' );
 927          }
 928  
 929          // TODO: more strict check?! Just "[\w_-]+" as regexp pattern?
 930          if( strpos( $code, '.' ) !== false )
 931          {
 932              return T_( 'The plugin code cannot include a dot!' );
 933          }
 934  
 935          if( ! empty($code) && isset( $this->index_code_ID[$code] ) )
 936          {
 937              if( $this->index_code_ID[$code] == $plugin_ID )
 938              { // Already set to same value
 939                  return true;
 940              }
 941              else
 942              {
 943                  return T_( 'The plugin code is already in use by another plugin.' );
 944              }
 945          }
 946  
 947          $Plugin = & $this->get_by_ID( $plugin_ID );
 948          if( ! $Plugin )
 949          {
 950              return false;
 951          }
 952  
 953          if( empty($code) )
 954          {
 955              $code = NULL;
 956          }
 957          else
 958          { // update indexes
 959              $this->index_code_ID[$code] = & $Plugin->ID;
 960              $this->index_code_Plugins[$code] = & $Plugin;
 961          }
 962  
 963          // Update references to code:
 964          // TODO: dh> we might want to update item renderer codes and blog ping plugin codes here! (old code=>new code)
 965  
 966          $Plugin->code = $code;
 967  
 968          return $DB->query( '
 969              UPDATE T_plugins
 970                SET plug_code = '.$DB->quote($code).'
 971              WHERE plug_ID = '.$plugin_ID );
 972      }
 973  
 974  
 975      /**
 976       * Set the status of an event for a given Plugin.
 977       *
 978       * @return boolean True, if status has changed; false if not
 979       */
 980  	function set_event_status( $plugin_ID, $plugin_event, $enabled )
 981      {
 982          global $DB;
 983  
 984          $enabled = $enabled ? 1 : 0;
 985  
 986          $DB->query( '
 987              UPDATE T_pluginevents
 988                 SET pevt_enabled = '.$enabled.'
 989               WHERE pevt_plug_ID = '.$plugin_ID.'
 990                 AND pevt_event = "'.$plugin_event.'"' );
 991  
 992          if( $DB->rows_affected )
 993          {
 994              $this->load_events();
 995  
 996              if( strpos($plugin_event, 'RenderItemAs') === 0 )
 997              { // Clear pre-rendered content cache, if RenderItemAs* events have been added or removed:
 998                  $DB->query( 'DELETE FROM T_items__prerendering WHERE 1=1' );
 999                  $ItemCache = & get_ItemCache();
1000                  $ItemCache->clear();
1001                  break;
1002              }
1003  
1004              return true;
1005          }
1006  
1007          return false;
1008      }
1009  
1010  
1011      /**
1012       * Set the priority for a given Plugin ID.
1013       *
1014       * It makes sure that the index is handled and writes it to DB.
1015       *
1016       * @return boolean|integer
1017       *   true, if already set to same value.
1018       *   false if another Plugin uses that priority already.
1019       *   1 in case of setting it into DB.
1020       */
1021  	function set_priority( $plugin_ID, $priority )
1022      {
1023          global $DB;
1024  
1025          if( ! preg_match( '~^1?\d?\d$~', $priority ) ) // using preg_match() to catch floating numbers
1026          {
1027              debug_die( 'Plugin priority must be numeric (0-100).' );
1028          }
1029  
1030          $Plugin = & $this->get_by_ID($plugin_ID);
1031          if( ! $Plugin )
1032          {
1033              return false;
1034          }
1035  
1036          if( $Plugin->priority == $priority )
1037          { // Already set to same value
1038              return true;
1039          }
1040  
1041          $r = $DB->query( '
1042              UPDATE T_plugins
1043                SET plug_priority = '.$DB->quote($priority).'
1044              WHERE plug_ID = '.$plugin_ID );
1045  
1046          $Plugin->priority = $priority;
1047  
1048          // TODO: dh> should only re-sort, if sorted by priority before - if it should get re-sorted at all!
1049          //$this->sort();
1050  
1051          return $r;
1052      }
1053  
1054  
1055      /**
1056       * Sort the list of plugins.
1057       *
1058       * WARNING: do NOT sort by anything else than priority unless you're handling a list of NOT-YET-INSTALLED plugins!
1059       *
1060       * @param string Order: 'priority' (default), 'name'
1061       */
1062  	function sort( $order = 'priority' )
1063      {
1064          $this->load_plugins_table();
1065  
1066          foreach( $this->sorted_IDs as $k => $plugin_ID )
1067          { // Instantiate every plugin, so invalid ones do not get unregistered during sorting (crashes PHP, because $sorted_IDs gets changed etc)
1068              if( ! $this->get_by_ID( $plugin_ID ) )
1069              {
1070                  unset($this->sorted_IDs[$k]);
1071              }
1072          }
1073  
1074          switch( $order )
1075          {
1076              case 'name':
1077                  usort( $this->sorted_IDs, array( & $this, 'sort_Plugin_name') );
1078                  break;
1079  
1080              case 'group':
1081                  usort( $this->sorted_IDs, array( & $this, 'sort_Plugin_group') );
1082                  break;
1083  
1084              default:
1085                  // Sort array by priority:
1086                  usort( $this->sorted_IDs, array( & $this, 'sort_Plugin_priority') );
1087          }
1088  
1089          $this->current_idx = -1;
1090      }
1091  
1092      /**
1093       * Callback function to sort plugins by priority (and classname, if they have same priority).
1094       */
1095  	function sort_Plugin_priority( & $a_ID, & $b_ID )
1096      {
1097          $a_Plugin = & $this->get_by_ID( $a_ID );
1098          $b_Plugin = & $this->get_by_ID( $b_ID );
1099  
1100          $r = $a_Plugin->priority - $b_Plugin->priority;
1101  
1102          if( $r == 0 )
1103          {
1104              $r = strcasecmp( $a_Plugin->classname, $b_Plugin->classname );
1105          }
1106  
1107          return $r;
1108      }
1109  
1110      /**
1111       * Callback function to sort plugins by name.
1112       *
1113       * WARNING: do NOT sort by anything else than priority unless you're handling a list of NOT-YET-INSTALLED plugins
1114       */
1115  	function sort_Plugin_name( & $a_ID, & $b_ID )
1116      {
1117          $a_Plugin = & $this->get_by_ID( $a_ID );
1118          $b_Plugin = & $this->get_by_ID( $b_ID );
1119  
1120          return strcasecmp( $a_Plugin->name, $b_Plugin->name );
1121      }
1122  
1123  
1124      /**
1125       * Callback function to sort plugins by group, sub-group and name.
1126       *
1127       * Those, which have a group get sorted above the ones without one.
1128       *
1129       * WARNING: do NOT sort by anything else than priority unless you're handling a list of NOT-YET-INSTALLED plugins
1130       */
1131  	function sort_Plugin_group( & $a_ID, & $b_ID )
1132      {
1133          $a_Plugin = & $this->get_by_ID( $a_ID );
1134          $b_Plugin = & $this->get_by_ID( $b_ID );
1135  
1136          // first check if both have a group (-1: only A has a group; 1: only B has a group; 0: both have a group or no group):
1137          $r = (int)empty($a_Plugin->group) - (int)empty($b_Plugin->group);
1138          if( $r != 0 )
1139          {
1140              return $r;
1141          }
1142  
1143          // Compare Group
1144          $r = strcasecmp( $a_Plugin->group, $b_Plugin->group );
1145          if( $r != 0 )
1146          {
1147              return $r;
1148          }
1149  
1150          // Compare Sub Group
1151          $r = strcasecmp( $a_Plugin->sub_group, $b_Plugin->sub_group );
1152          if( $r != 0 )
1153          {
1154              return $r;
1155          }
1156  
1157          // Compare Name
1158          return strcasecmp( $a_Plugin->name, $b_Plugin->name );
1159      }
1160  
1161  
1162      /**
1163       * Validate dependencies of a Plugin.
1164       *
1165       * @param Plugin
1166       * @param string Mode of check: either 'enable' or 'disable'
1167       * @return array The key 'note' holds an array of notes (recommendations), the key 'error' holds a list
1168       *               of messages for dependency errors.
1169       */
1170  	function validate_dependencies( & $Plugin, $mode )
1171      {
1172          global $DB, $app_name;
1173          global $app_version;
1174  
1175          $msgs = array();
1176  
1177          if( $mode == 'disable' )
1178          { // Check the whole list of installed plugins if they depend on our Plugin or it's (set of) events.
1179              $required_by_plugin = array(); // a list of plugin classnames that require our poor Plugin
1180  
1181              foreach( $this->sorted_IDs as $validate_against_ID )
1182              {
1183                  if( $validate_against_ID == $Plugin->ID )
1184                  { // the plugin itself
1185                      continue;
1186                  }
1187  
1188                  $against_Plugin = & $this->get_by_ID($validate_against_ID);
1189  
1190                  if( $against_Plugin->status != 'enabled' )
1191                  { // The plugin is not enabled (this check is needed when checking deps with the Plugins_admin class)
1192                      continue;
1193                  }
1194  
1195                  $deps = $against_Plugin->GetDependencies();
1196  
1197                  if( empty($deps['requires']) )
1198                  { // has no dependencies
1199                      continue;
1200                  }
1201  
1202                  if( ! empty($deps['requires']['plugins']) )
1203                  {
1204                      foreach( $deps['requires']['plugins'] as $l_req_plugin )
1205                      {
1206                          if( ! is_array($l_req_plugin) )
1207                          {
1208                              $l_req_plugin = array( $l_req_plugin, 0 );
1209                          }
1210  
1211                          if( $Plugin->classname == $l_req_plugin[0] )
1212                          { // our plugin is required by this one, check if it is the only instance
1213                              if( $this->count_regs($Plugin->classname) < 2 )
1214                              {
1215                                  $required_by_plugin[] = $against_Plugin->classname;
1216                              }
1217                          }
1218                      }
1219                  }
1220  
1221                  if( ! empty($deps['requires']['events_by_one']) )
1222                  {
1223                      foreach( $deps['requires']['events_by_one'] as $req_events )
1224                      {
1225                          // Get a list of plugins that provide all the events
1226                          $provided_by = array_keys( $this->get_list_by_all_events( $req_events ) );
1227  
1228                          if( in_array($Plugin->ID, $provided_by) && count($provided_by) < 2 )
1229                          { // we're the only Plugin which provides this set of events
1230                              $msgs['error'][] = sprintf( T_( 'The events %s are required by %s (ID %d).' ), implode_with_and($req_events), $against_Plugin->classname, $against_Plugin->ID );
1231                          }
1232                      }
1233                  }
1234  
1235                  if( ! empty($deps['requires']['events']) )
1236                  {
1237                      foreach( $deps['requires']['events'] as $req_event )
1238                      {
1239                          // Get a list of plugins that provide all the events
1240                          $provided_by = array_keys( $this->get_list_by_event( $req_event ) );
1241  
1242                          if( in_array($Plugin->ID, $provided_by) && count($provided_by) < 2 )
1243                          { // we're the only Plugin which provides this event
1244                              $msgs['error'][] = sprintf( T_( 'The event %s is required by %s (ID %d).' ), $req_event, $against_Plugin->classname, $against_Plugin->ID );
1245                          }
1246                      }
1247                  }
1248  
1249                  // TODO: We might also handle the 'recommends' and add it to $msgs['note']
1250              }
1251  
1252              if( ! empty( $required_by_plugin ) )
1253              { // Prepend the message to the beginning, because it's the most restrictive (IMHO)
1254                  $required_by_plugin = array_unique($required_by_plugin);
1255                  if( ! isset($msgs['error']) )
1256                  {
1257                      $msgs['error'] = array();
1258                  }
1259                  array_unshift( $msgs['error'], sprintf( T_('The plugin is required by the following plugins: %s.'), implode_with_and($required_by_plugin) ) );
1260              }
1261  
1262              return $msgs;
1263          }
1264  
1265  
1266          // mode 'enable':
1267          $deps = $Plugin->GetDependencies();
1268  
1269          if( empty($deps) )
1270          {
1271              return array();
1272          }
1273  
1274          foreach( $deps as $class => $dep_list ) // class: "requires" or "recommends"
1275          {
1276              if( ! is_array($dep_list) )
1277              { // Invalid format: "throw" error (needs not translation)
1278                  return array(
1279                          'error' => array( 'GetDependencies() did not return array of arrays. Please contact the plugin developer.' )
1280                      );
1281              }
1282              foreach( $dep_list as $type => $type_params )
1283              {
1284                  switch( $type )
1285                  {
1286                      case 'events_by_one':
1287                          foreach( $type_params as $sub_param )
1288                          {
1289                              if( ! is_array($sub_param) )
1290                              { // Invalid format: "throw" error (needs not translation)
1291                                  return array(
1292                                          'error' => array( 'GetDependencies() did not return array of arrays for "events_by_one". Please contact the plugin developer.' )
1293                                      );
1294                              }
1295                              if( ! $this->are_events_available( $sub_param, true ) )
1296                              {
1297                                  if( $class == 'recommends' )
1298                                  {
1299                                      $msgs['note'][] = sprintf( T_( 'The plugin recommends a plugin which provides all of the following events: %s.' ), implode_with_and( $sub_param ) );
1300                                  }
1301                                  else
1302                                  {
1303                                      $msgs['error'][] = sprintf( T_( 'The plugin requires a plugin which provides all of the following events: %s.' ), implode_with_and( $sub_param ) );
1304                                  }
1305                              }
1306                          }
1307                          break;
1308  
1309                      case 'events':
1310                          if( ! $this->are_events_available( $type_params, false ) )
1311                          {
1312                              if( $class == 'recommends' )
1313                              {
1314                                  $msgs['note'][] = sprintf( T_( 'The plugin recommends plugins which provide the events: %s.' ), implode_with_and( $type_params ) );
1315                              }
1316                              else
1317                              {
1318                                  $msgs['error'][] = sprintf( T_( 'The plugin requires plugins which provide the events: %s.' ), implode_with_and( $type_params ) );
1319                              }
1320                          }
1321                          break;
1322  
1323                      case 'plugins':
1324                          if( ! is_array($type_params) )
1325                          { // Invalid format: "throw" error (needs not translation)
1326                              return array(
1327                                      'error' => array( 'GetDependencies() did not return array of arrays for "plugins". Please contact the plugin developer.' )
1328                                  );
1329                          }
1330                          foreach( $type_params as $plugin_req )
1331                          {
1332                              if( ! is_array($plugin_req) )
1333                              {
1334                                  $plugin_req = array( $plugin_req, '0' );
1335                              }
1336                              elseif( ! isset($plugin_req[1]) )
1337                              {
1338                                  $plugin_req[1] = '0';
1339                              }
1340  
1341                              if( $versions = $DB->get_col( '
1342                                  SELECT plug_version FROM T_plugins
1343                                   WHERE plug_classname = '.$DB->quote($plugin_req[0]).'
1344                                       AND plug_status = "enabled"' ) )
1345                              {
1346                                  // Clean up version from CVS Revision prefix/suffix:
1347                                  $versions[] = $plugin_req[1];
1348                                  $clean_versions = preg_replace( array( '~^(CVS\s+)?\$'.'Revision:\s*~i', '~\s*\$$~' ), '', $versions );
1349                                  $clean_req_ver = array_pop($clean_versions);
1350                                  usort( $clean_versions, 'version_compare' );
1351                                  $clean_oldest_enabled = array_shift($clean_versions);
1352  
1353                                  if( version_compare( $clean_oldest_enabled, $clean_req_ver, '<' ) )
1354                                  { // at least one instance of the installed plugins is not the current version
1355                                      $msgs['error'][] = sprintf( T_( 'The plugin requires at least version %s of the plugin %s, but you have %s.' ), $plugin_req[1], $plugin_req[0], $clean_oldest_enabled );
1356                                  }
1357                              }
1358                              else
1359                              { // no plugin existing
1360                                  if( $class == 'recommends' )
1361                                  {
1362                                      $recommends[] = $plugin_req[0];
1363                                  }
1364                                  else
1365                                  {
1366                                      $requires[] = $plugin_req[0];
1367                                  }
1368                              }
1369                          }
1370  
1371                          if( ! empty( $requires ) )
1372                          {
1373                              $msgs['error'][] = sprintf( T_( 'The plugin requires the plugins: %s.' ), implode_with_and( $requires ) );
1374                          }
1375  
1376                          if( ! empty( $recommends ) )
1377                          {
1378                              $msgs['note'][] = sprintf( T_( 'The plugin recommends to install the plugins: %s.' ), implode_with_and( $recommends ) );
1379                          }
1380                          break;
1381  
1382  
1383                      case 'app_min':
1384                          // min b2evo version:
1385                          if( ! version_compare( $app_version, $type_params, '>=' ) )
1386                          {
1387                              if( $class == 'recommends' )
1388                              {
1389                                  $msgs['note'][] = sprintf( /* 1: recommened version; 2: application name (default "b2evolution"); 3: current application version */
1390                                      T_('The plugin recommends version %s of %s (%s is installed). Think about upgrading.'), $type_params, $app_name, $app_version );
1391                              }
1392                              else
1393                              {
1394                                  $msgs['error'][] = sprintf( /* 1: required version; 2: application name (default "b2evolution"); 3: current application version */
1395                                      T_('The plugin requires version %s of %s, but %s is installed.'), $type_params, $app_name, $app_version );
1396                              }
1397                          }
1398                          break;
1399  
1400  
1401                      case 'api_min':
1402                          // obsolete since 1.9:
1403                          continue;
1404  
1405  
1406                      default:
1407                          // Unknown depency type, throw an error:
1408                          $msgs['error'][] = sprintf( T_('Unknown dependency type (%s). This probably means that the plugin is not compatible and you have to upgrade your %s installation.'), $type, $app_name );
1409  
1410                  }
1411              }
1412          }
1413  
1414          return $msgs;
1415      }
1416  
1417  
1418      /**
1419       * Handle filter/unfilter_contents
1420       *
1421       * See {@link Plugins_admin::filter_contents()} and {@link Plugins_admin::unfilter_contents()}
1422       *
1423       * @param array renderer codes to use for opt-out, opt-in and lazy
1424       * @param array array params key => value, must contain:
1425       *  - 'event' => 'FilterItemContents' or 'UnfilterItemContents'
1426       *  - 'object_type' => 'Item' or 'Comment'
1427       *  - 'object_Blog' => the Blog where the edited Object belongs to
1428       *  - 'title' => the object title
1429       *  - 'content' => the object content
1430       *  @return mixed string rendered content on success | false on failure
1431       */
1432  	function process_event_filtering( $renderers, & $params )
1433      {
1434          if( !isset( $params['event'] ) || !in_array( $params['event'], array( 'FilterItemContents', 'UnfilterItemContents' ) ) )
1435          { // invalid event param
1436              return false;
1437          }
1438  
1439          if( !isset( $params['object_Blog'] ) )
1440          {
1441              global $Blog;
1442              if( empty( $Blog ) )
1443              {
1444                  return false;
1445              }
1446              $params['object_Blog'] = & $Blog;
1447          }
1448  
1449          if( isset( $params['object_type'] ) && ( $params['object_type'] == 'Comment' ) )
1450          {
1451              $rendering_setting_name = 'coll_apply_comment_rendering';
1452          }
1453          else
1454          {
1455              $rendering_setting_name = 'coll_apply_rendering';
1456          }
1457  
1458          $params = array_merge( array(
1459                  'title' => & $title,
1460                  'content' => & $content,
1461              ), $params
1462          );
1463  
1464          $event = $params['event'];
1465          $filter_Plugins = $this->get_list_by_event( $event );
1466  
1467          foreach( $filter_Plugins as $loop_filter_Plugin )
1468          { // Go through whole list of renders
1469              switch( $loop_filter_Plugin->get_coll_setting( $rendering_setting_name, $params['object_Blog'] ) )
1470              {
1471                  case 'stealth':
1472                  case 'always':
1473                      // echo 'FORCED ';
1474                      $this->call_method( $loop_filter_Plugin->ID, $event, $params );
1475                      break;
1476  
1477                  case 'opt-out':
1478                  case 'opt-in':
1479                  case 'lazy':
1480                      if( in_array( $loop_filter_Plugin->code, $renderers ) )
1481                      { // Option is activated
1482                          // echo 'OPT ';
1483                          $this->call_method( $loop_filter_Plugin->ID, $event, $params );
1484                      }
1485                      // else echo 'NOOPT ';
1486                      break;
1487  
1488                  case 'never':
1489                      // echo 'NEVER ';
1490                      break;    // STOP, don't render, go to next renderer
1491              }
1492          }
1493  
1494          return $content;
1495      }
1496  
1497  
1498      /**
1499       * Filter (post) contents by calling the relevant filter plugins.
1500       *
1501       * Works very much like render() except that it's called at insert/update time and BEFORE validation.
1502       * Gives an opportunity to do some serious cleanup on what the user has typed.
1503       *
1504       * This uses the lost of renderers, because filtering may need to work in conjunction with rendering,
1505       * e-g: code display: you want to filter out tags before validation and later you want to render color/fixed font.
1506       * For brute force filtering, use 'always' or 'stealth' modes.
1507       * @see Plugins::render()
1508       *
1509       * @param string content to render (by reference)
1510       * @param array renderer codes to use for opt-out, opt-in and lazy
1511       * @param array params must contain the 'object_type' ( Item or Comment ) and the 'object_Blog'
1512       * @return mixed string rendered content on success | false on failure
1513       */
1514  	function filter_contents( & $title, & $content, $renderers, & $params )
1515      {
1516          $params = array_merge( array(
1517                  'event' => 'FilterItemContents',
1518                  'title' => & $title,
1519                  'content' => & $content,
1520              ), $params
1521          );
1522  
1523          return $this->process_event_filtering( $renderers, $params );
1524      }
1525  
1526  
1527      /**
1528       * UnFilter (post) contents by calling the relevant filter plugins.
1529       *
1530       * This is the opposite of filter_content. It is used to restore some specifcs before editing text.
1531       * For example, this can be used to replace complex sequences of tags with a custome meta-tag,
1532       * e-g: <strong> can become <s> for convenient editing.
1533       *
1534       * This uses the list of renderers, because un/filtering may need to work in conjunction with rendering,
1535       * e-g: code display: you want to filter in/out tags before validation and later you want to render color/fixed font.
1536       * For brute force unfiltering, use 'always' or 'stealth' modes.
1537       * @see Plugins::render()
1538       * @see Plugins::filter()
1539       *
1540       * @todo fp> it would probably make sense to do the unfiltering in reverse order compared to filtering
1541       *
1542       * @param string title to render (by reference)
1543       * @param string content to render (by reference)
1544       * @param array renderer codes to use for opt-out, opt-in and lazy
1545       * @param array params must contain the 'object_type' ( Item or Comment ) and the 'object_Blog'
1546       * @return mixed string rendered content on success | false on failure
1547       */
1548  	function unfilter_contents( & $title, & $content, $renderers, & $params )
1549      {
1550          $params = array_merge( array(
1551                  'event' => 'UnfilterItemContents',
1552                  'title' => & $title,
1553                  'content' => & $content,
1554              ), $params
1555          );
1556  
1557          // fp> TODO: reverse order
1558          return $this->process_event_filtering( $renderers, $params );
1559      }
1560  
1561  }
1562  
1563  ?>

title

Description

title

Description

title

Description

title

title

Body