<?php
/**
 * This file implements the PluginS class.
 *
 * This is where you can plug in some {@link Plugin plugins} :D
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


// DEBUG: (Turn switch on or off to log debug info for specified category)
$GLOBALS['debug_plugins'] = false;


load_class( 'plugins/_plugin.class.php', 'Plugin' );


/**
 * Plugins Class
 *
 * This is where you can plug in some {@link Plugin plugins} :D
 *
 * @todo dh> Currently when a plugin goes into "broken" status (e.g. file not readable), it is "disabled" afterwards.
 *       This should rather remember the old status (e.g. "enabled") and make it enabled again.
 *
 * @package plugins
 */
class Plugins
{
	/**#@+
	 * @access private
	 */

	/**
	 * @var array of plugin_code => Plugin
	 */
	var $index_code_Plugins = array();

	/**
	 * @var array of plugin_ID => Plugin
	 */
	var $index_ID_Plugins = array();

	/**
	 * @see Plugins::load_events()
	 * @var array of event => plug_ID. IDs are sorted by priority.
	 */
	var $index_event_IDs = array();

	/**
	 * @var array of plug_ID => DB row from T_plugins. Used to lazy-instantiate Plugins.
	 */
	var $index_ID_rows = array();

	/**
	 * @var array of plug_code => plug_ID. Used to lazy-instantiate by code.
	 */
	var $index_code_ID = array();

	/**
	 * Cache Plugin codes by apply_rendering setting.
	 * @var array of apply_rendering => plug_code
	 */
	var $index_apply_rendering_codes = array();

	/**
	 * Cache all plugin codes by blogs apply_comment_rendering setting.
	 * @var array of blog_ID => array of plugin codes
	 */
	var $coll_apply_comment_rendering = array();

	/**
	 * Path to plugins.
	 *
	 * The preferred method is to have a sub-directory for each plugin (named
	 * after the plugin's classname), but they can be supplied just in this
	 * directory.
	 */
	var $plugins_path;

	/**
	 * Have we loaded the plugins table (T_plugins)?
	 * @var boolean
	 */
	var $loaded_plugins_table = false;

	/**
	 * Current object index in {@link $sorted_IDs} array.
	 * @var integer
	 */
	var $current_idx = -1;

	/**
	 * List of IDs, sorted. This gets used to lazy-instantiate a Plugin.
	 *
	 * @var array
	 */
	var $sorted_IDs = array();

	/**
	 * The smallest internal/auto-generated Plugin ID.
	 * @var integer
	 */
	var $smallest_internal_ID = 0;

	/**#@-*/


	/**#@+
	 * @access protected
	 */

	/**
	 * SQL to use in {@link load_plugins_table()}. Gets overwritten for {@link Plugins_admin}.
	 * @var string
	 * @static
	 */
	var $sql_load_plugins_table = '
			SELECT plug_ID, plug_priority, plug_classname, plug_code, plug_name, plug_shortdesc, plug_status, plug_version, plug_spam_weight
			  FROM T_plugins
			 WHERE plug_status = \'enabled\'
			 ORDER BY plug_priority, plug_classname';

	/**#@-*/


	/**
	 * Errors associated to plugins (during loading), indexed by plugin_ID and
	 * error class ("register").
	 *
	 * @var array
	 */
	var $plugin_errors = array();


	/**
	 * Constructor. Sets {@link $plugins_path} and load events.
	 */
	function __construct()
	{
		global $basepath, $plugins_subdir, $Timer;

		// Set plugin path:
		$this->plugins_path = $basepath.$plugins_subdir;

		$Timer->resume( 'plugins_init' );

		// Load events for enabled plugins:
		$this->load_events();

		$Timer->pause( 'plugins_init' );
	}


	/**
	 * Get a list of available Plugin groups.
	 *
	 * @return array
	 */
	function get_plugin_groups()
	{
		$result = array();

		foreach( $this->sorted_IDs as $plugin_ID )
		{
			$Plugin = & $this->get_by_ID( $plugin_ID );

			if( empty($Plugin->group) || in_array( $Plugin->group, $result ) )
			{
				continue;
			}

			$result[] = $Plugin->group;
		}

		return $result;
	}


	/**
	 * Will return an array that contents are references to plugins that have the same group, regardless of the sub_group.
	 *
	 * @return array
	 */
	function get_Plugins_in_group( $group )
	{
		$result = array();

		foreach( $this->sorted_IDs as $plugin_ID )
		{
			$Plugin = & $this->get_by_ID( $plugin_ID );
			if( $Plugin->group == $group )
			{
				$result[] = & $Plugin;
			}
		}

		return $result;
	}


	/**
	 * Will return an array that contents are references to plugins that have the same group and sub_group.
	 *
	 * @return array
	 */
	function get_Plugins_in_sub_group( $group, $sub_group = '' )
	{
		$result = array();

		foreach( $this->sorted_IDs as $plugin_ID )
		{
			$Plugin = & $this->get_by_ID( $plugin_ID );
			if( $Plugin->group == $group && $Plugin->sub_group == $sub_group )
			{
				$result[] = & $Plugin;
			}
		}

		return $result;
	}


	/**
	 * Sets the status of a Plugin in DB and registers it into the internal indices when "enabled".
	 * Otherwise it gets unregistered, but only when we're not in {@link Plugins_admin}, because we
	 * want to keep it in then in our indices.
	 *
	 * {@internal
	 * Note: this should probably always get called on the {@link $Plugins} object,
	 *       not {@link $admin_Plugins}.
	 * }}
	 *
	 * @param Plugin
	 * @param string New status ("enabled", "disabled", "needs_config", "broken")
	 * @return boolean True on success, false on failure.
	 */
	function set_Plugin_status( & $Plugin, $status )
	{
		global $DB, $Debuglog;

		if( $Plugin->status == $status
			&& $DB->query( "SELECT plug_status FROM T_plugins WHERE plug_ID = ".$DB->quote($Plugin->ID), 'set_Plugin_status safety net' ) == $status )
		{
			return true;
		}

		$DB->query( "UPDATE T_plugins SET plug_status = '".$status."' WHERE plug_ID = '".$Plugin->ID."'" );

		if( $status == 'enabled' )
		{ // Reload plugins tables, which includes the plugin in further requests
			$this->loaded_plugins_table = false;
			$this->load_plugins_table();
			$this->load_events();
		}
		else
		{
			// Notify the plugin that it has been disabled:
			$Plugin->BeforeDisable();

			$this->unregister( $Plugin );
		}

		$Plugin->status = $status;

		$Debuglog->add( 'Set status for plugin #'.$Plugin->ID.' to "'.$status.'"!', 'plugins' );

		return true;
	}


	/**
	 * Get path of a given Plugin classname.
	 *
	 * @param string Classname
	 * @return string Path
	 */
	function get_classfile_path($classname)
	{
		$plugin_filename = '_'.str_replace( '_plugin', '.plugin', $classname ).'.php';
		// Try <plug_classname>/<plug_classname>.php (subfolder) first
		$classfile_path = $this->plugins_path.$classname.'/'.$plugin_filename;

		if( ! is_readable( $classfile_path ) )
		{ // Look directly in $plugins_path
			$classfile_path = $this->plugins_path.$plugin_filename;
		}
		return $classfile_path;
	}


	/**
	 * Register a plugin.
	 *
	 * This handles the indexes, dynamically unregisters a Plugin that does not exist (anymore)
	 * and instantiates the Plugin's (User)Settings.
	 *
	 * @access protected
	 * @param string name of plugin class to instantiate and register
	 * @param int ID in database (0 if not installed)
	 * @param int Priority in database (-1 to keep default)
	 * @param string Path of the .php class file of the plugin.
	 * @param boolean Must the plugin exist (classfile_path and classname)?
	 *                This is used internally to be able to unregister a non-existing plugin.
	 * @return Plugin Plugin ref to newly created plugin; string in case of error
	 */
	function & register( $classname, $ID = 0, $priority = -1, $classfile_path = NULL, $must_exists = true )
	{
		global $Debuglog, $Messages, $Timer;

		if( $ID && isset($this->index_ID_Plugins[$ID]) )
		{
			debug_die( 'Tried to register already registered Plugin (ID '.$ID.')' ); // should never happen!
		}

		$Timer->resume( 'plugins_register' );

		if( empty($classfile_path) )
		{
			$classfile_path = $this->get_classfile_path($classname);
		}

		$Debuglog->add( 'register(): '.$classname.', ID: '.$ID.', priority: '.$priority.', classfile_path: ['.$classfile_path.']', 'plugins' );

		if( ! empty( $this->log_register ) )
		{	// Display additional log on upgrade page when we reload all plugins:
			global $plugins_path;
			echo get_install_format_text_and_log( '- Reloading "'.$classname.'" from <code>'.substr( $classfile_path, strlen( $plugins_path ) ).'</code><br />' );
			evo_flush();
		}

		if( ! is_readable( $classfile_path ) )
		{ // Plugin file not found!
			if( $must_exists )
			{
				$r = 'Plugin class file ['.rel_path_to_base($classfile_path).'] is not readable!';
				$Debuglog->add( $r, array( 'plugins', 'error' ) );

				// Get the Plugin object (must not exist)
				$Plugin = & $this->register( $classname, $ID, $priority, $classfile_path, false );
				$this->plugin_errors[$ID]['register'] = $r;
				$this->set_Plugin_status( $Plugin, 'broken' );

				// unregister:
				if( $this->unregister( $Plugin ) )
				{
					$Debuglog->add( 'Unregistered plugin ['.$classname.']!', array( 'plugins', 'error' ) );
				}
				else
				{
					$Plugin->name = $Plugin->classname; // use the classname instead of "unnamed plugin"
					$Timer->pause( 'plugins_register' );
					return $Plugin;
				}

				$Timer->pause( 'plugins_register' );
				return $r;
			}
		}
		elseif( ! class_exists($classname) ) // If there are several copies of one plugin for example..
		{
			$Debuglog->add( 'Loading plugin class file: '.$classname, 'plugins' );
			require_once $classfile_path;
		}

		if( ! class_exists( $classname ) )
		{ // the given class does not exist
			if( $must_exists )
			{
				$r = sprintf( 'Plugin class for &laquo;%s&raquo; in file &laquo;%s&raquo; not defined.', $classname, rel_path_to_base($classfile_path) );
				$Debuglog->add( $r, array( 'plugins', 'error' ) );

				// Get the Plugin object (must not exist)    fp> why is this recursive?
				$Plugin = & $this->register( $classname, $ID, $priority, $classfile_path, false );
				$this->plugin_errors[$ID]['register'] = $r;
				$this->set_Plugin_status( $Plugin, 'broken' );

				// unregister:
				if( $this->unregister( $Plugin ) )
				{
					$Debuglog->add( 'Unregistered plugin ['.$classname.']!', array( 'plugins', 'error' ) );
				}
				else
				{
					$Plugin->name = $Plugin->classname; // use the classname instead of "unnamed plugin"
					$Timer->pause( 'plugins_register' );
					return $Plugin;
				}

				$Timer->pause( 'plugins_register' );
				return $r;
			}
			else
			{
				$Plugin = new Plugin;	// COPY !
				$Plugin->code = NULL;
			}
		}
		else
		{
			$Plugin = new $classname;	// COPY !
		}

		$Plugin->classfile_path = $classfile_path;

		// Tell him his ID :)
		if( $ID == 0 )
		{
			$Plugin->ID = --$this->smallest_internal_ID;
		}
		else
		{
			$Plugin->ID = $ID;

			if( $ID > 0 )
			{ // Properties from T_plugins
				// Code
				$Plugin->code = $this->index_ID_rows[$Plugin->ID]['plug_code'];
				// Status
				$Plugin->status = $this->index_ID_rows[$Plugin->ID]['plug_status'];
			}
		}
		// Tell him his name :)
		$Plugin->classname = $classname;
		// Tell him his priority:
		if( $priority > -1 ) { $Plugin->priority = $priority; }

		if( empty($Plugin->name) )
		{
			$Plugin->name = $Plugin->classname;
		}

		// Memorizes Plugin in code hash array:
		if( ! empty($this->index_code_ID[ $Plugin->code ]) && $this->index_code_ID[ $Plugin->code ] != $Plugin->ID )
		{ // The plugin's default code is already in use!
			$Plugin->code = NULL;
		}
		else
		{
			$this->index_code_Plugins[ $Plugin->code ] = & $Plugin;
			$this->index_code_ID[ $Plugin->code ] = & $Plugin->ID;
		}
		$this->index_ID_Plugins[ $Plugin->ID ] = & $Plugin;

		if( ! in_array( $Plugin->ID, $this->sorted_IDs ) ) // TODO: check if this extra check is required..
		{ // not in our sort index yet
			$this->sorted_IDs[] = & $Plugin->ID;
		}

		// Init the Plugins (User)Settings members.
		$this->init_settings( $Plugin );

		// Stuff only for real/existing Plugins (which exist in DB):
		if( $Plugin->ID > 0 )
		{
			$tmp_params = array( 'db_row' => $this->index_ID_rows[$Plugin->ID], 'is_installed' => true );
			if( $Plugin->PluginInit( $tmp_params ) === false && $this->unregister( $Plugin ) )
			{
				$Debuglog->add( 'Unregistered plugin, because PluginInit returned false.', 'plugins' );
				$Plugin = '';
			}
			// Version check:
			elseif( $Plugin->version != $this->index_ID_rows[$Plugin->ID]['plug_version'] && $must_exists )
			{ // Version has changed since installation or last update
				$db_deltas = array();

				// Tell the Plugin that we've detected a version change:
				$tmp_params = array( 'old_version'=>$this->index_ID_rows[$Plugin->ID]['plug_version'], 'db_row'=>$this->index_ID_rows[$Plugin->ID] );

				if( $this->call_method( $Plugin->ID, 'PluginVersionChanged', $tmp_params ) === false )
				{
					$Debuglog->add( 'Set plugin status to "needs_config", because PluginVersionChanged returned false.', 'plugins' );
					$this->set_Plugin_status( $Plugin, 'needs_config' );
					if( $this->unregister( $Plugin ) )
					{ // only unregister the Plugin, if it's not the admin list's class:
						$Plugin = '';
					}
				}
				else
				{
					// Check if there are DB deltas required (also when downgrading!), without excluding any query type:
					load_funcs('_core/model/db/_upgrade.funcs.php');
					$db_deltas = db_delta( $Plugin->GetDbLayout() );

					if( empty($db_deltas) )
					{ // No DB changes needed, update (bump or decrease) the version
						global $DB;
						$Plugins_admin = & get_Plugins_admin();

						// Update version in DB:
						$DB->query( '
								UPDATE T_plugins
								   SET plug_version = '.$DB->quote($Plugin->version).'
								 WHERE plug_ID = '.$Plugin->ID );

						// Update "plug_version" in indexes:
						$this->index_ID_rows[$Plugin->ID]['plug_version'] = $Plugin->version;
						if( isset($Plugins_admin->index_ID_rows[$Plugin->ID]) )
						{
							$Plugins_admin->index_ID_rows[$Plugin->ID]['plug_version'] = $Plugin->version;
						}

						// Remove any prerendered content for the Plugins renderer code:
						if( ! empty($Plugin->code) )
						{
							$DB->query( '
									DELETE FROM T_items__prerendering
									 WHERE itpr_renderers REGEXP "^(.*\.)?'.$DB->escape($Plugin->code).'(\..*)?$"' );
						}

						// Detect new events (and delete obsolete ones - in case of downgrade):
						if( $Plugins_admin->save_events( $Plugin, array() ) )
						{
							$this->load_events(); // re-load for the current request
						}

						$Debuglog->add( 'Version for '.$Plugin->classname.' changed from '.$this->index_ID_rows[$Plugin->ID]['plug_version'].' to '.$Plugin->version, 'plugins' );
					}
					else
					{ // If there are DB schema changes needed, set the Plugin status to "needs_config"

						// TODO: automatic upgrade in some cases (e.g. according to query types)?

						$this->set_Plugin_status( $Plugin, 'needs_config' );
						$Debuglog->add( 'Set plugin status to "needs_config", because version DB schema needs upgrade.', 'plugins' );

						if( $this->unregister( $Plugin ) )
						{ // only unregister the Plugin, if it's not the admin list's class:
							$Plugin = '';
						}
					}
				}
			}

			if( $Plugin && isset($this->index_ID_rows[$Plugin->ID]) ) // may have been unregistered above
			{
				if( $this->index_ID_rows[$Plugin->ID]['plug_name'] !== NULL )
				{
					$Plugin->name = $this->index_ID_rows[$Plugin->ID]['plug_name'];
				}
				if( $this->index_ID_rows[$Plugin->ID]['plug_shortdesc'] !== NULL )
				{
					$Plugin->short_desc = $this->index_ID_rows[$Plugin->ID]['plug_shortdesc'];
				}
			}
		}
		else
		{ // This gets called for non-installed Plugins:
			// We're not instantiating UserSettings/Settings, since that needs a DB backend.

			$tmp_params = array( 'db_row' => array(), 'is_installed' => false );
			if( $Plugin->PluginInit( $tmp_params ) === false && $this->unregister( $Plugin ) )
			{
				$Debuglog->add( 'Unregistered plugin, because PluginInit returned false.', 'plugins' );
				$Plugin = '';
			}
		}

		$Timer->pause( 'plugins_register' );

		return $Plugin;
	}


	/**
	 * Un-register a plugin.
	 *
	 * This does not un-install it from DB, just from the internal indexes.
	 *
	 * @param Plugin
	 * @param boolean Force unregistering (ignored here, but used in Plugins_admin)
	 * @return boolean True, if unregistered
	 */
	function unregister( & $Plugin, $force = false )
	{
		global $Debuglog;

		$this->forget_events( $Plugin->ID );

		// TODO: Check if it is necessarry to unset from the $index_apply_rendering_codes and unset if it must be removed
		unset( $this->index_code_Plugins[ $Plugin->code ] );
		unset( $this->index_ID_Plugins[ $Plugin->ID ] );

		if( isset($this->index_ID_rows[ $Plugin->ID ]) )
		{ // It has an associated DB row (load_plugins_table() was called)
			unset($this->index_ID_rows[ $Plugin->ID ]);
		}

		$sort_key = array_search( $Plugin->ID, $this->sorted_IDs );
		if( $sort_key === false )
		{ // this may happen if a Plugin has unregistered itself
			$Debuglog->add( 'Tried to unregister not-installed plugin (not in $sorted_IDs)!', 'plugins' );
			return false;
		}
		unset( $this->sorted_IDs[$sort_key] );
		$this->sorted_IDs = array_values( $this->sorted_IDs );

		if( $this->current_idx >= $sort_key )
		{ // We have removed a file before or at the $sort_key'th position
			$this->current_idx--;
		}

		return true;
	}


	/**
	 * Forget the events a Plugin has registered.
	 *
	 * This gets used when {@link unregister() unregistering} a Plugin or if
	 * {@link Plugin::PluginInit()} returned false, which means
	 * "do not use it for subsequent events in the request".
	 *
	 * @param integer Plugin ID
	 */
	function forget_events( $plugin_ID )
	{
		// Forget events:
		foreach( array_keys($this->index_event_IDs) as $l_event )
		{
			while( ($key = array_search( $plugin_ID, $this->index_event_IDs[$l_event] )) !== false )
			{
				unset( $this->index_event_IDs[$l_event][$key] );
			}
		}
	}


	/**
	 * Init {@link Plugin::$Settings} and {@link Plugin::$UserSettings}, either by
	 * unsetting them for PHP5's overloading or instantiating them for PHP4.
	 *
	 * This only gets used for installed plugins, but we're initing it for PHP 5.1,
	 * to trigger a helpful error, when e.g. Plugin::Settings gets accessed in PluginInit().
	 *
	 * @param Plugin
	 */
	function init_settings( & $Plugin )
	{
		// we use overloading for PHP5, therefor the member has to be unset:
		// Note: this is somehow buggy at least in PHP 5.0.5, therefor we use it from 5.1 on.
		//       see http://forums.b2evolution.net/viewtopic.php?p=49031#49031
		unset( $Plugin->Settings );
		unset( $Plugin->UserSettings );
		unset( $Plugin->GroupSettings );

		// Nothing to do here, will get called through Plugin::__get() when accessed
		return;
	}


	/**
	 * Instantiate Settings object (class {@link PluginSettings}) for the given plugin.
	 *
	 * The plugin must provide setting definitions (through {@link Plugin::GetDefaultSettings()}
	 * OR {@link Plugin::GetDefaultUserSettings()}).
	 *
	 * @todo dh> there should be only one instance of Plugin(User)Settings, which would allow to
	 *           query the DB at once.
	 *           Defaults would need to be handled by Plugin(User)Settings::get_default() then.
	 *
	 * @param Plugin
	 * @param string settings type: "Settings", "UserSettings" or "GroupSettings"
	 * @return boolean NULL, if no Settings
	 */
	function instantiate_Settings( & $Plugin, $set_type )
	{
		global $Debuglog, $Timer, $Blog;

		$Timer->resume( 'plugins_inst_'.$set_type );

		// Call Plugin::GetDefaultSettings() or Plugin::GetDefaultUserSettings():
		// This does not use call_method, since the plugin may not be accessible through get_by_ID().
		// This may happen, if Plugin(User)Settings get accessed in PluginInit (which is allowed
		// when is_installed=true).
		$method = 'GetDefault'.$set_type;
		$params = array('for_editing'=>false);
		$defaults = $Plugin->$method( $params );

		if( $set_type == 'Settings' )
		{	// If general settings are requested we should also append custom, collection, widgets, messages, emails and shared settings:
			if( ! is_array( $defaults ) )
			{	// Initialize array for default settings:
				$defaults = array();
			}

			// Initialize object for Settings temporary because it may be used in the functions below to get default values:
			load_class( 'plugins/model/_pluginsettings.class.php', 'PluginSettings' );
			$Plugin->Settings = new PluginSettings( $Plugin->ID );

			$defaults = array_merge( $defaults, $Plugin->get_custom_setting_definitions( $params ) );
			$defaults = array_merge( $defaults, $Plugin->get_msg_setting_definitions( $params ) );
			$defaults = array_merge( $defaults, $Plugin->get_email_setting_definitions( $params ) );
			$defaults = array_merge( $defaults, $Plugin->get_shared_setting_definitions( $params ) );

			// Check what other settings are defined for the Plugin,
			// We should not merge them with $defaults because they are stored in different DB table,
			// I.e. they are should be initialized in $Plugin->Settings, but we still need this object for a proper settings work:
			if( isset( $Blog ) )
			{	// Only when current collection is defined in order to avoid errors because global $Blog may be used inside:
				$other_defaults = $Plugin->get_coll_setting_definitions( $params );
				$other_defaults = array_merge( $other_defaults, $Plugin->get_widget_param_definitions( $params ) );
			}
		}

		if( empty( $defaults ) && empty( $other_defaults ) )
		{	// No settings, no need to instantiate:
			$Timer->pause( 'plugins_inst_'.$set_type );
			return NULL;
		}

		if( ! is_array( $defaults ) )
		{	// Invalid format of default settings:
			$Debuglog->add( $Plugin->classname.'::GetDefault'.$set_type.'() did not return array!', array('plugins', 'error') );
			return NULL; // fp> correct me if I'm wrong.
		}

		if( $set_type == 'UserSettings' )
		{ // User specific settings:
			load_class( 'plugins/model/_pluginusersettings.class.php', 'PluginUserSettings' );

			$Plugin->UserSettings = new PluginUserSettings( $Plugin->ID );

			$set_Obj = & $Plugin->UserSettings;
		}
		elseif( $set_type == 'GroupSettings' )
		{	// Group specific settings:
			load_class( 'plugins/model/_plugingroupsettings.class.php', 'PluginGroupSettings' );

			$Plugin->GroupSettings = new PluginGroupSettings( $Plugin->ID );

			$set_Obj = & $Plugin->GroupSettings;
		}
		else
		{ // Global settings:
			load_class( 'plugins/model/_pluginsettings.class.php', 'PluginSettings' );

			$Plugin->Settings = new PluginSettings( $Plugin->ID );

			$set_Obj = & $Plugin->Settings;
		}

		// Register default values:
		foreach( $defaults as $l_name => $l_meta )
		{
			if( isset($l_meta['layout']) )
			{ // Skip non-value entries
				continue;
			}

			// Register settings as _defaults into Settings:
			if( isset($l_meta['defaultvalue']) )
			{
				$set_Obj->_defaults[$l_name] = $l_meta['defaultvalue'];
			}
			elseif( isset( $l_meta['type'] ) && strpos( $l_meta['type'], 'array' ) === 0 )
			{
				$set_Obj->_defaults[$l_name] = array();
			}
			elseif( isset( $l_meta['type'] ) && $l_meta['type'] == 'checklist' )
			{
				$set_Obj->_defaults[$l_name] = NULL;
			}
			elseif( isset( $l_meta['type'] ) && $l_meta['type'] == 'input_group' && is_array( $l_meta['inputs'] ) )
			{	// Get default values from input group fields:
				foreach( $l_meta['inputs'] as $l_meta_input_key => $l_meta_input_data )
				{
					if( isset( $l_meta_input_data['defaultvalue'] ) )
					{
						$set_Obj->_defaults[ $l_name.$l_meta_input_key ] = $l_meta_input_data['defaultvalue'];
					}
				}
			}
			else
			{
				$set_Obj->_defaults[$l_name] = '';
			}
		}

		$Timer->pause( 'plugins_inst_'.$set_type );

		return true;
	}


	/**
	 * Load plugins table and rewind iterator used by {@link get_next()}.
	 */
	function restart()
	{
		$this->load_plugins_table();

		$this->current_idx = -1;
	}


	/**
	 * Get next plugin in the list.
	 *
	 * NOTE: You'll have to call {@link restart()} or {@link load_plugins_table()}
	 * before using it.
	 *
	 * @return Plugin (false if no more plugin).
	 */
	function & get_next()
	{
		global $Debuglog;

		++$this->current_idx;

		$Debuglog->add( 'get_next() ('.$this->current_idx.')..', 'plugins' );

		if( isset($this->sorted_IDs[$this->current_idx]) )
		{
			$Plugin = & $this->get_by_ID( $this->sorted_IDs[$this->current_idx] );

			if( ! $Plugin )
			{ // recurse until we've been through whole $sorted_IDs!
				return $this->get_next();
			}

			$Debuglog->add( 'return: '.$Plugin->classname.' ('.$Plugin->ID.')', 'plugins' );
			return $Plugin;
		}
		else
		{
			$Debuglog->add( 'return: false', 'plugins' );
			--$this->current_idx;
			$r = false;
			return $r;
		}
	}


	/**
	 * Stop propagation of events to next plugins in {@link trigger_event()}.
	 */
	function stop_propagation()
	{
		$this->_stop_propagation = true;
	}


	/**
	 * Call all plugins for a given event.
	 *
	 * @param string event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @return boolean True, if at least one plugin has been called.
	 */
	function trigger_event( $event, $params = array() )
	{
		global $Debuglog;

		$Debuglog->add( 'Trigger event '.$event, 'plugins' );

		if( empty($this->index_event_IDs[$event]) )
		{ // No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
			return false;
		}

		$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[$event]), 'plugins' );

		foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
		{
			$this->call_method( $l_plugin_ID, $event, $params );

			if( ! empty($this->_stop_propagation) )
			{	// A plugin has requested to stop propagation.
				$this->_stop_propagation = false;
				break;
			}
		}
		return true;
	}


	/**
	 * Call all plugins for a given event, until the first one returns true.
	 *
	 * @param string event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @return array The (modified) params array with key "plugin_ID" set to the last called plugin;
	 *               Empty array if no Plugin returned true or no Plugin has this event registered.
	 */
	function trigger_event_first_true( $event, $params = NULL )
	{
		global $Debuglog;

		$Debuglog->add( 'Trigger event '.$event.' (first true)', 'plugins' );

		if( empty($this->index_event_IDs[$event]) )
		{ // No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
// DON'T RETURN HERE BECAUSE OF DIRTY HACK!!!
		}
		else
		{	// We have some events registered, loop through them:
			$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[$event]), 'plugins' );
			foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
			{
				$r = $this->call_method( $l_plugin_ID, $event, $params );
				if( $r === true )
				{
					$Debuglog->add( 'Plugin ID '.$l_plugin_ID.' returned true!', 'plugins' );
					// Save the ID of the plugin which returned true:
					$params['plugin_ID'] = & $l_plugin_ID;
					return $params;
				}
			}
		}
		return array();
	}


	/**
	 * Call all plugins for a given event, until the first one returns true.
	 *
	 * @param string event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @return array The (modified) params array with key "plugin_ID" set to the last called plugin;
	 *               Empty array if no Plugin returned true or no Plugin has this event registered.
	 */
	function trigger_event_first_true_with_params( $event, & $params )
	{
		global $Debuglog;

		$Debuglog->add( 'Trigger event '.$event.' (first true)', 'plugins' );

		if( empty( $this->index_event_IDs[ $event ] ) )
		{	// No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
// DON'T RETURN HERE BECAUSE OF DIRTY HACK!!!
		}
		else
		{	// We have some events registered, loop through them:
			$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[ $event ] ), 'plugins' );
			foreach( $this->index_event_IDs[ $event ] as $l_plugin_ID )
			{
				$r = $this->call_method( $l_plugin_ID, $event, $params );
				if( $r === true )
				{
					$Debuglog->add( 'Plugin ID '.$l_plugin_ID.' returned true!', 'plugins' );
					// Save the ID of the plugin which returned true:
					$params['plugin_ID'] = & $l_plugin_ID;
					return $params;
				}
			}
		}

		return array();
	}


	/**
	 * Call all plugins for a given event, until the first one returns false.
	 *
	 * @param string event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @return array The (modified) params array with key "plugin_ID" set to the last called plugin;
	 *               Empty array if no Plugin returned true or no Plugin has this event registered.
	 */
	function trigger_event_first_false( $event, $params = NULL )
	{
		global $Debuglog;

		$Debuglog->add( 'Trigger event '.$event.' (first false)', 'plugins' );

		if( empty($this->index_event_IDs[$event]) )
		{ // No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
			return array();
		}

		$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[$event]), 'plugins' );
		foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
		{
			$r = $this->call_method( $l_plugin_ID, $event, $params );
			if( $r === false )
			{
				$Debuglog->add( 'Plugin ID '.$l_plugin_ID.' returned false!', 'plugins' );
				$params['plugin_ID'] = & $l_plugin_ID;
				return $params;
			}
		}
		return array();
	}


	/**
	 * Call all plugins for a given event, until the first one returns a value
	 * (not NULL) (and $search is fulfilled, if given).
	 *
	 * @param string event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array|NULL Associative array of parameters for the Plugin
	 * @param array|NULL If provided, the return value gets checks against this criteria.
	 *        Can be:
	 *         - ( 'in_array' => 'needle' )
	 * @return array The (modified) params array with key "plugin_ID" set to the last called plugin
	 *               and 'plugin_return' set to the return value;
	 *               Empty array if no Plugin returned true or no Plugin has this event registered.
	 */
	function trigger_event_first_return( $event, $params = NULL, $search = NULL )
	{
		global $Debuglog;

		$Debuglog->add( 'Trigger event '.$event.' (first return)', 'plugins' );

		if( empty($this->index_event_IDs[$event]) )
		{ // No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
			return array();
		}

		$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[$event]), 'plugins' );
		foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
		{
			$r = $this->call_method( $l_plugin_ID, $event, $params );
			if( isset($r) )
			{
				if( isset($search) )
				{ // Apply $search:
					foreach( $search as $k => $v )
					{ // Check search criterias and continue if it does not match:
						switch( $k )
						{
							case 'in_array':
								if( ! in_array( $v, $r ) )
								{
									continue 3; // continue in main foreach loop
								}
								break;
							default:
								debug_die('Invalid search criteria in Plugins::trigger_event_first_return / '.$k);
						}
					}
				}
				$Debuglog->add( 'Plugin ID '.$l_plugin_ID.' returned '.( $r ? 'true' : 'false' ).'!', 'plugins' );
				$params['plugin_return'] = $r;
				$params['plugin_ID'] = & $l_plugin_ID;
				return $params;
			}
		}
		return array();
	}


	/**
	 * Trigger an event and return an index of params.
	 *
	 * This is handy to collect return values from all plugins hooking an event.
	 *
	 * @param string Event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @param string Index of $params that should get returned
	 * @return mixed The requested index of $params
	 */
	function get_trigger_event( $event, $params = NULL, $get = 'data' )
	{
		$params[$get] = & $params[$get]; // make it a reference, so it can get changed

		$this->trigger_event( $event, $params );

		return $params[$get];
	}


	/**
	 * The same as {@link get_trigger_event()}, but stop when the first Plugin returns true.
	 *
	 * @param string Event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @param string Index of $params that should get returned
	 * @return mixed The requested index of $params
	 */
	function get_trigger_event_first_true( $event, $params = NULL, $get = 'data' )
	{
		$params[$get] = & $params[$get]; // make it a reference, so it can get changed

		$this->trigger_event_first_true( $event, $params );

		return $params[$get];
	}


	/**
	 * Trigger an event and return the first return value of a plugin.
	 *
	 * @param string Event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @return mixed NULL if no Plugin returned something or the return value of the first Plugin
	 */
	function get_trigger_event_first_return( $event, $params = NULL )
	{
		$r = $this->trigger_event_first_return( $event, $params );

		if( ! isset($r['plugin_return']) )
		{
			return NULL;
		}

		return $r['plugin_return'];
	}


	/**
	 * Trigger an event and return an array of all return values of the
	 * relevant plugins.
	 *
	 * @param string Event name, see {@link Plugins_admin::get_supported_events()}
	 * @param array Associative array of parameters for the Plugin
	 * @param boolean Ignore {@link empty() empty} return values?
	 * @return array List of return values, indexed by Plugin ID
	 */
	function trigger_collect( $event, $params = NULL, $ignore_empty = true )
	{
		if( empty($this->index_event_IDs[$event]) )
		{
			return array();
		}

		$r = array();
		foreach( $this->index_event_IDs[$event] as $p_ID )
		{
			$sub_r = $this->call_method_if_active( $p_ID, $event, $params );

			if( $ignore_empty && empty($sub_r) )
			{
				continue;
			}

			$r[$p_ID] = $sub_r;
		}

		return $r;
	}


	/**
	 * Trigger a karma collecting event in order to get Karma percentage.
	 *
	 * @param string Event
	 * @param array Params to the event
	 * @return integer|NULL Spam Karma (-100 - 100); "100" means "absolutely spam"; NULL if no plugin gave us a karma value
	 */
	function trigger_karma_collect( $event, $params )
	{
		global $Debuglog;

		$karma_abs = NULL;
		$karma_divider = 0; // total of the "spam detection relevance weight"

		$Debuglog->add( 'Trigger karma collect event '.$event, 'plugins' );

		if( empty($this->index_event_IDs[$event]) )
		{ // No events registered
			$Debuglog->add( 'No registered plugins.', 'plugins' );
			return NULL;
		}

		$this->load_plugins_table(); // We need index_ID_rows below

		$Debuglog->add( 'Registered plugin IDs: '.implode( ', ', $this->index_event_IDs[$event]), 'plugins' );

		$count_plugins = 0;
		foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
		{
			$plugin_weight = $this->index_ID_rows[$l_plugin_ID]['plug_spam_weight'];

			if( $plugin_weight < 1 )
			{
				$Debuglog->add( 'Skipping plugin #'.$l_plugin_ID.', because is has weight '.$plugin_weight.'.', 'plugins' );
				continue;
			}

			$params['cur_karma'] = ( $karma_divider ? round($karma_abs / $karma_divider) : NULL );
			$params['cur_karma_abs'] = $karma_abs;
			$params['cur_karma_divider'] = $karma_divider;
			$params['cur_count_plugins'] = $count_plugins;

			// Call the plugin:
			$plugin_karma = $this->call_method( $l_plugin_ID, $event, $params );

			if( ! is_numeric( $plugin_karma ) )
			{
				continue;
			}

			$count_plugins++;

			if( $plugin_karma > 100 )
			{
				$plugin_karma = 100;
			}
			elseif( $plugin_karma < -100 )
			{
				$plugin_karma = -100;
			}

			$karma_abs += ( $plugin_karma * $plugin_weight );
			$karma_divider += $plugin_weight;

			if( ! empty($this->_stop_propagation) )
			{
				$this->_stop_propagation = false;
				break;
			}
		}

		if( ! $karma_divider )
		{
			return NULL;
		}

		$karma = round($karma_abs / $karma_divider);

		if( $karma > 100 )
		{
			$karma = 100;
		}
		elseif( $karma < -100 )
		{
			$karma = -100;
		}

		return $karma;
	}


	/**
	 * Call a method on a Plugin.
	 *
	 * This makes sure that the Timer for the Plugin gets resumed.
	 *
	 * @param integer Plugin ID
	 * @param string Method name.
	 * @param array Params (by reference).
	 * @return NULL|mixed Return value of the plugin's method call or NULL if no such method.
	 */
	function call_method( $plugin_ID, $method, & $params )
	{
		global $Timer, $debug, $Debuglog;

		$Plugin = & $this->get_by_ID( $plugin_ID );

		if( ! method_exists( $Plugin, $method ) )
		{
			return NULL;
		}

		if( $debug )
		{
			/*
			// Note: this is commented out, because $debug_params gets not dumped anymore (last line of this block)
			// Hide passwords from Debuglog!
			// Clone/copy (references!):
			$debug_params = array();
			foreach( $params as $k => $v )
			{
				$debug_params[$k] = $v;
			}
			if( isset($debug_params['pass']) )
			{
				$debug_params['pass'] = '-hidden-';
			}
			if( isset($debug_params['pass_md5']) )
			{
				$debug_params['pass_md5'] = '-hidden-';
			}
			$Debuglog->add( 'Calling '.$Plugin->classname.'(#'.$Plugin->ID.')->'.$method.'( '.htmlspecialchars(var_export( $debug_params, true )).' )', 'plugins' );
			*/
			$Debuglog->add( 'Calling '.$Plugin->classname.'(#'.$Plugin->ID.')->'.$method.'( )', 'plugins' );
		}

		if( $method == 'CacheObjects' )
		{	// Deny plugins with deprecated event:
			debug_die( 'The plugin event CacheObjects is deprecated' );
		}

		$Timer->resume( $Plugin->classname.'_(#'.$Plugin->ID.')' );
		$r = $Plugin->$method( $params );
		$Timer->pause( $Plugin->classname.'_(#'.$Plugin->ID.')' );

		return $r;
	}


	/**
	 * Call a method on a Plugin if it is not deactivated.
	 *
	 * This is a wrapper around {@link call_method()}.
	 *
	 * fp> why doesn't call_method always check if it's deactivated?
	 *
	 * @param integer Plugin ID
	 * @param string Method name.
	 * @param array Params (by reference).
	 * @return NULL|mixed Return value of the plugin's method call or NULL if no such method (or inactive).
	 */
	function call_method_if_active( $plugin_ID, $method, & $params )
	{
		if( ! $this->has_event($plugin_ID, $method) )
		{
			return NULL;
		}

		return $this->call_method( $plugin_ID, $method, $params );
	}


	/**
	 * Call a specific plugin by its code.
	 *
	 * This will call the {@link Plugin::SkinTag()} event handler.
	 *
	 * @param string plugin code
	 * @param array Associative array of parameters (gets passed to the plugin)
	 * @return boolean
	 */
	function call_by_code( $code, $params = array() )
	{
		$Plugin = & $this->get_by_code( $code );

		if( ! $Plugin )
		{
			global $Debuglog;
			$Debuglog->add( 'No plugin available for code ['.$code.']!', array('plugins', 'error') );
			return false;
		}

		$this->call_method_if_active( $Plugin->ID, 'SkinTag', $params );

		return true;
	}


	/**
	 * Render the content of an item, Comment, Message, Widget by calling the relevant renderer plugins.
	 *
	 * @param string content to render (by reference)
	 * @param array renderer codes to use for opt-out, opt-in and lazy
	 * @param string Output format, see {@link format_to_output()}. Only 'htmlbody',
	 *        'entityencoded', 'xml', 'htmlfeed' and 'text' are supported.
	 * @param array Additional params to the Render* methods (e.g. "Item" for items).
	 *              Do not use "data" or "format" here, because it gets used internally.
	 * @param string Prefix of render function: 'Render' - for rendering at save in DB time, 'Display' - for rendering at display on screen time
	 * @return string rendered content
	 */
	function render( & $content, $renderers, $format, $params, $event_prefix = 'Render' )
	{
		$params['data'] = & $content;
		$params['format'] = $format;

		if( in_array( $format, array('htmlbody','htmlfeed','entityencoded') ) )
		{
			$event = $event_prefix.'ItemAsHtml'; // 'RenderItemAsHtml'/'DisplayItemAsHtml'
		}
		elseif( $format == 'xml' )
		{
			$event = $event_prefix.'ItemAsXml'; // 'RenderItemAsXml'/'DisplayItemAsXml'
		}
		elseif( $format == 'text' )
		{
			$event = $event_prefix.'ItemAsText'; // 'RenderItemAsText'/'DisplayItemAsText'
		}
		else debug_die( 'Unexpected $format passed to Plugins::render(): '.var_export($format, true) );

		$renderer_Plugins = $this->get_list_by_event( $event );
		// Set blog from where the "apply_rendering" collection setting should be get
		if( isset( $params['Item'] ) && !empty( $params['Item'] ) )
		{ // the Item is set, get Item Blog
			$Item = & $params['Item'];
			$setting_Blog = & $Item->get_Blog();
		}
		else
		{ // use global Blog if it is set
			global $Collection, $Blog;
			if( !empty( $Blog ) )
			{
				$setting_Blog = $Blog;
			}
			else
			{	// Try to get Blog by global blog ID
				global $blog;
				if( !empty( $blog ) )
				{
					$BlogCache = & get_BlogCache();
					$setting_Blog = & $BlogCache->get_by_ID( $blog );
				}
				else
				{
					$setting_Blog = NULL;
				}
			}
		}

		if( empty( $setting_Blog ) )
		{ // This should be impossible, but make sure $setting_Blog is set
			$setting_Blog = & get_setting_Blog( 'default_blog_ID' );
		}

		// Collect all renderer plugins used in current page in order to know when we really need to load their JS/CSS files:
		global $evo_renderers_used_in_current_page;
		if( ! is_array( $evo_renderers_used_in_current_page ) )
		{	// Initialize array for curently used renderer plugins once:
			$evo_renderers_used_in_current_page = array();
		}
		$evo_renderers_used_in_current_page += $renderers;

		foreach( $renderer_Plugins as $loop_RendererPlugin )
		{ // Go through whole list of renders
			$apply_rendering_value = $loop_RendererPlugin->get_coll_setting( 'coll_apply_rendering', $setting_Blog );
			if( $loop_RendererPlugin->is_renderer_enabled( $apply_rendering_value, $renderers ) )
			{ // Plugin is enabled to call method
				$this->call_method( $loop_RendererPlugin->ID, $event, $params );
			}
		}

		return $content;
	}


	/**
	 * Quick-render a string with a single plugin and format it for output.
	 *
	 * @todo rename
	 *
	 * @param string Plugin code (must have render() method)
	 * @param array
	 *   'data': Data to render
	 *   'format: format to output, see {@link format_to_output()}
	 * @return string Rendered string
	 */
	function quick( $plugin_code, $params )
	{
		global $Debuglog;

		if( !is_array($params) )
		{
			$params = array( 'format' => 'htmlbody', 'data' => $params );
		}
		else
		{
			$params = $params; // copy
		}

		$Plugin = & $this->get_by_code( $plugin_code );
		if( $Plugin )
		{
			// Get the most appropriate handler:
			$events = $this->get_enabled_events( $Plugin->ID );
			$event = false;
			if( $params['format'] == 'htmlbody' || $params['format'] == 'htmlentityencoded' )
			{
				if( in_array( 'RenderItemAsHtml', $events ) )
				{
					$event = 'RenderItemAsHtml';
				}
			}
			elseif( $params['format'] == 'xml' )
			{
				if( in_array( 'RenderItemAsXml', $events ) )
				{
					$event = 'RenderItemAsXml';
				}
			}

			if( $event )
			{
				$this->call_method( $Plugin->ID, $event, $params );
			}
			else
			{
				$Debuglog->add( $Plugin->classname.'(ID '.$Plugin->ID.'): failed to quick-render (tried method '.$event.')!', array( 'plugins', 'error' ) );
			}
			return format_to_output( $params['data'], $params['format'] );
		}
		else
		{
			$Debuglog->add( 'Plugins::quick() - failed to instantiate Plugin by code ['.$plugin_code.']!', array( 'plugins', 'error' ) );
			return format_to_output( $params['data'], $params['format'] );
		}
	}


	/**
	 * Load Plugins data from T_plugins (only once), ordered by priority.
	 *
	 * This fills the needed indexes to lazy-instantiate a Plugin when requested.
	 */
	function load_plugins_table()
	{
		if( $this->loaded_plugins_table )
		{
			return;
		}
		global $Debuglog, $DB;

		$Debuglog->add( 'Loading plugins table data.', 'plugins' );

		$this->index_ID_rows = array();
		$this->index_code_ID = array();
		$this->sorted_IDs = array();

		foreach( $DB->get_results( $this->sql_load_plugins_table, ARRAY_A, 'Loading plugins table data' ) as $row )
		{ // Loop through installed plugins:
			$this->index_ID_rows[$row['plug_ID']] = $row; // remember the rows to instantiate the Plugin on request
			if( ! empty( $row['plug_code'] ) )
			{
				$this->index_code_ID[$row['plug_code']] = $row['plug_ID'];
			}

			$this->sorted_IDs[] = $row['plug_ID'];
		}

		$this->loaded_plugins_table = true;
	}


	/**
	 * Load Plugin data from T_plugins by Class name
	 *
	 * This fills the needed indexes to lazy-instantiate a Plugin when requested.
	 *
	 * @param string Class name
	 */
	function load_plugin_by_classname( $classname )
	{
		global $Debuglog, $DB;

		$Debuglog->add( sprintf( 'Loading plugin %s by class name.', $classname ), 'plugins' );

		$SQL = new SQL( 'Load Plugin data by class name' );
		$SQL->SELECT( 'plug_ID, plug_priority, plug_classname, plug_code, plug_name, plug_shortdesc, plug_status, plug_version, plug_spam_weight' );
		$SQL->FROM( 'T_plugins' );
		$SQL->WHERE( 'plug_classname = '.$DB->quote( $classname ) );
		if( $plugin = $DB->get_row( $SQL, ARRAY_A ) )
		{
			if( isset( $this->index_ID_rows[$plugin['plug_ID']] ) )
			{ // Plugin already was loaded before
				return;
			}

			$this->index_ID_rows[$plugin['plug_ID']] = $plugin; // remember the rows to instantiate the Plugin on request
			if( ! empty( $plugin['plug_code'] ) )
			{
				$this->index_code_ID[$plugin['plug_code']] = $plugin['plug_ID'];
			}

			$this->sorted_IDs[] = $plugin['plug_ID'];

			// Load the events of this plugin
			$this->load_events_by_classname( $classname );
		}
	}


	/**
	 * Load rendering Plugins specific apply_rendering setting for the given Blog and setting_name
	 *
	 * @param String setting name ( 'coll_apply_rendering', 'coll_apply_comment_rendering' )
	 * @param Object the Blog which apply rendering setting should be loaded
	 * @param string Setting type: 'coll', 'msg', 'email', 'shared'
	 */
	function load_index_apply_rendering( $setting_name, & $Blog, $type = 'coll' )
	{
		if( is_null( $Blog ) )
		{ // Use general settings (e.g. for Messages)
			$blog_ID = 0;
		}
		else
		{ // Use collection settings (for Items and Comments)
			$blog_ID = $Blog->ID;
		}

		if( isset( $this->index_apply_rendering_codes[$blog_ID][$setting_name] ) )
		{ // This data is already loaded
			return;
		}

		// make sure plugins are loaded
		$this->load_plugins_table();

		$index = & $this->sorted_IDs;
		foreach( $index as $plugin_ID )
		{ // iterate through each plugin
			$Plugin = $this->get_by_ID( $plugin_ID );
			if( $Plugin->group != 'rendering' )
			{ // This plugin doesn't belong to the rendering plugins group
				continue;
			}
			switch( $type )
			{
				case 'coll':
					// Get plugin collection setting value:
					$rendering_value = $Plugin->get_coll_setting( $setting_name, $Blog );
					break;

				case 'msg':
					// Get plugin message setting value:
					$rendering_value = $Plugin->get_msg_setting( $setting_name );
					break;

				case 'email':
					// Get plugin email setting value:
					$rendering_value = $Plugin->get_email_setting( $setting_name );
					break;

				case 'shared':
					// Get plugin shared setting value:
					$rendering_value = 'opt-in';
					break;

				default:
					debug_die( 'Invalid plugin setting type!' );
					// EXIT HERE.
			}
			$this->index_apply_rendering_codes[$blog_ID][$setting_name][$rendering_value][] = $Plugin->code;
		}
	}


	/**
	 * Get a specific plugin by its ID.
	 *
	 * This is the workhorse when it comes to lazy-instantiating a Plugin.
	 *
	 * @param integer plugin ID
	 * @return Plugin (false in case of error)
	 */
	function & get_by_ID( $plugin_ID )
	{
		global $Debuglog;

		if( ! isset($this->index_ID_Plugins[ $plugin_ID ]) )
		{ // Plugin is not instantiated yet
			$Debuglog->add( 'get_by_ID(): Instantiate Plugin (ID '.$plugin_ID.').', 'plugins' );

			$this->load_plugins_table();

			#pre_dump( 'get_by_ID(), index_ID_rows', $this->index_ID_rows );

			if( ! isset( $this->index_ID_rows[$plugin_ID] ) || ! $this->index_ID_rows[$plugin_ID] )
			{ // no plugin rows cached
				#debug_die( 'Cannot instantiate Plugin (ID '.$plugin_ID.') without DB information.' );
				$Debuglog->add( 'get_by_ID(): Plugin (ID '.$plugin_ID.') not registered/enabled in DB!', array( 'plugins', 'error' ) );
				$r = false;
				return $r;
			}

			$row = & $this->index_ID_rows[$plugin_ID];

			// Register the plugin:
			$Plugin = & $this->register( $row['plug_classname'], $row['plug_ID'], $row['plug_priority'] );

			if( is_string( $Plugin ) )
			{
				$Debuglog->add( 'Requested plugin [#'.$plugin_ID.'] not found!', 'plugins' );
				$r = false;
				return $r;
			}

			$this->index_ID_Plugins[ $plugin_ID ] = & $Plugin;
		}

		return $this->index_ID_Plugins[ $plugin_ID ];
	}


	/**
	 * Get a plugin by its classname.
	 *
	 * @param string
	 * @return Plugin (false in case of error)
	 */
	function & get_by_classname( $classname )
	{
		$this->load_plugins_table(); // We use index_ID_rows (no own index yet)

		foreach( $this->index_ID_rows as $plug_ID => $row )
		{
			if( $row['plug_classname'] == $classname )
			{
				return $this->get_by_ID($plug_ID);
			}
		}

		$r = false;
		return $r;
	}


	/**
	 * Get a specific Plugin by its code.
	 *
	 * @param string plugin code
	 * @return Plugin (false in case of error)
	 */
	function & get_by_code( $plugin_code )
	{
		global $Debuglog;

		$r = false;

		if( ! isset($this->index_code_Plugins[ $plugin_code ]) )
		{ // Plugin is not registered yet
			$this->load_plugins_table();

			if( ! isset($this->index_code_ID[ $plugin_code ]) )
			{
				$Debuglog->add( 'Requested plugin ['.$plugin_code.'] is not registered/enabled!', 'plugins' );
				return $r;
			}

			if( ! $this->get_by_ID( $this->index_code_ID[$plugin_code] ) )
			{
				$Debuglog->add( 'Requested plugin ['.$plugin_code.'] could not get instantiated!', 'plugins' );
				return $r;
			}
		}

		return $this->index_code_Plugins[ $plugin_code ];
	}


	/**
	 * Get a list of Plugins for a given event.
	 *
	 * @param string Event name
	 * @return array plugin_ID => & Plugin
	 */
	function get_list_by_event( $event )
	{
		$r = array();

		if( isset($this->index_event_IDs[$event]) )
		{
			foreach( $this->index_event_IDs[$event] as $l_plugin_ID )
			{
				if( $Plugin = & $this->get_by_ID( $l_plugin_ID ) )
				{
					$r[ $l_plugin_ID ] = & $Plugin;
					unset($Plugin); // so that we do not overwrite the reference in the next loop
				}
			}
		}

		return $r;
	}


	/**
	 * Get a list of Plugins for a list of events. Every Plugin is only once in this list.
	 *
	 * @param array Array of events
	 * @return array plugin_ID => & Plugin
	 */
	function get_list_by_events( $events )
	{
		$r = array();

		foreach( $events as $l_event )
		{
			foreach( array_keys($this->get_list_by_event( $l_event )) as $l_plugin_ID )
			{
				if( $Plugin = & $this->get_by_ID( $l_plugin_ID ) )
				{
					$r[ $l_plugin_ID ] = & $Plugin;
					unset($Plugin); // so that we do not overwrite the reference in the next loop
				}
			}
		}

		return $r;
	}


	/**
	 * Get a list of plugins that provide all given events.
	 *
	 * @return array plugin_ID => & Plugin
	 */
	function get_list_by_all_events( $events )
	{
		$candidates = array();

		foreach( $events as $l_event )
		{
			if( empty($this->index_event_IDs[$l_event]) )
			{
				return array();
			}

			if( empty($candidates) )
			{
				$candidates = $this->index_event_IDs[$l_event];
			}
			else
			{
				$candidates = array_intersect( $candidates, $this->index_event_IDs[$l_event] );
				if( empty($candidates) )
				{
					return array();
				}
			}
		}

		$r = array();
		foreach( $candidates as $plugin_ID )
		{
			$Plugin = & $this->get_by_ID( $plugin_ID );
			if( $Plugin )
			{
				$r[ $plugin_ID ] = & $Plugin;
				unset($Plugin); // so that we do not overwrite the reference in the next loop
			}
		}

		return $r;
	}


	/**
	 * Get a list of (enabled) events for a given Plugin ID.
	 *
	 * @param integer Plugin ID
	 * @return array
	 */
	function get_enabled_events( $plugin_ID )
	{
		$r = array();
		foreach( $this->index_event_IDs as $l_event => $l_plugin_IDs )
		{
			if( in_array( $plugin_ID, $l_plugin_IDs ) )
			{
				$r[] = $l_event;
			}
		}
		return $r;
	}


	/**
	 * Has a plugin a specific event registered and enabled?
	 *
	 * @todo fp> The plugin should discover its events itself / This question should be asked to the Plugin itself.
	 *       dh> Well, "discover" means to look up if it has a given method; this does not appear to belong into the Plugin class.
	 *           A plugin won't also really know if some event is enabled or disabled.
	 *
	 * @param integer
	 * @param string
	 * @return boolean
	 */
	function has_event( $plugin_ID, $event )
	{
		return isset($this->index_event_IDs[$event])
			&& in_array( $plugin_ID, $this->index_event_IDs[$event] );
	}


	/**
	 * Check if the requested list of events is provided by any or one plugin.
	 *
	 * @param array|string A single event or a list thereof
	 * @param boolean Make sure there's at least one plugin that provides them all?
	 *                This is useful for event pairs like "RequestCaptcha" and "ValidateCaptcha", which
	 *                should be served by the same plugin.
	 * @return boolean
	 */
	function are_events_available( $events, $require_all_in_same_plugin = false )
	{
		if( ! is_array($events) )
		{
			$events = array($events);
		}

		if( $require_all_in_same_plugin )
		{
			return (bool)$this->get_list_by_all_events( $events );
		}

		return (bool)$this->get_list_by_events( $events );
	}


	/**
	 * (Re)load Plugin Events for enabled (normal use) or all (admin use) plugins.
	 */
	function load_events()
	{
		global $Debuglog, $DB;

		$this->index_event_IDs = array();

		$Debuglog->add( 'Loading plugin events.', 'plugins' );
		foreach( $DB->get_results( '
				SELECT pevt_plug_ID, pevt_event
					FROM T_pluginevents INNER JOIN T_plugins ON pevt_plug_ID = plug_ID
				 WHERE pevt_enabled > 0
				   AND plug_status = \'enabled\'
				 ORDER BY plug_priority, plug_classname', OBJECT, 'Loading plugin events' ) as $l_row )
		{
			$this->index_event_IDs[$l_row->pevt_event][] = $l_row->pevt_plug_ID;
		}
	}


	/**
	 * (Re)load Plugin Events plugin by classname.
	 *
	 * @param string Class name
	 */
	function load_events_by_classname( $classname )
	{
		global $Debuglog, $DB;

		$Debuglog->add( sprintf( 'Loading plugin events by classname "%s".', $classname ), 'plugins' );
		foreach( $DB->get_results( '
				SELECT pevt_plug_ID, pevt_event
					FROM T_pluginevents INNER JOIN T_plugins ON pevt_plug_ID = plug_ID
				 WHERE pevt_enabled > 0
				   AND plug_classname = '.$DB->quote( $classname ), OBJECT, sprintf( 'Loading plugin events by classname "%s"', $classname ) ) as $l_row )
		{
			$this->index_event_IDs[$l_row->pevt_event][] = $l_row->pevt_plug_ID;
		}
	}


	/**
	 * Load an object from a Cache plugin or create a new one if we have a
	 * cache miss or no caching plugins.
	 *
	 * It registers a shutdown function, that refreshes the data to the cache plugin
	 * which is not optimal, but we have no hook to see if data retrieved from
	 * a {@link DataObjectCache} derived class has changed.
	 * @param string object name
	 * @param string eval this to create the object. Default is to create an object
	 *               of class $objectName.
	 * @return boolean True, if retrieved from cache; false if not
	 */
	function get_object_from_cacheplugin_or_create( $objectName, $eval_create_object = NULL )
	{
		$get_return = $this->trigger_event_first_true( 'CacheObjects',
			array( 'action' => 'get', 'key' => 'object_'.$objectName ) );

		if( isset( $get_return['plugin_ID'] ) )
		{
			if( is_object($get_return['data']) )
			{
				$GLOBALS[$objectName] = $get_return['data']; // COPY! (get_Cache() uses $$objectName instead of $GLOBALS - no deal for PHP5 anyway)

				$Plugin = & $this->get_by_ID( $get_return['plugin_ID'] );
				register_shutdown_function( array(&$Plugin, 'CacheObjects'),
					array( 'action' => 'set', 'key' => 'object_'.$objectName, 'data' => & $GLOBALS[$objectName] ) );

				return true;
			}
		}

		// Cache miss, create it:
		if( empty($eval_create_object) )
		{
			$GLOBALS[$objectName] = new $objectName(); // COPY (FUNC)
		}
		else
		{
			eval( '$GLOBALS[\''.$objectName.'\'] = '.$eval_create_object.';' );
		}

		// Try to set in cache:
		$set_return = $this->trigger_event_first_true( 'CacheObjects',
			array( 'action' => 'set', 'key' => 'object_'.$objectName, 'data' => & $GLOBALS[$objectName] ) );

		if( isset( $set_return['plugin_ID'] ) )
		{ // success, register a shutdown function to save this data on shutdown
			$Plugin = & $this->get_by_ID( $set_return['plugin_ID'] );
			register_shutdown_function( array(&$Plugin, 'CacheObjects'),
				array( 'action' => 'set', 'key' => 'object_'.$objectName, 'data' => & $GLOBALS[$objectName] ) );
		}

		return false;
	}


	/**
	 * Callback, which gets used for {@link Results}.
	 *
	 * @return Plugin (false in case of error)
	 */
	function & instantiate( $row )
	{
		return $this->get_by_ID( $row->plug_ID );
	}


	/**
	 * Validate renderer list.
	 *
	 * @param array renderer codes ('default' will include all "opt-out"-ones)
	 * @params array params to set Blog and apply_rendering setting_name ( see {@link load_index_apply_rendering()} )
	 * @return array validated array of renderer codes
	 */
	function validate_renderer_list( $renderers = array('default'), $params )
	{
		// Init Blog and $setting_name from the given params
		if( isset( $params['Item'] ) )
		{ // Validate post renderers
			$Item = & $params['Item'];
			$Collection = $Blog = & $Item->get_Blog();
			$setting_name = 'coll_apply_rendering';
			$setting_type = 'coll';
		}
		elseif( isset( $params['Comment'] ) )
		{ // Validate comment renderers
			$Comment = & $params['Comment'];
			$Item = & $Comment->get_Item();
			$Collection = $Blog = & $Item->get_Blog();
			$setting_name = 'coll_apply_comment_rendering';
			$setting_type = 'coll';
		}
		elseif( isset( $params['Message'] ) )
		{ // Validate message renderers
			$Collection = $Blog = NULL;
			$setting_name = 'msg_apply_rendering';
			$setting_type = 'msg';
		}
		elseif( isset( $params['EmailCampaign'] ) )
		{	// Validate message renderers:
			$Collection = $Blog = NULL;
			$setting_name = 'email_apply_rendering';
			$setting_type = 'email';
		}
		elseif( isset( $params['setting_name'] ) )
		{	// Validate the given rendering option:
			$setting_name = $params['setting_name'];
			if( isset( $params['Blog'] ) )
			{	// If Collection is given:
				$Collection = $Blog = & $params['Blog'];
				$setting_type = 'coll';
				if( $setting_name == 'shared_apply_rendering' )
				{	// Force to posts/items rendering settings:
					$setting_name = 'coll_apply_rendering';
				}
				if( !in_array( $setting_name, array( 'coll_apply_rendering', 'coll_apply_comment_rendering', 'shared_apply_rendering' ) ) )
				{
					debug_die( 'Invalid apply rendering param name received!' );
				}
			}
			if( $setting_name == 'shared_apply_rendering' )
			{	// Set shared type instead of collection for this spec setting name:
				$Collection = $Blog = NULL;
				$setting_type = 'shared';
			}
		}
		else
		{ // Invalid params to validate renderers!
			return array();
		}

		$blog_ID = empty( $Blog ) ? 0 : $Blog->ID;

		// Make sure the requested apply_rendering settings are loaded
		$this->load_index_apply_rendering( $setting_name, $Blog, $setting_type );

		$validated_renderers = array();

		// Get requested apply_rendering setting array
		$index = & $this->index_apply_rendering_codes[$blog_ID][$setting_name];

		if( isset( $index['stealth'] ) )
		{
			// pre_dump( 'stealth:', $index['stealth'] );
			$validated_renderers = array_merge( $validated_renderers, $index['stealth'] );
		}
		if( isset( $index['always'] ) )
		{
			// pre_dump( 'always:', $index['always'] );
			$validated_renderers = array_merge( $validated_renderers, $index['always'] );
		}

		if( isset( $index['opt-out'] ) )
		{
			foreach( $index['opt-out'] as $l_code )
			{
				if( in_array( $l_code, $renderers ) // Option is activated
					|| in_array( 'default', $renderers ) ) // OR we're asking for default renderer set
				{
					// pre_dump( 'opt-out:', $l_code );
					$validated_renderers[] = $l_code;
				}
			}
		}

		if( isset( $index['opt-in'] ) )
		{
			foreach( $index['opt-in'] as $l_code )
			{
				if( in_array( $l_code, $renderers ) ) // Option is activated
				{
					// pre_dump( 'opt-in:', $l_code );
					$validated_renderers[] = $l_code;
				}
			}
		}
		if( isset( $index['lazy'] ) )
		{
			foreach( $index['lazy'] as $l_code )
			{
				if( in_array( $l_code, $renderers ) ) // Option is activated
				{
					// pre_dump( 'lazy:', $l_code );
					$validated_renderers[] = $l_code;
				}
			}
		}

		// Make sure there's no renderer code with a dot, as the list gets imploded by that when saved:
		foreach( $validated_renderers as $k => $l_code )
		{
			if( empty($l_code) || strpos( $l_code, '.' ) !== false )
			{
				unset( $validated_renderers[$k] );
			}
			/*
			dh> not required, now that the method has been moved back to Plugins (from ~_admin)
			else
			{ // remove the ones which are not enabled:
				$Plugin = & $this->get_by_code($l_code);
				if( ! $Plugin || $Plugin->status != 'enabled' )
				{
					unset( $validated_renderers[$k] );
				}
			}
			*/
		}

		// echo 'validated Renderers: '.count( $validated_renderers );
		return $validated_renderers;
	}


	/**
	 * Get renderers options which can be used for form selectors, checkboxes and etc.
	 *
	 * @param array If given, assume these renderers to be checked.
	 * @param array params from where to get 'apply_rendering' setting
	 */
	function get_renderer_options( $current_renderers = NULL, $params )
	{
		global $inc_path, $admin_url;

		load_funcs( 'plugins/_plugin.funcs.php' );

		$checkbox_options = array();

		$this->restart(); // make sure iterator is at start position

		if( ! is_array( $current_renderers ) )
		{
			$current_renderers = explode( '.', $current_renderers );
		}

		$setting_Blog = NULL;
		if( isset( $params['setting_name'] ) && $params['setting_name'] == 'msg_apply_rendering' )
		{ // get Message apply_rendering setting
			$setting_name = $params['setting_name'];
		}
		elseif( isset( $params['setting_name'] ) && $params['setting_name'] == 'email_apply_rendering' )
		{	// get EmailCampaign apply_rendering setting:
			$setting_name = $params['setting_name'];
		}
		elseif( isset( $params['setting_name'] ) && $params['setting_name'] == 'shared_apply_rendering' )
		{	// get apply_rendering setting for widget from sharaed container:
			$setting_name = $params['setting_name'];
		}
		elseif( isset( $params['Comment'] ) && !empty( $params['Comment'] ) )
		{ // get Comment apply_rendering setting
			$Comment = & $params['Comment'];
			$comment_Item = & $Comment->get_Item();
			$setting_Blog = & $comment_Item->get_Blog();
			$setting_name = 'coll_apply_comment_rendering';
		}
		elseif( isset( $params['Item'] ) )
		{ // get Post apply_rendering setting
			$setting_name = 'coll_apply_rendering';
			$Item = & $params['Item'];
			$setting_Blog = & $Item->get_Blog();
		}
		elseif( isset( $params['setting_name'] ) )
		{	// Get given setting:
			$setting_name = $params['setting_name'];
			if( ! empty( $params['Blog'] ) )
			{	// If Collection is given::
				$setting_Blog = & $params['Blog'];
				if( $setting_name == 'shared_apply_rendering' )
				{	// Force to posts/items rendering settings:
					$setting_name = 'coll_apply_rendering';
				}
			}
		}
		else
		{ // Invalid params
			return $checkbox_options;
		}

		switch( $setting_name )
		{
			case 'msg_apply_rendering':
				// Get Message renderer plugins
				$RendererPlugins = $this->get_list_by_events( array('FilterMsgContent') );
				break;

			case 'email_apply_rendering':
				// Get Email Campaign renderer plugins
				$RendererPlugins = $this->get_list_by_events( array('FilterEmailContent') );
				break;

			case 'coll_apply_comment_rendering':
				// Get Comment renderer plugins
				$RendererPlugins = $this->get_list_by_events( array('FilterCommentContent') );
				break;

			case 'shared_apply_rendering':
			case 'coll_apply_rendering':
			default:
				// Get Item renderer plugins
				$RendererPlugins = $this->get_list_by_events( array( 'RenderItemAsHtml', 'RenderItemAsXml', 'RenderItemAsText', 'DisplayItemAsHtml', 'DisplayItemAsXml', 'DisplayItemAsText' ) );
				break;
		}

		foreach( $RendererPlugins as $loop_RendererPlugin )
		{ // Go through whole list of renders
			// echo ' ',$loop_RendererPlugin->code;
			if( empty( $loop_RendererPlugin->code ) )
			{ // No unique code!
				continue;
			}
			if( empty( $setting_Blog ) && $setting_name != 'msg_apply_rendering' && $setting_name != 'email_apply_rendering' && $setting_name != 'shared_apply_rendering' )
			{ // If $setting_Blog is not set we can't get collection apply_rendering options
				continue;
			}

			if( $setting_name == 'msg_apply_rendering' )
			{ // get rendering setting from plugin message settings
				$apply_rendering = $loop_RendererPlugin->get_msg_setting( $setting_name );
			}
			elseif( $setting_name == 'email_apply_rendering' )
			{	// get rendering setting from plugin email settings:
				$apply_rendering = $loop_RendererPlugin->get_email_setting( $setting_name );
			}
			elseif( $setting_name == 'shared_apply_rendering' )
			{	// get rendering setting from plugin shared settings:
				$apply_rendering = $loop_RendererPlugin->get_shared_setting( $setting_name );
			}
			else
			{ // get rendering setting from plugin coll settings
				$apply_rendering = $loop_RendererPlugin->get_coll_setting( $setting_name, $setting_Blog );
			}

			$ignored_apply_rendering = array( 'stealth', 'never' );
			if( isset( $params['ignored_apply_rendering'] ) )
			{
				$ignored_apply_rendering = array_merge( $ignored_apply_rendering, $params['ignored_apply_rendering'] );
			}

			if( in_array( $apply_rendering, $ignored_apply_rendering )
				|| empty( $apply_rendering ) )
			{ // This is not an option.
				continue;
			}

			$checkbox_option = array(
					'code'       => $loop_RendererPlugin->code,
					'short_desc' => $loop_RendererPlugin->short_desc,
					'name'       => $loop_RendererPlugin->name,
					'checked'    => false,
					'disabled'   => false,
					'help_link'  => $loop_RendererPlugin->get_help_link( '$help_url' ),
				);

			switch( $apply_rendering )
			{
				case 'always':
					//$r .= ' checked="checked" disabled="disabled"';
					$checkbox_option['checked'] = true;
					$checkbox_option['disabled'] = true;
					break;

				case 'opt-out':
					if( in_array( $loop_RendererPlugin->code, $current_renderers ) // Option is activated
						|| in_array( 'default', $current_renderers ) ) // OR we're asking for default renderer set
					{
						$checkbox_option['checked'] = true;
					}
					break;

				case 'opt-in':
					if( in_array( $loop_RendererPlugin->code, $current_renderers ) ) // Option is activated
					{
						$checkbox_option['checked'] = true;
					}
					break;

				case 'lazy':
					if( in_array( $loop_RendererPlugin->code, $current_renderers ) ) // Option is activated
					{
						$checkbox_option['checked'] = true;
					}
					$checkbox_option['disabled'] = true;
					break;
			}

			$checkbox_options[] = $checkbox_option;
		}

		return $checkbox_options;
	}


	/**
	 * Get checkable list of renderers
	 *
	 * @param array If given, assume these renderers to be checked.
	 * @param array params from where to get 'apply_rendering' setting
	 */
	function get_renderer_checkboxes( $current_renderers = NULL, $params )
	{
		if( isset( $params['setting_name'] ) )
		{	// Use the defined setting name from params:
			$setting_name = $params['setting_name'];
		}
		elseif( ! empty( $params['Comment'] ) )
		{	// Use setting name for Comment:
			$setting_name = 'coll_apply_comment_rendering';
		}
		elseif( ! empty( $params['Item'] ) )
		{	// Use setting name for Item:
			$setting_name = 'coll_apply_rendering';
		}
		else
		{	// Invalid params, Exit here:
			return '';
		}

		$name_prefix = isset( $params['name_prefix'] ) ? $params['name_prefix'] : '';

		// Set different prefix if you use several toolbars on one page:
		$js_prefix = isset( $params['js_prefix'] ) ? $params['js_prefix'] : '';

		$r = '<input type="hidden" name="renderers_displayed" value="1" />';

		$renderer_checkbox_options = $this->get_renderer_options( $current_renderers, $params );
		foreach( $renderer_checkbox_options as $option )
		{
			$r .= '<div id="block_renderer_'.$option['code'].'">';

			// Checkbox:
			$r .= '<input type="checkbox" class="checkbox" name="'.$name_prefix.'renderers[]" value="'.$option['code'].'" id="'.$js_prefix.'renderer_'.$option['code'].'"';
			if( $option['checked'] )
			{	// Is checked:
				$r .= ' checked="checked"';
			}
			if( $option['disabled'] )
			{	// Is disabled:
				$r .= ' disabled="disabled"';
			}
			$r .= ' title="'.format_to_output( $option['short_desc'], 'formvalue' ).'"';
			if( ! empty( $js_prefix ) )
			{	// Set prefix, Used in JS code to disable/enable plugin toolbar:
				$r .= ' data-prefix="'.format_to_output( $js_prefix, 'formvalue' ).'"';
			}
			$r .= ' />';

			// Label:
			$r .= ' <label for="'.$js_prefix.'renderer_'.$option['code'].'"';
			$r .= ' title="'.format_to_output( $option['short_desc'], 'formvalue' ).'">';
			$r .= format_to_output( $option['name'] ).'</label>';

			// fp> TODO: the first thing we want here is a TINY javascript popup with the LONG desc. The links to readme and external help should be inside of the tiny popup.
			// fp> a javascript DHTML onhover help would be even better than the JS popup

			// External help link:
			$r .= ' '.$option['help_link'];

			$r .= "</div>\n";
		}

		if( empty( $renderer_checkbox_options ) )
		{
			if( is_admin_page() )
			{ // Display info about no renderer plugins only in backoffice
				if( check_user_perm( 'admin', 'normal' ) )
				{
					global $admin_url;
					switch( $setting_name )
					{
						case 'msg_apply_rendering':
							if( check_user_perm( 'perm_messaging', 'reply' ) && check_user_perm( 'options', 'edit' ) )
							{ // Check if current user can edit the messaging settings
								$settings_url = $admin_url.'?ctrl=msgsettings&amp;tab=renderers';
							}
							break;

						case 'email_apply_rendering':
							if( check_user_perm( 'perm_messaging', 'reply' ) && check_user_perm( 'options', 'edit' ) )
							{ // Check if current user can edit the email settings
								$settings_url = $admin_url.'?ctrl=email&amp;tab=settings&amp;tab3=renderers';
							}
							break;

						case 'shared_apply_rendering':
							if( check_user_perm( 'options', 'edit' ) )
							{	// Check if current user can edit the plugin settings for shared container:
								$settings_url = $admin_url.'?ctrl=plugins&amp;tab=shared';
							}
							break;

						case 'coll_apply_comment_rendering':
						case 'coll_apply_rendering':
						default:
							if( ! empty( $setting_Blog ) && check_user_perm( 'blog_properties', 'edit', false, $setting_Blog->ID ) )
							{ // Check if current user can edit the blog plugin settings
								$settings_url = $admin_url.'?ctrl=coll_settings&amp;tab=plugins&amp;blog='.$setting_Blog->ID;
							}
							break;
					}
				}

				if( ! empty( $settings_url ) )
				{ // Display a link to plugin settings only when current user has a permission to edit them
					$r .= '<a title="'.T_('Configure plugins').'" href="'.$settings_url.'"'.'>';
				}
				$r .= T_('No renderer plugins can be selected by the user.');
				if( ! empty( $settings_url ) )
				{
					$r .= '</a>';
				}
			}
			else
			{
				return '';
			}
		}

		return $r;
	}


	/**
	 * Display captcha
	 *
	 * @param array Associative array of parameters:
	 *   - 'Form':                   Form object
	 *   - 'form_type':              Form type
	 *   - 'form_position':          Current form position where this function is called
	 *   - 'captcha_info':           Info under the captcha field in note style
	 *   - 'captcha_info_anonymous': Captcha info for not logged in user when the plugin knows it will NOT ask for captcha in case of logged in users
	 */
	function display_captcha( $params = array() )
	{
		if( ! isset( $params['Form'] ) ||
		    ! isset( $params['form_type'] ) ||
		    ! isset( $params['form_position'] ) )
		{	// Exit here if the mandatory params are not defined:
			return;
		}

		$params = array_merge( array(
				'captcha_template_question' => '<span class="evo_captcha_question">$captcha_question$</span><br>',
				'captcha_template_answer'   => '<span class="evo_captcha_answer">$captcha_answer$</span><br>',
				// Default captcha info text(can be customized by plugin):
				'captcha_info' => T_('We ask for this in order to slow down spammers.')
													.'<br>'.T_('Sorry for the inconvenience.')
													.( is_logged_in() ? '' : '<br>'.T_('Please log in to avoid this antispam check.') ),
			), $params );

		$form_type = $params['form_type'];

		if( ! isset( $this->captcha_data ) )
		{	// Initialize array to cache captcha data per current page request:
			$this->captcha_data = array();
		}

		if( ! isset( $this->captcha_data[ $form_type ] ) )
		{	// Load once captcha data per form type and use this for all next calls of this function:
			$plugin_data = $this->trigger_event_first_return( 'RequestCaptcha', $params );
			$this->captcha_data[ $form_type ] = isset( $plugin_data['plugin_return'] ) ? $plugin_data['plugin_return'] : false;
		}

		$captcha_data = $this->captcha_data[ $form_type ];

		if( isset( $captcha_data['captcha_position'], $captcha_data['captcha_html'] ) &&
		    $captcha_data['captcha_position'] == $params['form_position'] )
		{	// Display captcha html code only for requested form type and position:
			$Form = & $params['Form'];
			if( ! isset( $params['form_use_fieldset'] ) || $params['form_use_fieldset'] )
			{	// Begin fieldset if it is required from skin file:
				$Form->begin_fieldset();
			}

			$Form->info_field( T_('Antispam'), $captcha_data['captcha_html'], array(
				'note'     => ( isset( $captcha_data['captcha_info'] ) ? $captcha_data['captcha_info'] : $params['captcha_info'] ),
				'required' => true,
			) );

			if( ! isset( $params['form_use_fieldset'] ) || $params['form_use_fieldset'] )
			{	// End fieldset if it is required from skin file:
				$Form->end_fieldset();
			}
		}
	}


	// Deprecated stubs: {{{

	/**
	 * @deprecated by Plugins_admin::count_regs()
	 */
	function count_regs( $classname )
	{
		global $Debuglog;
		$Debuglog->add('Call to deprecated method Plugins::count_regs()', 'deprecated');
		$Plugins_admin = & get_Plugins_admin();
		return $Plugins_admin->count_regs($classname);
	}


	/**
	 * Validate renderer list.
	 *
	 * @deprecated by Plugins::validate_renderer_list()
	 * @param array renderer codes ('default' will include all "opt-out"-ones)
	 * @return array validated array of renderer codes
	 */
	function validate_list( $renderers = array('default') )
	{
		global $Debuglog, $Plugins, $debug;
		$Debuglog->add('Call to deprecated method Plugins::validate_list()', 'deprecated');

		$error_msg = 'Error because of a deprecated method call Plugins::validate_list().';
		if( $debug == 0 )
		{
			$error_msg = $error_msg."<br />".'Enable debugging to see which incompatible plugin has triggered this error and uninstall the deprecated plugin.';
			$error_msg = $error_msg."<br />".get_manual_link( 'debugging', 'Check the manual about how to enable debugging.' );
		}
		else
		{
			$error_msg = $error_msg."<br />".'Check the debug info below which incompatible plugin has triggered this error and uninstall the deprecated plugin.';
		}
		$error_msg = $error_msg."<br />".get_manual_link( 'plugins', 'Check the manual about how to uninstall a plugin.' );
		debug_die( $error_msg );
		// Already exited here

		// Return an empty array, because this method call is not supported anymore
		return array();
	}


	// }}}
}

?>
