View Issue Details

IDProjectCategoryView StatusLast Update
0008514mantisbtotherpublic2008-04-19 04:10
Reporterjreese Assigned Tojreese  
PrioritynormalSeverityfeatureReproducibilityN/A
Status closedResolutionfixed 
Target Version1.2.0a1Fixed in Version1.2.0a1 
Summary0008514: Support a dynamic and lightweight plugin system
Description

Mantis would certainly benefit from having a pluggable interface for supporting new features and abilities. The implementation needs to be dynamic and lightweight, loading only what is absolutely necessary for a plugin to work, and without a large amount of overhead to manage and utilize the plugins.

I have implemented a very simple, but powerful, event-based plugin system that uses very little overhead for normal operations, and defers loading files until they are needed. With no plugins installed, there is nearly no overhead.

At the moment, the plugin system is in a stable state, looking for feedback from anyone willing to test the patch and post their thoughts and reactions. The patch includes two basic plugins: one is the example from the wiki page, the other is a debug plugin for assisting with plugin development.

Additional Information

http://www.mantisbt.org/wiki/doku.php/mantisbt:dynamic_plugin_requirements

TagsNo tags attached.
Attached Files
mantis-plugins-2007-10-28.patch (45,585 bytes)   
diff --git a/admin/install.php b/admin/install.php
index fff33c7..ac9a8d8 100644
--- a/admin/install.php
+++ b/admin/install.php
@@ -26,6 +26,7 @@
 	//@@@ put this somewhere
 	set_time_limit ( 0 ) ;
 	$g_skip_open_db = true;  # don't open the database in database_api.php
+	$g_plugins_disabled = true;
 	@require_once( dirname( dirname( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'core.php' );
 	$g_error_send_page_header = false; # bypass page headers in error handler
 
diff --git a/admin/schema.php b/admin/schema.php
index dc9c5ca..c4b78d4 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -361,4 +361,8 @@ $upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' )
 	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 $upgrade[] = Array('CreateIndexSQL', Array( 'idx_typeowner', config_get( 'mantis_tokens_table' ), 'type, owner' ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_plugin_table' ), "
+	basename		C(40)	NOTNULL PRIMARY,
+	enabled			L		NOTNULL DEFAULT '0'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 ?>
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index b6b79a3..1b480e7 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -1346,6 +1346,7 @@
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
 	$g_mantis_news_table					= '%db_table_prefix%_news%db_table_suffix%';
+	$g_mantis_plugin_table					= '%db_table_prefix%_plugin%db_table_suffix%';
 	$g_mantis_project_category_table		= '%db_table_prefix%_project_category%db_table_suffix%';
 	$g_mantis_project_file_table			= '%db_table_prefix%_project_file%db_table_suffix%';
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
@@ -1919,4 +1920,18 @@
 	
 	# The twitter account password.
 	$g_twitter_password = '';
+
+	#############################
+	# Plugin System
+	#############################
+
+	# enable/disable plugins
+	$g_plugins_enabled 	= ON;
+
+	# absolute path to plugin files.
+	$g_plugin_path		= $g_absolute_path . 'plugins' . DIRECTORY_SEPARATOR;
+
+	# management threshold.
+	$g_manage_plugin_threshold = ADMINISTRATOR;
+
 ?>
diff --git a/core.php b/core.php
index 815521b..5cc26bd 100644
--- a/core.php
+++ b/core.php
@@ -172,4 +172,11 @@
 	if ( !isset( $g_bypass_headers ) && !headers_sent() ) {
 		header( 'Content-type: text/html;charset=' . lang_get( 'charset' ) );
 	}
+
+	# Plugin initialization
+	require_once( $t_core_path.'plugin_api.php' );
+	require_once( $t_core_path.'events_inc.php' );
+	if ( !isset( $g_plugins_disabled ) ) {
+		plugin_init_all();
+	}
 ?>
diff --git a/core/constant_inc.php b/core/constant_inc.php
index b1a8cce..4084153 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -322,6 +322,10 @@
 	# ERROR_TOKEN_*
 	define( 'ERROR_TOKEN_NOT_FOUND', 2300 );
 
+	# ERROR_PLUGIN *
+	define( 'ERROR_PLUGIN_NOT_REGISTERED', 2400 );
+	define( 'ERROR_PLUGIN_PAGE_NOT_FOUND', 2401 );
+
 	# Status Legend Position
 	define( 'STATUS_LEGEND_POSITION_TOP',		1);
 	define( 'STATUS_LEGEND_POSITION_BOTTOM',	2);
@@ -414,4 +418,9 @@
 	define( 'SPONSORSHIP_REQUESTED',      1 );
 	define( 'SPONSORSHIP_PAID',           2 );
 
+	# Plugin events
+	define( 'EVENT_TYPE_DEFAULT',		0 );
+	define( 'EVENT_TYPE_EXECUTE',		1 );
+	define( 'EVENT_TYPE_OUTPUT',		2 );
+	define( 'EVENT_TYPE_PROCESS',		3 );
 ?>
diff --git a/core/events_inc.php b/core/events_inc.php
new file mode 100644
index 0000000..64005a6
--- /dev/null
+++ b/core/events_inc.php
@@ -0,0 +1,21 @@
+<?php
+
+# Declare supported plugin events
+plugin_declare_events( array(
+
+	# Events specific to plugins
+	'EVENT_PLUGIN_INIT' 		=> EVENT_TYPE_EXECUTE,
+
+	# Events for processing data
+	'EVENT_TEXT_GENERAL'		=> EVENT_TYPE_PROCESS,
+	'EVENT_TEXT_LINKS'			=> EVENT_TYPE_PROCESS,
+	'EVENT_TEXT_RSS'			=> EVENT_TYPE_PROCESS,
+	
+	# Events for layout additions
+	'EVENT_PAGE_HEAD' 			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_TOP' 			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_BOTTOM'			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_END'			=> EVENT_TYPE_OUTPUT,
+
+) );
+
diff --git a/core/html_api.php b/core/html_api.php
index cb50e51..c8bbc7b 100644
--- a/core/html_api.php
+++ b/core/html_api.php
@@ -283,6 +283,8 @@
 	# --------------------
 	# (7) End the <head> section
 	function html_head_end() {
+		plugin_event( 'EVENT_PAGE_HEAD' );
+
 		echo '</head>', "\n";
 	}
 
@@ -317,6 +319,8 @@
 			echo '<a href="http://www.mantisbt.org" title="Free Web Based Bug Tracker"><img border="0" width="242" height="102" alt="Mantis Bugtracker" src="images/mantis_logo.gif" /></a>';
 			echo '</div>';
 		}
+
+		plugin_event( 'EVENT_PAGE_TOP' );
 	}
 
 	# --------------------
@@ -389,6 +393,8 @@
 		if ( !is_blank( $t_page ) && file_exists( $t_page ) && !is_dir( $t_page ) ) {
 			include( $t_page );
 		}
+
+		plugin_event( 'EVENT_PAGE_BOTTOM' );
 	}
 
 	# --------------------
@@ -474,6 +480,8 @@
 	# --------------------
 	# (14) End the <body> section
 	function html_body_end() {
+		plugin_event( 'EVENT_PAGE_END' );
+
 		echo '</body>', "\n";
 	}
 
@@ -672,7 +680,8 @@
 		$t_manage_user_page 		= 'manage_user_page.php';
 		$t_manage_project_menu_page = 'manage_proj_page.php';
 		$t_manage_custom_field_page = 'manage_custom_field_page.php';
-		$t_manage_config_page = 'adm_config_report.php';
+		$t_manage_plugin_page		= 'manage_plugin_page.php';
+		$t_manage_config_page		= 'adm_config_report.php';
 		$t_manage_prof_menu_page    = 'manage_prof_menu_page.php';
 		# $t_documentation_page 		= 'documentation_page.php';
 
@@ -689,6 +698,9 @@
 			case $t_manage_config_page:
 				$t_manage_config_page = '';
 				break;
+			case $t_manage_plugin_page:
+				$t_manage_plugin_page = '';
+				break;
 			case $t_manage_prof_menu_page:
 				$t_manage_prof_menu_page = '';
 				break;
@@ -710,6 +722,9 @@
 		if ( access_has_global_level( config_get( 'manage_global_profile_threshold' ) ) ) {
 			print_bracket_link( $t_manage_prof_menu_page, lang_get( 'manage_global_profiles_link' ) );
 		}
+		if ( access_has_global_level( config_get( 'manage_plugin_threshold' ) ) ) {
+			print_bracket_link( $t_manage_plugin_page, lang_get( 'manage_plugin_link' ) );
+		}
 		if ( access_has_project_level( config_get( 'view_configuration_threshold' ) ) ) {
 			print_bracket_link( $t_manage_config_page, lang_get( 'manage_config_link' ) );
 		}
diff --git a/core/lang_api.php b/core/lang_api.php
index 69cedd4..c8a7ade 100644
--- a/core/lang_api.php
+++ b/core/lang_api.php
@@ -36,17 +36,25 @@
 	# ------------------
 	# Loads the specified language and stores it in $g_lang_strings,
 	# to be used by lang_get
-	function lang_load( $p_lang ) {
+	function lang_load( $p_lang, $p_dir=null ) {
 		global $g_lang_strings, $g_active_language;
 
 		$g_active_language  = $p_lang;
-		if ( isset( $g_lang_strings[ $p_lang ] ) ) {
+		if ( isset( $g_lang_strings[ $p_lang ] ) && is_null( $p_dir ) ) {
 			return;
 		}
 
-		$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+		$t_lang_dir = $p_dir;
+
+		if ( is_null( $t_lang_dir ) ) {
+			$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+			require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+		} else {
+			if ( is_file( $t_lang_dir . 'strings_' . $p_lang . '.txt' ) ) {
+				include_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+			}
+		}
 
-		require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
 
 		# Allow overriding strings declared in the language file.
 		# custom_strings_inc.php can use $g_active_language
@@ -231,6 +239,14 @@
 		if ( lang_exists( $p_string, $t_lang ) ) {
 			return $g_lang_strings[ $t_lang ][ $p_string];
 		} else {
+			$t_plugin_current = plugin_get_current();
+			if ( !is_null( $t_plugin_current ) ) {
+				lang_load( $t_lang, config_get( 'plugin_path' ).$t_plugin_current.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR );
+				if ( lang_exists( $p_string, $t_lang ) ) {
+					return $g_lang_strings[ $t_lang ][ $p_string];
+				}
+			}
+
 			if ( $t_lang == 'english' ) {
 				error_parameters( $p_string );
 				trigger_error( ERROR_LANG_STRING_NOT_FOUND, WARNING );
diff --git a/core/plugin_api.php b/core/plugin_api.php
new file mode 100644
index 0000000..4caf479
--- /dev/null
+++ b/core/plugin_api.php
@@ -0,0 +1,651 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin API
+ * Handles the initialisation, management, and execution of plugins.
+ *
+ * @package PluginAPI
+ */
+
+### Public API functions
+
+/**
+ * Determine if a given plugin basename has been registered.
+ * @return boolean True if registered
+ */
+function plugin_is_registered( $p_basename ) {
+	global $g_plugin_info;
+	return ( isset( $g_plugin_info[$p_basename] ) && !is_null( $g_plugin_info[$p_basename] ) );
+}
+
+/**
+ * Make sure a given plugin basename has been registered.
+ * Triggers ERROR_PLUGIN_NOT_REGISTERED otherwise.
+ */
+function plugin_ensure_registered( $p_basename ) {
+	if ( !plugin_is_registered( $p_basename ) ) {
+		trigger_error( ERROR_PLUGIN_NOT_REGISTERED, ERROR );
+	}
+}
+
+/**
+ * Get the currently executing plugin's basename.
+ * @return string Plugin basename
+ */
+function plugin_get_current() {
+	global $g_plugin_current;
+	return $g_plugin_current;
+}
+
+/**
+ * Set the currently executing plugin's basename.
+ * @param string Plugin basename
+ */
+function plugin_set_current( $p_basename ) {
+	global $g_plugin_current;
+	$g_plugin_current = $p_basename;
+}
+
+/**
+ * Get the information array registered by the given plugin.
+ * @param string Plugin name (defaults to current plugin)
+ * @return array Plugin info (null if unregistered)
+ */
+function plugin_info( $p_basename=null ) {
+	global $g_plugin_info, $g_plugin_current;
+
+	if ( is_null( $p_basename ) && isset( $g_plugin_current ) ) {
+		$p_basename = $g_plugin_current;
+	}
+
+	return ( isset( $g_plugin_info[$p_basename] ) ? $g_plugin_info[$p_basename] : null );
+}
+
+/**
+ * Get the URL to the plugin wrapper page.
+ * @param string Page name
+ * @param string Plugin basename (defaults to current plugin)
+ */
+function plugin_page( $p_page, $p_basename=null ) {
+	if ( is_null( $p_basename ) ) {
+		$p_basename = plugin_get_current();
+	}
+	return 'plugin.php?name='.$p_basename.'&action='.$p_page;
+}
+
+/**
+ * Register an event callback function.
+ * If an event does exist at the time of registration, it will be
+ * automatically declared with the given event type.
+ * @param string Event name
+ * @param string Callback function name
+ * @param int Event type (for unregistered events)
+ */
+function plugin_register( $p_event, $p_callback, $p_type=null ) {
+	global $g_plugin_current, $g_plugin_events;
+
+	if ( !isset( $g_plugin_current ) ) {
+		return;
+	}
+
+	if ( !isset( $g_plugin_events ) ) {
+		$g_plugin_events = array();
+	}
+
+	if ( !isset( $g_plugin_events[$p_event] ) ) {
+		plugin_declare_event( $p_event, $p_type );
+	}
+	
+	$g_plugin_events[$p_event]['callbacks'][$g_plugin_current] = $p_callback;
+}
+
+/**
+ * Declare a plugin event.
+ * If the event already exists, it will not be modified.
+ * @param string Event name
+ * @param int Event type
+ */
+function plugin_declare_event( $p_name, $p_type ) {
+	global $g_plugin_events;
+
+	if ( !isset( $g_plugin_events[$p_name] ) ) {
+		if ( is_null( $p_type ) ) {
+			$p_type = EVENT_TYPE_DEFAULT;
+		}
+
+		$g_plugin_events[$p_name] = array( 
+			'type' => $p_type, 
+			'callbacks' => array()
+		);
+	}
+}
+
+/**
+ * Convenience function for declaring multiple events.
+ * @param array Array of event name/type key/value pairs
+ */
+function plugin_declare_events( $p_events ) {
+	foreach ( $p_events as $p_name => $p_type ) {
+		plugin_declare_event( $p_name, $p_type );
+	}
+}
+
+/**
+ * Signal an event and plugin callbacks.
+ * @param string Event name
+ * @param multi Event parameters
+ * @param int Event type override
+ * @return multi Null if event undeclared, appropriate return value otherwise
+ */
+function plugin_event( $p_event, $p_params=null, $p_type=null ) {
+	global $g_plugin_events;
+
+	if ( !isset( $g_plugin_events[$p_event] ) ) {
+		return null;
+	}
+
+	if ( is_null( $p_type ) ) {
+		$t_type = $g_plugin_events[$p_event]['type'];
+	} else {
+		$t_type = $p_type;
+	}
+	$t_callbacks = $g_plugin_events[$p_event]['callbacks'];
+
+	switch ( $t_type ) {
+		case EVENT_TYPE_EXECUTE:
+			return plugin_event_execute( $p_event, $t_callbacks );
+
+		case EVENT_TYPE_OUTPUT:
+			return plugin_event_output( $p_event, $t_callbacks, $p_params );
+
+		case EVENT_TYPE_PROCESS:
+			return plugin_event_process( $p_event, $t_callbacks, $p_params );
+		
+		default:
+			return plugin_event_default( $p_event, $t_callbacks, $p_params );
+	}
+}
+
+/**
+ * Given a base table name for a plugin, add appropriate prefix and suffix.
+ * Convenience for plugin schema definitions.
+ * @param string Table name
+ * @return string Full table name
+ */
+function plugin_table( $p_name ) {
+	$t_current = plugin_get_current();
+	return config_get( 'db_table_prefix' ) .
+		'_plugin_' . $t_current . '_' . $p_name .
+		config_get( 'db_table_suffix' );
+}
+
+### Plugin management functions
+
+/**
+ * Include the appropriate script for a plugin.
+ * @param srting Plugin basename
+ * @param boolean Include events script
+ */
+function plugin_include( $p_basename, $p_include_events=false ) {
+	$t_path = config_get( 'plugin_path' );
+
+	$t_register_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'register.php';
+	if ( is_file( $t_register_file ) ) {
+		include_once( $t_register_file );
+	}
+
+	if ( $p_include_events ) {
+		$t_events_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'events.php';
+		if ( is_file( $t_events_file ) ) {
+			include_once( $t_events_file );
+		}
+	}
+}
+
+/**
+ * Get the script information from the script file or cache.
+ * @param string Plugin basename
+ * @return array Script information
+ */
+function plugin_get_info( $p_basename ) {
+	global $g_plugin_info;
+
+	if ( plugin_is_registered( $p_basename ) ) {
+		return $g_plugin_info[$p_basename];
+	}
+
+	$t_current_plugin = plugin_get_current();
+	plugin_set_current( $p_basename );
+
+	plugin_include( $p_basename );
+
+	$t_plugin = null;
+
+	$t_info_function = 'plugin_callback_'.$p_basename.'_info';
+	if ( function_exists( $t_info_function ) ) {
+		$t_plugin = $t_info_function();
+	}
+
+	plugin_set_current( $t_current_plugin );
+
+	return $t_plugin;
+}
+
+function plugin_get_schema( $p_basename ) {
+	$t_current_plugin = plugin_get_current();
+	plugin_set_current( $p_basename );
+
+	plugin_include( $p_basename );
+
+	$t_schema_function = 'plugin_callback_'.$p_basename.'_schema';
+	if ( !function_exists( $t_schema_function ) ) {
+		return null;
+	}
+	$t_schema = $t_schema_function();
+
+	plugin_set_current( $t_current_plugin );
+
+	if ( is_array( $t_schema ) ) {
+		return $t_schema;
+	}
+
+	return null;
+}
+
+/**
+ * List all installed plugins.
+ * @return array Installed plugins
+ */
+function plugin_get_installed() {
+	$t_plugin_table = config_get( 'mantis_plugin_table' );
+
+	$t_query = "SELECT * FROM $t_plugin_table";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array( 'mantis' => '1' );
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_basename = $t_row['basename'];
+		$t_plugins[$t_basename] = $t_row['enabled'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * List enabled plugins.
+ * @return array Enabled plugin basenames
+ */
+function plugin_get_enabled() {
+	$t_plugin_table = config_get( 'mantis_plugin_table' );
+
+	$t_query = "SELECT basename FROM $t_plugin_table WHERE enabled=1";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array();
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_plugins[] = $t_row['basename'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * Search the plugins directory for plugins.
+ * @return array Plugin basename/info key/value pairs.
+ */
+function plugin_find_all() {
+	$t_plugin_path = config_get( 'plugin_path' );
+	$t_plugins = array( 'mantis' => plugin_get_info( 'mantis' ) );
+
+	if ( $t_dir = opendir( $t_plugin_path ) ) {
+		while ( ($t_file = readdir( $t_dir )) !== false ) {
+			if ( '.' == $t_file || '..' == $t_file ) continue;
+			if ( is_dir( $t_plugin_path.$t_file ) ) {
+				$t_plugin_info = plugin_get_info( $t_file );
+				if ( !is_null( $t_plugin_info ) ) {
+					$t_plugins[$t_file] = $t_plugin_info;
+				}
+			}
+		}
+		closedir( $t_dir );
+	}
+	return $t_plugins;
+}
+
+/**
+ * Determine if a given plugin is installed.
+ * @param string Plugin basename
+ * @retrun boolean True if plugin is installed
+ */
+function plugin_is_installed( $p_basename ) {
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$c_basename 	= db_prepare_string( $p_basename );
+
+	$t_query = "SELECT COUNT(*) FROM $t_plugin_table WHERE basename=" . db_param(0);
+	$t_result = db_query_bound( $t_query, array( $c_basename ) );
+	return ( 0 < db_result( $t_result ) );
+}
+
+/**
+ * Install a plugin to the database.
+ * @param string Plugin basename
+ */
+function plugin_install( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	if ( plugin_is_installed( $p_basename ) ) {
+		return;
+	}
+
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "INSERT INTO $t_plugin_table ( basename, enabled )
+				VALUES ( ".db_param(0).", 1 )";
+	db_query_bound( $t_query, array( $c_basename ) );
+
+	if ( false === ( config_get( 'schema_plugin_'.$p_basename, false ) ) ) {
+		config_set( 'plugin_schema_'.$p_basename, -1 );
+	}
+	plugin_upgrade( $p_basename );
+}
+
+/**
+ * Determine if an installed plugin needs to upgrade its schema.
+ * @param string Plugin basename
+ * @return boolean True if plugin needs schema ugrades.
+ */
+function plugin_needs_upgrade( $p_basename ) {
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$t_plugin_schema = plugin_get_schema( $p_basename );
+	if ( is_null( $t_plugin_schema ) ) {
+		return false;
+	}
+
+	$t_plugin_schema_version = config_get( 'plugin_schema_'.$p_basename, -1 );
+
+	return ( $t_plugin_schema_version < count( $t_plugin_schema ) - 1 );
+}
+
+/**
+ * Upgrade an installed plugin's schema.
+ * @param string Plugin basename
+ * @return multi True if upgrade completed, null if problem
+ */
+function plugin_upgrade( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	$t_schema_version = config_get( 'plugin_schema_'.$p_basename, -1 );
+	$t_schema = plugin_get_schema( $p_basename );
+
+	global $g_db;
+	$t_dict = NewDataDictionary( $g_db );
+
+	$i = $t_schema_version + 1;
+	while ( $i < count( $t_schema ) ) {
+		$t_target = $t_schema[$i][1][0];
+
+		if ( $t_schema[$i][0] == 'InsertData' ) {
+			$t_sqlarray = call_user_func_array( $t_schema[$i][0], $t_schema[$i][1] );
+		} else if ( $t_schema[$i][0] == 'UpdateSQL' ) {
+			$t_sqlarray = array( $t_schema[$i][1] );
+			$t_target = $t_schema[$i][1];
+		} else {
+			$t_sqlarray = call_user_func_array( Array( $t_dict, $t_schema[$i][0] ), $t_schema[$i][1] );
+		}
+		$t_status = $t_dict->ExecuteSQLArray( $t_sqlarray );
+
+		if ( 2 == $t_status ) {
+			config_set( 'plugin_schema_'.$p_basename, $i );
+		} else {
+			return null;
+		}
+
+		$i++;
+	}
+
+	return true;
+}
+
+/**
+ * Uninstall a plugin from the database.
+ * @param string Plugin basename
+ */
+function plugin_uninstall( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	if ( !plugin_is_installed( $p_basename ) ) {
+		return;
+	}
+
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "DELETE FROM $t_plugin_table WHERE basename=" . db_param(0);
+	db_query_bound( $t_query, array( $c_basename ) );
+}
+
+### Core usage only.
+
+/**
+ * Initialize all enabled plugins.
+ * Post-signals EVENT_PLUGIN_INIT.
+ */
+function plugin_init_all() {
+	if ( OFF == config_get( 'plugins_enabled' ) ) {
+		return;
+	}
+
+	global $g_plugin_info;
+	if ( !isset( $g_plugin_info ) ) {
+		$g_plugin_info = array();
+	}
+
+	# Initial plugin for version dependencies
+	$g_plugin_info['mantis'] = array(
+		'name' => 'Mantis',
+		'description' => 'The Mantis bugtracker.',
+		'version' => MANTIS_VERSION,
+		'requires' => array(),
+		'author' => 'The Mantis Team',
+		'url' => 'http://mantisbt.org',
+	);
+
+	plugin_init_array( plugin_get_enabled() );
+
+	plugin_event( 'EVENT_PLUGIN_INIT' );
+}
+
+/**
+ * Recursive plugin initialization to handle dependencies.
+ * @param array Plugin basenames to initialize.
+ */
+function plugin_init_array( $p_plugins, $p_depth=0 ) {
+	global $g_plugin_current;
+
+	$t_plugins_retry = array();
+
+	foreach( $p_plugins as $t_basename ) {
+		$g_plugin_current = $t_basename;
+
+		if ( !plugin_init( $t_basename ) ) {
+			# Dependent plugin
+			$t_plugins_retry[] = $t_basename;
+		}
+	}
+
+	# Recurse on dependent plugins
+	if ( $p_depth < count( $p_plugins ) ) {
+		plugin_init_array( $t_plugins_retry, $p_depth + 1 );
+	}
+
+	unset( $g_plugin_current );
+}
+
+/**
+ * Initialize a single plugin.
+ * @param string Plugin basename
+ * @return boolean True if plugin initialized, false otherwise.
+ */
+function plugin_init( $p_basename ) {
+	global $g_plugin_info;
+
+	plugin_include( $p_basename );
+
+	$t_plugin = plugin_get_info( $p_basename );
+	if ( $t_plugin !== null ) {
+		$g_plugin_info[$p_basename] = $t_plugin;
+
+		# handle dependent plugins
+		if ( isset( $t_plugin['requires'] ) ) {
+			foreach ( $t_plugin['requires'] as $t_required => $t_version ) {
+				if ( !isset( $g_plugin_info[$t_required] ) ||
+					( !is_null( $t_version ) &&
+					$g_plugin_info[$t_required]['version'] < $t_version ) ) {
+					return false;
+				}
+			}
+		}
+
+		$t_register_function = 'plugin_callback_'.$p_basename.'_register';
+		if ( function_exists( $t_register_function ) ) {
+			$t_register_function();
+		}
+	}
+
+	return true;
+}
+
+### Event-handling functions
+
+/**
+ * Executes a plugin's callback function for a given event.
+ * @param string Event name
+ * @param string Plugin basename
+ * @param string Callback name
+ * @param multi Parameters for event callback
+ * @return multi Null if callback not found, value from callback otherwise
+ */
+function plugin_event_callback( $p_event, $p_basename, $p_callback, $p_params=null ) {
+	$t_plugin_current = plugin_get_current();
+
+	plugin_include( $p_basename, true );
+	$t_function = 'plugin_event_callback_'.$p_basename.'_'.$p_callback;
+
+	$t_value = null;
+	if ( function_exists( $t_function ) ) {
+		plugin_set_current( $p_basename );
+		$t_value = $t_function( $p_event, $p_params );
+		plugin_set_current( $t_plugin_current );
+	}
+
+	return $t_value;
+}
+
+/**
+ * Process an execute event type.
+ * All plugin callbacks will be called with no parameters, and their
+ * return values will be ignored.
+ * @param string Event name
+ * @param array Array of plugin basename/callback key/value pairs
+ */
+function plugin_event_execute( $p_event, $p_callbacks ) {
+	foreach( $p_callbacks as $t_basename => $t_callback ) {
+		plugin_event_callback( $p_event, $t_basename, $t_callback );
+	}
+}
+
+/**
+ * Process an output event type.
+ * All plugin callbacks will be called with the given parameters, and their
+ * return values will be echoed to the client, separated by a given string.
+ * If there are no callbacks, then nothing will be sent as output.
+ * @param string Event name
+ * @param array Array of plugin basename/callback key/value pairs
+ * @param multi Output separator (if single string) or indexed array of pre, mid, and post strings
+ */
+function plugin_event_output( $p_event, $p_callbacks, $p_params=null ) {
+	$t_prestring = '';
+	$t_separator = '';
+	$t_poststring = '';
+
+	if ( is_array( $p_params ) ) {
+		switch ( count( $p_params ) ) {
+			case 3:
+				$t_poststring = $p_params[2];
+			case 2:
+				$t_separator = $p_params[1];
+			case 1:
+				$t_prestring = $p_params[0];
+		}
+	} else {
+		$t_separator = $p_params;
+	}
+
+	$t_output = $t_prestring;
+	foreach( $p_callbacks as $t_basename => $t_callback ) {
+		if ( '' != $t_output ) {
+			$t_output .= $p_separator;
+	   	}
+		$t_output .= plugin_event_callback( $p_event, $t_basename, $t_callback, $p_params );
+	}
+	if ( 0 < count( $p_callbacks ) ) {
+		echo $t_output,$t_poststring;
+	}
+}
+
+/**
+ * Process a process event type.
+ * The first plugin callback with be called with the given input.  All following
+ * plugins will be called with the previous plugin's output as its input.  The
+ * final plugin's return value will be returned to the event origin.
+ * @param string Event name
+ * @param array Array of plugin basename/callback key/value pairs
+ * @param string Input string
+ * @return string Output string
+ */
+function plugin_event_process( $p_event, $p_callbacks, $p_input ) {
+	$t_output = $p_input;
+	foreach( $p_callbacks as $t_basename => $t_callback ) {
+		$t_output = plugin_event_callback( $p_event, $t_basename, $t_callback, $t_output );
+	}
+	return $t_output;
+}
+
+/**
+ * Process a default event type.
+ * All plugin callbacks will be called with the given data parameters.  The
+ * return value of each callback will be appended to an array with the plugin's
+ * basename as the key.  This array will then be returned to the event origin.
+ * @param string Event name
+ * @param array Array of plugin basename/callback key/value pairs
+ * @param multi Data
+ * @return array Array of plugin basename/return key/value pairs
+ */
+function plugin_event_default( $p_event, $p_callbacks, $p_data ) {
+	$t_output = array();	
+	foreach( $p_callbacks as $t_basename => $t_callback ) {
+		$t_output[$t_basename] = plugin_event_callback( $p_event, $t_basename, $t_callback, $p_data );
+	}
+	return $t_output;
+}
diff --git a/core/string_api.php b/core/string_api.php
index e418d46..f9662c3 100644
--- a/core/string_api.php
+++ b/core/string_api.php
@@ -101,7 +101,7 @@
 		$p_string = string_preserve_spaces_at_bol( $p_string );
 		$p_string = string_nl2br( $p_string );
 
-		return $p_string;
+		return plugin_event( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -111,7 +111,7 @@
 		$p_string = string_html_specialchars( $p_string );
 		$p_string = string_restore_valid_html_tags( $p_string, /* multiline = */ false );
 
-		return $p_string;
+		return plugin_event( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -124,7 +124,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return plugin_event( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -137,7 +137,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return plugin_event( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -159,7 +159,7 @@
 		# another escaping to escape the special characters created by the generated links
 		$t_string = string_html_specialchars( $t_string );
 
-		return $t_string;
+		return plugin_event( 'EVENT_TEXT_RSS', $p_string );
 	}
 
 	# --------------------
diff --git a/lang/strings_english.txt b/lang/strings_english.txt
index e1181ad..49d9e69 100644
--- a/lang/strings_english.txt
+++ b/lang/strings_english.txt
@@ -296,6 +296,8 @@ $MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
 $MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
 $MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 $MANTIS_ERROR[ERROR_TOKEN_NOT_FOUND] = 'Token could not be found.';
+$MANTIS_ERROR[ERROR_PLUGIN_NOT_REGISTERED] = 'Plugin is not registered with Mantis.';
+$MANTIS_ERROR[ERROR_PLUGIN_PAGE_NOT_FOUND] = 'Plugin page not found.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -705,6 +707,7 @@ $s_manage_users_link = 'Manage Users';
 $s_manage_projects_link = 'Manage Projects';
 $s_manage_custom_field_link = 'Manage Custom Fields';
 $s_manage_global_profiles_link = 'Manage Global Profiles';
+$s_manage_plugin_link = 'Manage Plugins';
 $s_permissions_summary_report = 'Permissions Report';
 $s_manage_config_link = 'Manage Configuration';
 $s_manage_threshold_config = 'Workflow Thresholds';
@@ -797,6 +800,20 @@ $s_all_users = 'All Users';
 $s_set_configuration_option = 'Set Configuration Option';
 $s_delete_config_sure_msg = 'Are you sure you wish to delete this configuration option?';
 
+# manage_plugin_page.php
+$s_plugin = 'Plugin';
+$s_plugins_installed = 'Installed Plugins';
+$s_plugins_available = 'Available Plugins';
+$s_plugin_description = 'Description';
+$s_plugin_author = 'Author: %s';
+$s_plugin_url = 'Website: %s';
+$s_plugin_depends = 'Dependencies';
+$s_plugin_no_depends = 'No dependencies';
+$s_plugin_install = 'Install';
+$s_plugin_upgrade = 'Upgrade';
+$s_plugin_uninstall = 'Uninstall';
+$s_plugin_uninstall_message = 'Are you sure you want to uninstall the %s plugin?';
+
 # manage_proj_add.php
 $s_project_added_msg = 'Project has been successfully added...';
 
diff --git a/manage_plugin_install.php b/manage_plugin_install.php
new file mode 100644
index 0000000..dae612a
--- /dev/null
+++ b/manage_plugin_install.php
@@ -0,0 +1,29 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+plugin_install( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_page.php b/manage_plugin_page.php
new file mode 100644
index 0000000..03d979d
--- /dev/null
+++ b/manage_plugin_page.php
@@ -0,0 +1,231 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+html_page_top1( lang_get( 'manage_plugin_link' ) );
+html_page_top2();
+
+print_manage_menu( 'manage_plugin_page.php' );
+
+$t_plugins = plugin_find_all();
+$t_plugins_installed = plugin_get_installed();
+
+$t_plugins_available = array();
+foreach( $t_plugins as $t_basename => $t_info ) {
+	if ( !isset( $t_plugins_installed[$t_basename] ) ) {
+		$t_plugins_available[$t_basename] = $t_info;
+	}
+}
+
+?>
+
+<?php if ( 0 < count( $t_plugins_installed ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_installed' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_installed as $t_basename => $t_enabled ) {
+	$t_description = string_display_links( $t_plugins[$t_basename]['description'] );
+	$t_author = $t_plugins[$t_basename]['author'];
+	$t_contact = $t_plugins[$t_basename]['contact'];
+	$t_page = $t_plugins[$t_basename]['page'] ;
+	$t_url = $t_plugins[$t_basename]['url'] ;
+	$t_requires = $t_plugins[$t_basename]['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_plugins[$t_basename]['name'].' '.$t_plugins[$t_basename]['version'] );
+	if ( !is_null( $t_page ) && !is_blank( $t_page ) ) {
+		$t_name = '<a href="' . string_display( $t_page ) . '">' . $t_name . '</a>';
+	}
+
+	if ( !is_null( $t_author ) && !is_blank( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_display( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<font color="green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					} else {
+						$t_depends[] = '<font color="brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					}
+				} else {
+					$t_depends[] = '<font color="red">'.string_display( $t_plugin.' '.$t_version ).'</font>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<font color="green">' . lang_get( 'plugin_no_depends' ) . '</font>';
+	}
+
+	$t_upgrade = '';
+	if ( plugin_needs_upgrade( $t_basename ) ) {
+		$t_upgrade = '<form action="manage_plugin_upgrade.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_upgrade' ).'"></form>';
+	}
+	
+	$t_uninstall = '';
+	if ( 'mantis' != $t_basename ) {
+		$t_uninstall = '<form action="manage_plugin_uninstall.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_uninstall' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_upgrade,$t_uninstall,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php if ( 0 < count( $t_plugins_available ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_available' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_available as $t_basename => $t_info ) {
+	$t_description = string_display_links( $t_info['description'] );
+	$t_author = $t_info['author'];
+	$t_contact = $t_info['contact'];
+	$t_url = $t_info['url'] ;
+	$t_requires = $t_info['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_info['name'].' '.$t_info['version'] );
+
+	if ( !is_null( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_display( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	$t_ready = true;
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<font color="green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					} else {
+						$t_ready = false;
+						$t_depends[] = '<font color="brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					}
+				} else {
+					$t_ready = false;
+					$t_depends[] = '<font color="red">'.string_display( $t_plugin.' '.$t_version ).'</font>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<font color="green">' . lang_get( 'plugin_no_depends' ) . '</font>';
+	}
+
+	$t_install = '';
+	if ( $t_ready ) {
+		$t_install = '<form action="manage_plugin_install.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_install' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_install,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php
+html_page_bottom1();
+
diff --git a/manage_plugin_uninstall.php b/manage_plugin_uninstall.php
new file mode 100644
index 0000000..957b6d7
--- /dev/null
+++ b/manage_plugin_uninstall.php
@@ -0,0 +1,32 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+$t_plugin_info = plugin_get_info( $f_basename );
+
+helper_ensure_confirmed( sprintf( lang_get( 'plugin_uninstall_message' ), $t_plugin_info['name'] ), lang_get( 'plugin_uninstall' ) );
+
+plugin_uninstall( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_upgrade.php b/manage_plugin_upgrade.php
new file mode 100644
index 0000000..f29f3a5
--- /dev/null
+++ b/manage_plugin_upgrade.php
@@ -0,0 +1,29 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+$t_status = plugin_upgrade( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/plugin.php b/plugin.php
new file mode 100644
index 0000000..ce8241a
--- /dev/null
+++ b/plugin.php
@@ -0,0 +1,36 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+$t_plugin_path = config_get( 'plugin_path' );
+
+$f_basename = gpc_get_string( 'name' );
+$f_action = gpc_get_string( 'action' );
+
+plugin_ensure_registered( $f_basename );
+
+$t_page = $t_plugin_path.$f_basename.DIRECTORY_SEPARATOR.
+		'pages'.DIRECTORY_SEPARATOR.$f_action.'.php';
+
+if ( !is_file( $t_page ) ) {
+		trigger_error( ERROR_PLUGIN_PAGE_NOT_FOUND, ERROR );
+}
+
+include( $t_page );
+
diff --git a/plugins/debug/events.php b/plugins/debug/events.php
new file mode 100644
index 0000000..4b27e2f
--- /dev/null
+++ b/plugins/debug/events.php
@@ -0,0 +1,9 @@
+<?php
+
+function plugin_event_callback_debug_dump() {
+	global $g_plugin_info, $g_plugin_events;
+
+	echo '<pre>';
+	var_dump( $g_plugin_info, $g_plugin_events );
+	echo '</pre>';
+}
diff --git a/plugins/debug/register.php b/plugins/debug/register.php
new file mode 100644
index 0000000..b24919c
--- /dev/null
+++ b/plugins/debug/register.php
@@ -0,0 +1,16 @@
+<?php
+
+function plugin_callback_debug_info() {
+	return array( 
+		'name' => 'Plugin Debugger',
+		'version' => '1.0',
+		'description' => 'Outputs useful debugging information for plugin developers.',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+function plugin_callback_debug_register() {
+	plugin_register( 'EVENT_PAGE_END', 'dump' );
+}
diff --git a/plugins/supercow/events.php b/plugins/supercow/events.php
new file mode 100644
index 0000000..0edb208
--- /dev/null
+++ b/plugins/supercow/events.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * Handle the EVENT_PLUGIN_INIT callback.
+ */
+function plugin_event_callback_supercow_header() {
+	header( 'X-Mantis: This Mantis has super cow powers.' );
+}
diff --git a/plugins/supercow/register.php b/plugins/supercow/register.php
new file mode 100644
index 0000000..fd652a8
--- /dev/null
+++ b/plugins/supercow/register.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Return plugin details to the API.
+ * @return array Plugin details
+ */
+function plugin_callback_supercow_info() {
+	return array(
+		'name' => 'Super Cow Powers',
+		'description' => 'Gives your Mantis installation super cow powers.',
+		'version' => '1.0',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+/**
+ * Register callback methods for any events necessary.
+ */
+function plugin_callback_supercow_register() {
+	plugin_register( 'EVENT_PLUGIN_INIT', 'header' );
+}
mantis-plugins-2007-10-28.patch (45,585 bytes)   
mantis-plugins-2007-10-30.patch (47,819 bytes)   
diff --git a/admin/install.php b/admin/install.php
index fff33c7..ac9a8d8 100644
--- a/admin/install.php
+++ b/admin/install.php
@@ -26,6 +26,7 @@
 	//@@@ put this somewhere
 	set_time_limit ( 0 ) ;
 	$g_skip_open_db = true;  # don't open the database in database_api.php
+	$g_plugins_disabled = true;
 	@require_once( dirname( dirname( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'core.php' );
 	$g_error_send_page_header = false; # bypass page headers in error handler
 
diff --git a/admin/schema.php b/admin/schema.php
index dc9c5ca..c4b78d4 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -361,4 +361,8 @@ $upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' )
 	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 $upgrade[] = Array('CreateIndexSQL', Array( 'idx_typeowner', config_get( 'mantis_tokens_table' ), 'type, owner' ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_plugin_table' ), "
+	basename		C(40)	NOTNULL PRIMARY,
+	enabled			L		NOTNULL DEFAULT '0'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 ?>
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index b6b79a3..1b480e7 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -1346,6 +1346,7 @@
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
 	$g_mantis_news_table					= '%db_table_prefix%_news%db_table_suffix%';
+	$g_mantis_plugin_table					= '%db_table_prefix%_plugin%db_table_suffix%';
 	$g_mantis_project_category_table		= '%db_table_prefix%_project_category%db_table_suffix%';
 	$g_mantis_project_file_table			= '%db_table_prefix%_project_file%db_table_suffix%';
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
@@ -1919,4 +1920,18 @@
 	
 	# The twitter account password.
 	$g_twitter_password = '';
+
+	#############################
+	# Plugin System
+	#############################
+
+	# enable/disable plugins
+	$g_plugins_enabled 	= ON;
+
+	# absolute path to plugin files.
+	$g_plugin_path		= $g_absolute_path . 'plugins' . DIRECTORY_SEPARATOR;
+
+	# management threshold.
+	$g_manage_plugin_threshold = ADMINISTRATOR;
+
 ?>
diff --git a/core.php b/core.php
index 815521b..a51b08a 100644
--- a/core.php
+++ b/core.php
@@ -159,6 +159,10 @@
 		}
 	}
 	
+	# Initialize Event System
+	require_once( $t_core_path.'event_api.php' );
+	require_once( $t_core_path.'events_inc.php' );
+
 	require_once( $t_core_path.'project_api.php' );
 	require_once( $t_core_path.'project_hierarchy_api.php' );
 	require_once( $t_core_path.'access_api.php' );
@@ -172,4 +176,10 @@
 	if ( !isset( $g_bypass_headers ) && !headers_sent() ) {
 		header( 'Content-type: text/html;charset=' . lang_get( 'charset' ) );
 	}
+
+	# Plugin initialization
+	require_once( $t_core_path.'plugin_api.php' );
+	if ( !isset( $g_plugins_disabled ) ) {
+		plugin_init_all();
+	}
 ?>
diff --git a/core/constant_inc.php b/core/constant_inc.php
index b1a8cce..2220e94 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -322,6 +322,13 @@
 	# ERROR_TOKEN_*
 	define( 'ERROR_TOKEN_NOT_FOUND', 2300 );
 
+	# ERROR_EVENT_*
+	define( 'ERROR_EVENT_UNDECLARED', 2400 );
+
+	# ERROR_PLUGIN *
+	define( 'ERROR_PLUGIN_NOT_REGISTERED', 2500 );
+	define( 'ERROR_PLUGIN_PAGE_NOT_FOUND', 2501 );
+
 	# Status Legend Position
 	define( 'STATUS_LEGEND_POSITION_TOP',		1);
 	define( 'STATUS_LEGEND_POSITION_BOTTOM',	2);
@@ -414,4 +421,9 @@
 	define( 'SPONSORSHIP_REQUESTED',      1 );
 	define( 'SPONSORSHIP_PAID',           2 );
 
+	# Plugin events
+	define( 'EVENT_TYPE_DEFAULT',		0 );
+	define( 'EVENT_TYPE_EXECUTE',		1 );
+	define( 'EVENT_TYPE_OUTPUT',		2 );
+	define( 'EVENT_TYPE_PROCESS',		3 );
 ?>
diff --git a/core/event_api.php b/core/event_api.php
new file mode 100644
index 0000000..22d879a
--- /dev/null
+++ b/core/event_api.php
@@ -0,0 +1,228 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+/**
+ * Event API
+ * Handles the event system.
+ *
+ * @author John Reese
+ */
+
+##### Public API #####
+
+/**
+ * Declare an event of a given type.
+ * Will do nothing if event already exists.
+ * @param string Event name
+ * @param int Event type
+ */
+function event_declare( $p_name, $p_type=EVENT_TYPE_DEFAULT ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) {
+
+		$g_event_cache[$p_name] = array( 
+			'type' => $p_type, 
+			'callbacks' => array()
+		);
+	}	
+}
+
+/**
+ * Convenience function for decleare multiple events.
+ * @param array Events
+ */
+function event_declare_many( $p_events ) {
+	foreach ( $p_events as $t_name => $t_type ) {
+		event_declare( $t_name, $t_type );
+	}
+}
+
+/**
+ * Hook a callback function to a given event.
+ * A plugin's basename must be specified for proper handling of plugin callbacks.
+ * @param string Event name
+ * @param string Callback function
+ * @param string Plugin basename
+ */
+function event_hook( $p_name, $p_callback, $p_plugin=false ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) { 
+		trigger_error( ERROR_EVENT_UNDECLARED, WARNING );
+		return;
+	}
+
+	$g_event_cache[$p_name]['callbacks'][$p_callback] = $p_plugin;
+}
+
+/**
+ * Signal an event to execute and handle callbacks as necessary.
+ * @param string Event name
+ * @param multi Event parameters
+ * @param int Event type override
+ * @return multi Null if event undeclared, appropriate return value otherwise
+ */
+function event_signal( $p_name, $p_params=null, $p_type=null ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) {
+		trigger_error( ERROR_EVENT_UNDECLARED, WARNING );
+		return;
+	}
+
+	if ( is_null( $p_type ) ) {
+		$t_type = $g_event_cache[$p_name]['type'];
+	} else {
+		$t_type = $p_type;
+	}
+	$t_callbacks = $g_event_cache[$p_name]['callbacks'];
+
+	switch ( $t_type ) {
+		case EVENT_TYPE_EXECUTE:
+			return event_type_execute( $p_name, $t_callbacks );
+
+		case EVENT_TYPE_OUTPUT:
+			return event_type_output( $p_name, $t_callbacks, $p_params );
+
+		case EVENT_TYPE_PROCESS:
+			return event_type_process( $p_name, $t_callbacks, $p_params );
+		
+		default:
+			return event_type_default( $p_name, $t_callbacks, $p_params );
+	}
+}
+
+##### Event-handling functions #####
+
+/**
+ * Executes a plugin's callback function for a given event.
+ * @param string Event name
+ * @param string Callback name
+ * @param string Plugin basename
+ * @param multi Parameters for event callback
+ * @return multi Null if callback not found, value from callback otherwise
+ */
+function event_callback( $p_event, $p_callback, $p_plugin, $p_params=null ) {
+	if ( $p_plugin !== false ) {
+		plugin_include( $p_plugin, true );
+		plugin_push_current( $p_plugin );
+	}
+
+	$t_value = null;
+	if ( function_exists( $p_callback ) ) {
+		$t_value = $p_callback( $p_event, $p_params );
+	}
+
+	if ( $p_plugin !== false ) {
+		plugin_pop_current();
+	}
+
+	return $t_value;
+}
+
+/**
+ * Process an execute event type.
+ * All callbacks will be called with no parameters, and their
+ * return values will be ignored.
+ * @param string Event name
+ * @param array Array of callback functions
+ */
+function event_type_execute( $p_event, $p_callbacks ) {
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		event_callback( $p_event, $t_callback, $t_plugin );
+	}
+}
+
+/**
+ * Process an output event type.
+ * All callbacks will be called with the given parameters, and their
+ * return values will be echoed to the client, separated by a given string.
+ * If there are no callbacks, then nothing will be sent as output.
+ * @param string Event name
+ * @param array Array of callback functions
+ * @param multi Output separator (if single string) or indexed array of pre, mid, and post strings
+ */
+function event_type_output( $p_event, $p_callbacks, $p_params=null ) {
+	$t_prestring = '';
+	$t_separator = '';
+	$t_poststring = '';
+
+	if ( is_array( $p_params ) ) {
+		switch ( count( $p_params ) ) {
+			case 3:
+				$t_poststring = $p_params[2];
+			case 2:
+				$t_separator = $p_params[1];
+			case 1:
+				$t_prestring = $p_params[0];
+		}
+	} else {
+		$t_separator = $p_params;
+	}
+
+	$t_output = array();
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output[] = event_callback( $p_event, $t_callback, $t_plugin, $p_params );
+	}
+	if ( 0 < count( $p_callbacks ) ) {
+		echo $t_prestring, implode( $t_separator, $t_output ), $t_poststring;
+	}
+}
+
+/**
+ * Process a process event type.
+ * The first callback with be called with the given input.  All following
+ * callbask will be called with the previous's output as its input.  The
+ * final callback's return value will be returned to the event origin.
+ * @param string Event name
+ * @param array Array of callback functions
+ * @param string Input string
+ * @return string Output string
+ */
+function event_type_process( $p_event, $p_callbacks, $p_input ) {
+	$t_output = $p_input;
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output = event_callback( $p_event, $t_callback, $t_plugin, $t_output );
+	}
+	return $t_output;
+}
+
+/**
+ * Process a default event type.
+ * All callbacks will be called with the given data parameters.  The
+ * return value of each callback will be appended to an array with the callback's
+ * basename as the key.  This array will then be returned to the event origin.
+ * @param string Event name
+ * @param array Array of callback functions
+ * @param multi Data
+ * @return array Array of callback/return key/value pairs
+ */
+function event_type_default( $p_event, $p_callbacks, $p_data ) {
+	$t_output = array();	
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output[$t_callback] = event_callback( $p_event, $t_callback, $t_plugin, $p_data );
+	}
+	return $t_output;
+}
+
diff --git a/core/events_inc.php b/core/events_inc.php
new file mode 100644
index 0000000..4b22e59
--- /dev/null
+++ b/core/events_inc.php
@@ -0,0 +1,21 @@
+<?php
+
+# Declare supported plugin events
+event_declare_many( array(
+
+	# Events specific to plugins
+	'EVENT_PLUGIN_INIT' 		=> EVENT_TYPE_EXECUTE,
+
+	# Events for processing data
+	'EVENT_TEXT_GENERAL'		=> EVENT_TYPE_PROCESS,
+	'EVENT_TEXT_LINKS'			=> EVENT_TYPE_PROCESS,
+	'EVENT_TEXT_RSS'			=> EVENT_TYPE_PROCESS,
+	
+	# Events for layout additions
+	'EVENT_PAGE_HEAD' 			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_TOP' 			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_BOTTOM'			=> EVENT_TYPE_OUTPUT,
+	'EVENT_PAGE_END'			=> EVENT_TYPE_OUTPUT,
+
+) );
+
diff --git a/core/html_api.php b/core/html_api.php
index cb50e51..bc1d696 100644
--- a/core/html_api.php
+++ b/core/html_api.php
@@ -283,6 +283,8 @@
 	# --------------------
 	# (7) End the <head> section
 	function html_head_end() {
+		event_signal( 'EVENT_PAGE_HEAD' );
+
 		echo '</head>', "\n";
 	}
 
@@ -317,6 +319,8 @@
 			echo '<a href="http://www.mantisbt.org" title="Free Web Based Bug Tracker"><img border="0" width="242" height="102" alt="Mantis Bugtracker" src="images/mantis_logo.gif" /></a>';
 			echo '</div>';
 		}
+
+		event_signal( 'EVENT_PAGE_TOP' );
 	}
 
 	# --------------------
@@ -389,6 +393,8 @@
 		if ( !is_blank( $t_page ) && file_exists( $t_page ) && !is_dir( $t_page ) ) {
 			include( $t_page );
 		}
+
+		event_signal( 'EVENT_PAGE_BOTTOM' );
 	}
 
 	# --------------------
@@ -474,6 +480,8 @@
 	# --------------------
 	# (14) End the <body> section
 	function html_body_end() {
+		event_signal( 'EVENT_PAGE_END' );
+
 		echo '</body>', "\n";
 	}
 
@@ -672,7 +680,8 @@
 		$t_manage_user_page 		= 'manage_user_page.php';
 		$t_manage_project_menu_page = 'manage_proj_page.php';
 		$t_manage_custom_field_page = 'manage_custom_field_page.php';
-		$t_manage_config_page = 'adm_config_report.php';
+		$t_manage_plugin_page		= 'manage_plugin_page.php';
+		$t_manage_config_page		= 'adm_config_report.php';
 		$t_manage_prof_menu_page    = 'manage_prof_menu_page.php';
 		# $t_documentation_page 		= 'documentation_page.php';
 
@@ -689,6 +698,9 @@
 			case $t_manage_config_page:
 				$t_manage_config_page = '';
 				break;
+			case $t_manage_plugin_page:
+				$t_manage_plugin_page = '';
+				break;
 			case $t_manage_prof_menu_page:
 				$t_manage_prof_menu_page = '';
 				break;
@@ -710,6 +722,9 @@
 		if ( access_has_global_level( config_get( 'manage_global_profile_threshold' ) ) ) {
 			print_bracket_link( $t_manage_prof_menu_page, lang_get( 'manage_global_profiles_link' ) );
 		}
+		if ( access_has_global_level( config_get( 'manage_plugin_threshold' ) ) ) {
+			print_bracket_link( $t_manage_plugin_page, lang_get( 'manage_plugin_link' ) );
+		}
 		if ( access_has_project_level( config_get( 'view_configuration_threshold' ) ) ) {
 			print_bracket_link( $t_manage_config_page, lang_get( 'manage_config_link' ) );
 		}
diff --git a/core/lang_api.php b/core/lang_api.php
index 69cedd4..c8a7ade 100644
--- a/core/lang_api.php
+++ b/core/lang_api.php
@@ -36,17 +36,25 @@
 	# ------------------
 	# Loads the specified language and stores it in $g_lang_strings,
 	# to be used by lang_get
-	function lang_load( $p_lang ) {
+	function lang_load( $p_lang, $p_dir=null ) {
 		global $g_lang_strings, $g_active_language;
 
 		$g_active_language  = $p_lang;
-		if ( isset( $g_lang_strings[ $p_lang ] ) ) {
+		if ( isset( $g_lang_strings[ $p_lang ] ) && is_null( $p_dir ) ) {
 			return;
 		}
 
-		$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+		$t_lang_dir = $p_dir;
+
+		if ( is_null( $t_lang_dir ) ) {
+			$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+			require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+		} else {
+			if ( is_file( $t_lang_dir . 'strings_' . $p_lang . '.txt' ) ) {
+				include_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+			}
+		}
 
-		require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
 
 		# Allow overriding strings declared in the language file.
 		# custom_strings_inc.php can use $g_active_language
@@ -231,6 +239,14 @@
 		if ( lang_exists( $p_string, $t_lang ) ) {
 			return $g_lang_strings[ $t_lang ][ $p_string];
 		} else {
+			$t_plugin_current = plugin_get_current();
+			if ( !is_null( $t_plugin_current ) ) {
+				lang_load( $t_lang, config_get( 'plugin_path' ).$t_plugin_current.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR );
+				if ( lang_exists( $p_string, $t_lang ) ) {
+					return $g_lang_strings[ $t_lang ][ $p_string];
+				}
+			}
+
 			if ( $t_lang == 'english' ) {
 				error_parameters( $p_string );
 				trigger_error( ERROR_LANG_STRING_NOT_FOUND, WARNING );
diff --git a/core/plugin_api.php b/core/plugin_api.php
new file mode 100644
index 0000000..4d098e4
--- /dev/null
+++ b/core/plugin_api.php
@@ -0,0 +1,469 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+/**
+ * Plugin API
+ * Handles the initialisation, management, and execution of plugins.
+ *
+ * @package PluginAPI
+ */
+
+### Public API functions
+
+/**
+ * Determine if a given plugin basename has been registered.
+ * @return boolean True if registered
+ */
+function plugin_is_registered( $p_basename ) {
+	global $g_plugin_cache;
+	return ( isset( $g_plugin_cache[$p_basename] ) && !is_null( $g_plugin_cache[$p_basename] ) );
+}
+
+/**
+ * Make sure a given plugin basename has been registered.
+ * Triggers ERROR_PLUGIN_NOT_REGISTERED otherwise.
+ */
+function plugin_ensure_registered( $p_basename ) {
+	if ( !plugin_is_registered( $p_basename ) ) {
+		trigger_error( ERROR_PLUGIN_NOT_REGISTERED, ERROR );
+	}
+}
+
+/**
+ * Get the currently executing plugin's basename.
+ * @return string Plugin basename
+ */
+function plugin_get_current() {
+	global $g_plugin_current;
+	return ( isset( $g_plugin_current[0] ) ? $g_plugin_current[0] : null );
+}
+
+/**
+ * Add the current plugin to the stack
+ * @param string Plugin basename
+ */
+function plugin_push_current( $p_basename ) {
+	global $g_plugin_current;
+	array_unshift( $g_plugin_current, $p_basename );
+}
+
+/**
+ * Remove the current plugin from the stack
+ * @return string Plugin basename
+ */
+function plugin_pop_current() {
+	global $g_plugin_current;
+	return array_shift( $g_plugin_current );
+}
+
+/**
+ * Get the information array registered by the given plugin.
+ * @param string Plugin name (defaults to current plugin)
+ * @return array Plugin info (null if unregistered)
+ */
+function plugin_info( $p_basename=null ) {
+	global $g_plugin_cache;
+
+	if ( is_null( $p_basename ) ) {
+		$p_basename = plugin_get_current();
+	}
+
+	return ( isset( $g_plugin_cache[$p_basename] ) ? $g_plugin_cache[$p_basename] : null );
+}
+
+/**
+ * Get the URL to the plugin wrapper page.
+ * @param string Page name
+ * @param string Plugin basename (defaults to current plugin)
+ */
+function plugin_page( $p_page, $p_basename=null ) {
+	if ( is_null( $p_basename ) ) {
+		$p_basename = plugin_get_current();
+	}
+	return 'plugin.php?name='.$p_basename.'&action='.$p_page;
+}
+
+/**
+ * Given a base table name for a plugin, add appropriate prefix and suffix.
+ * Convenience for plugin schema definitions.
+ * @param string Table name
+ * @return string Full table name
+ */
+function plugin_table( $p_name ) {
+	$t_current = plugin_get_current();
+	return config_get( 'db_table_prefix' ) .
+		'_plugin_' . $t_current . '_' . $p_name .
+		config_get( 'db_table_suffix' );
+}
+
+/**
+ * Hook a plugin's callback function to an event.
+ * @param string Event name
+ * @param string Callback function
+ */
+function plugin_event_hook( $p_name, $p_callback ) {
+	$t_basename = plugin_get_current();
+	$t_function = 'plugin_event_' . $t_basename . '_' . $p_callback;
+	event_hook( $p_name, $t_function, $t_basename );
+}
+
+### Plugin management functions
+
+/**
+ * Include the appropriate script for a plugin.
+ * @param srting Plugin basename
+ * @param boolean Include events script
+ */
+function plugin_include( $p_basename, $p_include_events=false ) {
+	$t_path = config_get( 'plugin_path' );
+
+	$t_register_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'register.php';
+	if ( is_file( $t_register_file ) ) {
+		include_once( $t_register_file );
+	}
+
+	if ( $p_include_events ) {
+		$t_events_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'events.php';
+		if ( is_file( $t_events_file ) ) {
+			include_once( $t_events_file );
+		}
+	}
+}
+
+/**
+ * Get the script information from the script file or cache.
+ * @param string Plugin basename
+ * @return array Script information
+ */
+function plugin_get_info( $p_basename ) {
+	global $g_plugin_cache;
+
+	if ( plugin_is_registered( $p_basename ) ) {
+		return $g_plugin_cache[$p_basename];
+	}
+
+	plugin_push_current( $p_basename );
+
+	plugin_include( $p_basename );
+
+	$t_plugin = null;
+
+	$t_info_function = 'plugin_callback_'.$p_basename.'_info';
+	if ( function_exists( $t_info_function ) ) {
+		$t_plugin = $t_info_function();
+	}
+
+	plugin_pop_current();
+
+	return $t_plugin;
+}
+
+function plugin_get_schema( $p_basename ) {
+	plugin_push_current( $p_basename );
+
+	plugin_include( $p_basename );
+
+	$t_schema_function = 'plugin_callback_'.$p_basename.'_schema';
+	if ( !function_exists( $t_schema_function ) ) {
+		return null;
+	}
+	$t_schema = $t_schema_function();
+
+	plugin_pop_current();
+
+	if ( is_array( $t_schema ) ) {
+		return $t_schema;
+	}
+
+	return null;
+}
+
+/**
+ * List all installed plugins.
+ * @return array Installed plugins
+ */
+function plugin_get_installed() {
+	$t_plugin_table = config_get( 'mantis_plugin_table' );
+
+	$t_query = "SELECT * FROM $t_plugin_table";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array( 'mantis' => '1' );
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_basename = $t_row['basename'];
+		$t_plugins[$t_basename] = $t_row['enabled'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * List enabled plugins.
+ * @return array Enabled plugin basenames
+ */
+function plugin_get_enabled() {
+	$t_plugin_table = config_get( 'mantis_plugin_table' );
+
+	$t_query = "SELECT basename FROM $t_plugin_table WHERE enabled=1";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array();
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_plugins[] = $t_row['basename'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * Search the plugins directory for plugins.
+ * @return array Plugin basename/info key/value pairs.
+ */
+function plugin_find_all() {
+	$t_plugin_path = config_get( 'plugin_path' );
+	$t_plugins = array( 'mantis' => plugin_get_info( 'mantis' ) );
+
+	if ( $t_dir = opendir( $t_plugin_path ) ) {
+		while ( ($t_file = readdir( $t_dir )) !== false ) {
+			if ( '.' == $t_file || '..' == $t_file ) continue;
+			if ( is_dir( $t_plugin_path.$t_file ) ) {
+				$t_plugin_info = plugin_get_info( $t_file );
+				if ( !is_null( $t_plugin_info ) ) {
+					$t_plugins[$t_file] = $t_plugin_info;
+				}
+			}
+		}
+		closedir( $t_dir );
+	}
+	return $t_plugins;
+}
+
+/**
+ * Determine if a given plugin is installed.
+ * @param string Plugin basename
+ * @retrun boolean True if plugin is installed
+ */
+function plugin_is_installed( $p_basename ) {
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$c_basename 	= db_prepare_string( $p_basename );
+
+	$t_query = "SELECT COUNT(*) FROM $t_plugin_table WHERE basename=" . db_param(0);
+	$t_result = db_query_bound( $t_query, array( $c_basename ) );
+	return ( 0 < db_result( $t_result ) );
+}
+
+/**
+ * Install a plugin to the database.
+ * @param string Plugin basename
+ */
+function plugin_install( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	if ( plugin_is_installed( $p_basename ) ) {
+		return;
+	}
+
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "INSERT INTO $t_plugin_table ( basename, enabled )
+				VALUES ( ".db_param(0).", 1 )";
+	db_query_bound( $t_query, array( $c_basename ) );
+
+	if ( false === ( config_get( 'schema_plugin_'.$p_basename, false ) ) ) {
+		config_set( 'plugin_schema_'.$p_basename, -1 );
+	}
+	plugin_upgrade( $p_basename );
+}
+
+/**
+ * Determine if an installed plugin needs to upgrade its schema.
+ * @param string Plugin basename
+ * @return boolean True if plugin needs schema ugrades.
+ */
+function plugin_needs_upgrade( $p_basename ) {
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$t_plugin_schema = plugin_get_schema( $p_basename );
+	if ( is_null( $t_plugin_schema ) ) {
+		return false;
+	}
+
+	$t_plugin_schema_version = config_get( 'plugin_schema_'.$p_basename, -1 );
+
+	return ( $t_plugin_schema_version < count( $t_plugin_schema ) - 1 );
+}
+
+/**
+ * Upgrade an installed plugin's schema.
+ * @param string Plugin basename
+ * @return multi True if upgrade completed, null if problem
+ */
+function plugin_upgrade( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	$t_schema_version = config_get( 'plugin_schema_'.$p_basename, -1 );
+	$t_schema = plugin_get_schema( $p_basename );
+
+	global $g_db;
+	$t_dict = NewDataDictionary( $g_db );
+
+	$i = $t_schema_version + 1;
+	while ( $i < count( $t_schema ) ) {
+		$t_target = $t_schema[$i][1][0];
+
+		if ( $t_schema[$i][0] == 'InsertData' ) {
+			$t_sqlarray = call_user_func_array( $t_schema[$i][0], $t_schema[$i][1] );
+		} else if ( $t_schema[$i][0] == 'UpdateSQL' ) {
+			$t_sqlarray = array( $t_schema[$i][1] );
+			$t_target = $t_schema[$i][1];
+		} else {
+			$t_sqlarray = call_user_func_array( Array( $t_dict, $t_schema[$i][0] ), $t_schema[$i][1] );
+		}
+		$t_status = $t_dict->ExecuteSQLArray( $t_sqlarray );
+
+		if ( 2 == $t_status ) {
+			config_set( 'plugin_schema_'.$p_basename, $i );
+		} else {
+			return null;
+		}
+
+		$i++;
+	}
+
+	return true;
+}
+
+/**
+ * Uninstall a plugin from the database.
+ * @param string Plugin basename
+ */
+function plugin_uninstall( $p_basename ) {
+	access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+	if ( !plugin_is_installed( $p_basename ) ) {
+		return;
+	}
+
+	$t_plugin_table	= config_get( 'mantis_plugin_table' );
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "DELETE FROM $t_plugin_table WHERE basename=" . db_param(0);
+	db_query_bound( $t_query, array( $c_basename ) );
+}
+
+### Core usage only.
+
+/**
+ * Initialize all enabled plugins.
+ * Post-signals EVENT_PLUGIN_INIT.
+ */
+function plugin_init_all() {
+	if ( OFF == config_get( 'plugins_enabled' ) ) {
+		return;
+	}
+
+	global $g_plugin_cache;
+	if ( !isset( $g_plugin_cache ) ) {
+		$g_plugin_cache = array();
+	}
+
+	global $g_plugin_current;
+	if ( !isset( $g_plugin_current ) ) {
+		$g_plugin_current = array();
+	}
+
+	# Initial plugin for version dependencies
+	$g_plugin_cache['mantis'] = array(
+		'name' => 'Mantis',
+		'description' => 'The Mantis bugtracker.',
+		'version' => MANTIS_VERSION,
+		'requires' => array(),
+		'author' => 'The Mantis Team',
+		'url' => 'http://mantisbt.org',
+	);
+
+	plugin_init_array( plugin_get_enabled() );
+
+	event_signal( 'EVENT_PLUGIN_INIT' );
+}
+
+/**
+ * Recursive plugin initialization to handle dependencies.
+ * @param array Plugin basenames to initialize.
+ */
+function plugin_init_array( $p_plugins, $p_depth=0 ) {
+	$t_plugins_retry = array();
+
+	foreach( $p_plugins as $t_basename ) {
+		if ( !plugin_init( $t_basename ) ) {
+			# Dependent plugin
+			$t_plugins_retry[] = $t_basename;
+		}
+	}
+
+	# Recurse on dependent plugins
+	if ( $p_depth < count( $p_plugins ) ) {
+		plugin_init_array( $t_plugins_retry, $p_depth + 1 );
+	}
+}
+
+/**
+ * Initialize a single plugin.
+ * @param string Plugin basename
+ * @return boolean True if plugin initialized, false otherwise.
+ */
+function plugin_init( $p_basename ) {
+	global $g_plugin_cache;
+
+	plugin_push_current( $p_basename );
+	plugin_include( $p_basename );
+
+	$t_plugin = plugin_get_info( $p_basename );
+	if ( $t_plugin !== null ) {
+		$g_plugin_cache[$p_basename] = $t_plugin;
+
+		# handle dependent plugins
+		if ( isset( $t_plugin['requires'] ) ) {
+			foreach ( $t_plugin['requires'] as $t_required => $t_version ) {
+				if ( !isset( $g_plugin_cache[$t_required] ) ||
+					( !is_null( $t_version ) &&
+					$g_plugin_cache[$t_required]['version'] < $t_version ) ) {
+					return false;
+				}
+			}
+		}
+
+		$t_init_function = 'plugin_callback_'.$p_basename.'_initialize';
+		if ( function_exists( $t_init_function ) ) {
+			$t_init_function();
+		}
+	}
+
+	plugin_pop_current();
+
+	return true;
+}
+
diff --git a/core/string_api.php b/core/string_api.php
index e418d46..a7d2baf 100644
--- a/core/string_api.php
+++ b/core/string_api.php
@@ -101,7 +101,7 @@
 		$p_string = string_preserve_spaces_at_bol( $p_string );
 		$p_string = string_nl2br( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -111,7 +111,7 @@
 		$p_string = string_html_specialchars( $p_string );
 		$p_string = string_restore_valid_html_tags( $p_string, /* multiline = */ false );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -124,7 +124,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -137,7 +137,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -159,7 +159,7 @@
 		# another escaping to escape the special characters created by the generated links
 		$t_string = string_html_specialchars( $t_string );
 
-		return $t_string;
+		return event_signal( 'EVENT_TEXT_RSS', $p_string );
 	}
 
 	# --------------------
diff --git a/lang/strings_english.txt b/lang/strings_english.txt
index e17a93a..4110843 100644
--- a/lang/strings_english.txt
+++ b/lang/strings_english.txt
@@ -296,6 +296,9 @@ $MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
 $MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
 $MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 $MANTIS_ERROR[ERROR_TOKEN_NOT_FOUND] = 'Token could not be found.';
+$MANTIS_ERROR[ERROR_EVENT_UNDECLARED] = 'Event has not yet been declared.';
+$MANTIS_ERROR[ERROR_PLUGIN_NOT_REGISTERED] = 'Plugin is not registered with Mantis.';
+$MANTIS_ERROR[ERROR_PLUGIN_PAGE_NOT_FOUND] = 'Plugin page not found.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -706,6 +709,7 @@ $s_manage_users_link = 'Manage Users';
 $s_manage_projects_link = 'Manage Projects';
 $s_manage_custom_field_link = 'Manage Custom Fields';
 $s_manage_global_profiles_link = 'Manage Global Profiles';
+$s_manage_plugin_link = 'Manage Plugins';
 $s_permissions_summary_report = 'Permissions Report';
 $s_manage_config_link = 'Manage Configuration';
 $s_manage_threshold_config = 'Workflow Thresholds';
@@ -798,6 +802,20 @@ $s_all_users = 'All Users';
 $s_set_configuration_option = 'Set Configuration Option';
 $s_delete_config_sure_msg = 'Are you sure you wish to delete this configuration option?';
 
+# manage_plugin_page.php
+$s_plugin = 'Plugin';
+$s_plugins_installed = 'Installed Plugins';
+$s_plugins_available = 'Available Plugins';
+$s_plugin_description = 'Description';
+$s_plugin_author = 'Author: %s';
+$s_plugin_url = 'Website: %s';
+$s_plugin_depends = 'Dependencies';
+$s_plugin_no_depends = 'No dependencies';
+$s_plugin_install = 'Install';
+$s_plugin_upgrade = 'Upgrade';
+$s_plugin_uninstall = 'Uninstall';
+$s_plugin_uninstall_message = 'Are you sure you want to uninstall the %s plugin?';
+
 # manage_proj_add.php
 $s_project_added_msg = 'Project has been successfully added...';
 
diff --git a/manage_plugin_install.php b/manage_plugin_install.php
new file mode 100644
index 0000000..998ad70
--- /dev/null
+++ b/manage_plugin_install.php
@@ -0,0 +1,33 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+plugin_install( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_page.php b/manage_plugin_page.php
new file mode 100644
index 0000000..faf3c30
--- /dev/null
+++ b/manage_plugin_page.php
@@ -0,0 +1,235 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+html_page_top1( lang_get( 'manage_plugin_link' ) );
+html_page_top2();
+
+print_manage_menu( 'manage_plugin_page.php' );
+
+$t_plugins = plugin_find_all();
+$t_plugins_installed = plugin_get_installed();
+
+$t_plugins_available = array();
+foreach( $t_plugins as $t_basename => $t_info ) {
+	if ( !isset( $t_plugins_installed[$t_basename] ) ) {
+		$t_plugins_available[$t_basename] = $t_info;
+	}
+}
+
+?>
+
+<?php if ( 0 < count( $t_plugins_installed ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_installed' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_installed as $t_basename => $t_enabled ) {
+	$t_description = string_display_links( $t_plugins[$t_basename]['description'] );
+	$t_author = $t_plugins[$t_basename]['author'];
+	$t_contact = $t_plugins[$t_basename]['contact'];
+	$t_page = $t_plugins[$t_basename]['page'] ;
+	$t_url = $t_plugins[$t_basename]['url'] ;
+	$t_requires = $t_plugins[$t_basename]['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_plugins[$t_basename]['name'].' '.$t_plugins[$t_basename]['version'] );
+	if ( !is_null( $t_page ) && !is_blank( $t_page ) ) {
+		$t_name = '<a href="' . string_display( $t_page ) . '">' . $t_name . '</a>';
+	}
+
+	if ( !is_null( $t_author ) && !is_blank( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_display( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<font color="green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					} else {
+						$t_depends[] = '<font color="brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					}
+				} else {
+					$t_depends[] = '<font color="red">'.string_display( $t_plugin.' '.$t_version ).'</font>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<font color="green">' . lang_get( 'plugin_no_depends' ) . '</font>';
+	}
+
+	$t_upgrade = '';
+	if ( plugin_needs_upgrade( $t_basename ) ) {
+		$t_upgrade = '<form action="manage_plugin_upgrade.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_upgrade' ).'"></form>';
+	}
+	
+	$t_uninstall = '';
+	if ( 'mantis' != $t_basename ) {
+		$t_uninstall = '<form action="manage_plugin_uninstall.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_uninstall' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_upgrade,$t_uninstall,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php if ( 0 < count( $t_plugins_available ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_available' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_available as $t_basename => $t_info ) {
+	$t_description = string_display_links( $t_info['description'] );
+	$t_author = $t_info['author'];
+	$t_contact = $t_info['contact'];
+	$t_url = $t_info['url'] ;
+	$t_requires = $t_info['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_info['name'].' '.$t_info['version'] );
+
+	if ( !is_null( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_display( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	$t_ready = true;
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<font color="green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					} else {
+						$t_ready = false;
+						$t_depends[] = '<font color="brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					}
+				} else {
+					$t_ready = false;
+					$t_depends[] = '<font color="red">'.string_display( $t_plugin.' '.$t_version ).'</font>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<font color="green">' . lang_get( 'plugin_no_depends' ) . '</font>';
+	}
+
+	$t_install = '';
+	if ( $t_ready ) {
+		$t_install = '<form action="manage_plugin_install.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_install' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_install,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php
+html_page_bottom1();
+
diff --git a/manage_plugin_uninstall.php b/manage_plugin_uninstall.php
new file mode 100644
index 0000000..af511da
--- /dev/null
+++ b/manage_plugin_uninstall.php
@@ -0,0 +1,36 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+$t_plugin_info = plugin_get_info( $f_basename );
+
+helper_ensure_confirmed( sprintf( lang_get( 'plugin_uninstall_message' ), $t_plugin_info['name'] ), lang_get( 'plugin_uninstall' ) );
+
+plugin_uninstall( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_upgrade.php b/manage_plugin_upgrade.php
new file mode 100644
index 0000000..95947af
--- /dev/null
+++ b/manage_plugin_upgrade.php
@@ -0,0 +1,33 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+$t_status = plugin_upgrade( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/plugin.php b/plugin.php
new file mode 100644
index 0000000..ce8241a
--- /dev/null
+++ b/plugin.php
@@ -0,0 +1,36 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+$t_plugin_path = config_get( 'plugin_path' );
+
+$f_basename = gpc_get_string( 'name' );
+$f_action = gpc_get_string( 'action' );
+
+plugin_ensure_registered( $f_basename );
+
+$t_page = $t_plugin_path.$f_basename.DIRECTORY_SEPARATOR.
+		'pages'.DIRECTORY_SEPARATOR.$f_action.'.php';
+
+if ( !is_file( $t_page ) ) {
+		trigger_error( ERROR_PLUGIN_PAGE_NOT_FOUND, ERROR );
+}
+
+include( $t_page );
+
diff --git a/plugins/debug/events.php b/plugins/debug/events.php
new file mode 100644
index 0000000..f2e3968
--- /dev/null
+++ b/plugins/debug/events.php
@@ -0,0 +1,9 @@
+<?php
+
+function plugin_event_debug_dump() {
+	global $g_plugin_cache, $g_event_cache;
+
+	echo '<pre>';
+	var_dump( $g_plugin_cache, $g_event_cache );
+	echo '</pre>';
+}
diff --git a/plugins/debug/register.php b/plugins/debug/register.php
new file mode 100644
index 0000000..b924000
--- /dev/null
+++ b/plugins/debug/register.php
@@ -0,0 +1,16 @@
+<?php
+
+function plugin_callback_debug_info() {
+	return array( 
+		'name' => 'Plugin Debugger',
+		'version' => '1.0',
+		'description' => 'Outputs useful debugging information for plugin developers.',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+function plugin_callback_debug_initialize() {
+	plugin_event_hook( 'EVENT_PAGE_END', 'dump' );
+}
diff --git a/plugins/supercow/events.php b/plugins/supercow/events.php
new file mode 100644
index 0000000..d90f5e0
--- /dev/null
+++ b/plugins/supercow/events.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * Handle the EVENT_PLUGIN_INIT callback.
+ */
+function plugin_event_supercow_header() {
+	header( 'X-Mantis: This Mantis has super cow powers.' );
+}
diff --git a/plugins/supercow/register.php b/plugins/supercow/register.php
new file mode 100644
index 0000000..9a4fe47
--- /dev/null
+++ b/plugins/supercow/register.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Return plugin details to the API.
+ * @return array Plugin details
+ */
+function plugin_callback_supercow_info() {
+	return array(
+		'name' => 'Super Cow Powers',
+		'description' => 'Gives your Mantis installation super cow powers.',
+		'version' => '1.0',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+/**
+ * Register callback methods for any events necessary.
+ */
+function plugin_callback_supercow_initialize() {
+	plugin_event_hook( 'EVENT_PLUGIN_INIT', 'header' );
+}
mantis-plugins-2007-10-30.patch (47,819 bytes)   
mantis-plugins-2007-11-01.patch (50,196 bytes)   
diff --git a/admin/install.php b/admin/install.php
index fff33c7..ac9a8d8 100644
--- a/admin/install.php
+++ b/admin/install.php
@@ -26,6 +26,7 @@
 	//@@@ put this somewhere
 	set_time_limit ( 0 ) ;
 	$g_skip_open_db = true;  # don't open the database in database_api.php
+	$g_plugins_disabled = true;
 	@require_once( dirname( dirname( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'core.php' );
 	$g_error_send_page_header = false; # bypass page headers in error handler
 
diff --git a/admin/schema.php b/admin/schema.php
index dc9c5ca..c4b78d4 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -361,4 +361,8 @@ $upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' )
 	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 $upgrade[] = Array('CreateIndexSQL', Array( 'idx_typeowner', config_get( 'mantis_tokens_table' ), 'type, owner' ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_plugin_table' ), "
+	basename		C(40)	NOTNULL PRIMARY,
+	enabled			L		NOTNULL DEFAULT '0'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 ?>
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index b6b79a3..1b480e7 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -1346,6 +1346,7 @@
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
 	$g_mantis_news_table					= '%db_table_prefix%_news%db_table_suffix%';
+	$g_mantis_plugin_table					= '%db_table_prefix%_plugin%db_table_suffix%';
 	$g_mantis_project_category_table		= '%db_table_prefix%_project_category%db_table_suffix%';
 	$g_mantis_project_file_table			= '%db_table_prefix%_project_file%db_table_suffix%';
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
@@ -1919,4 +1920,18 @@
 	
 	# The twitter account password.
 	$g_twitter_password = '';
+
+	#############################
+	# Plugin System
+	#############################
+
+	# enable/disable plugins
+	$g_plugins_enabled 	= ON;
+
+	# absolute path to plugin files.
+	$g_plugin_path		= $g_absolute_path . 'plugins' . DIRECTORY_SEPARATOR;
+
+	# management threshold.
+	$g_manage_plugin_threshold = ADMINISTRATOR;
+
 ?>
diff --git a/core.php b/core.php
index 815521b..a51b08a 100644
--- a/core.php
+++ b/core.php
@@ -159,6 +159,10 @@
 		}
 	}
 	
+	# Initialize Event System
+	require_once( $t_core_path.'event_api.php' );
+	require_once( $t_core_path.'events_inc.php' );
+
 	require_once( $t_core_path.'project_api.php' );
 	require_once( $t_core_path.'project_hierarchy_api.php' );
 	require_once( $t_core_path.'access_api.php' );
@@ -172,4 +176,10 @@
 	if ( !isset( $g_bypass_headers ) && !headers_sent() ) {
 		header( 'Content-type: text/html;charset=' . lang_get( 'charset' ) );
 	}
+
+	# Plugin initialization
+	require_once( $t_core_path.'plugin_api.php' );
+	if ( !isset( $g_plugins_disabled ) ) {
+		plugin_init_all();
+	}
 ?>
diff --git a/core/constant_inc.php b/core/constant_inc.php
index b1a8cce..bddb93b 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -322,6 +322,14 @@
 	# ERROR_TOKEN_*
 	define( 'ERROR_TOKEN_NOT_FOUND', 2300 );
 
+	# ERROR_EVENT_*
+	define( 'ERROR_EVENT_UNDECLARED', 2400 );
+
+	# ERROR_PLUGIN *
+	define( 'ERROR_PLUGIN_NOT_REGISTERED', 2500 );
+	define( 'ERROR_PLUGIN_ALREAD_INSTALLED', 2501 );
+	define( 'ERROR_PLUGIN_PAGE_NOT_FOUND', 2502 );
+
 	# Status Legend Position
 	define( 'STATUS_LEGEND_POSITION_TOP',		1);
 	define( 'STATUS_LEGEND_POSITION_BOTTOM',	2);
@@ -414,4 +422,9 @@
 	define( 'SPONSORSHIP_REQUESTED',      1 );
 	define( 'SPONSORSHIP_PAID',           2 );
 
+	# Plugin events
+	define( 'EVENT_TYPE_DEFAULT',		0 );
+	define( 'EVENT_TYPE_EXECUTE',		1 );
+	define( 'EVENT_TYPE_OUTPUT',		2 );
+	define( 'EVENT_TYPE_PROCESS',		3 );
 ?>
diff --git a/core/event_api.php b/core/event_api.php
new file mode 100644
index 0000000..5365050
--- /dev/null
+++ b/core/event_api.php
@@ -0,0 +1,232 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+/**
+ * Event API
+ * Handles the event system.
+ *
+ * @author John Reese
+ */
+
+##### Cache variables #####
+
+$g_event_cache = array();
+
+##### Public API #####
+
+/**
+ * Declare an event of a given type.
+ * Will do nothing if event already exists.
+ * @param string Event name
+ * @param int Event type
+ */
+function event_declare( $p_name, $p_type=EVENT_TYPE_DEFAULT ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) {
+
+		$g_event_cache[$p_name] = array( 
+			'type' => $p_type, 
+			'callbacks' => array()
+		);
+	}	
+}
+
+/**
+ * Convenience function for decleare multiple events.
+ * @param array Events
+ */
+function event_declare_many( $p_events ) {
+	foreach ( $p_events as $t_name => $t_type ) {
+		event_declare( $t_name, $t_type );
+	}
+}
+
+/**
+ * Hook a callback function to a given event.
+ * A plugin's basename must be specified for proper handling of plugin callbacks.
+ * @param string Event name
+ * @param string Callback function
+ * @param string Plugin basename
+ */
+function event_hook( $p_name, $p_callback, $p_plugin=false ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) { 
+		trigger_error( ERROR_EVENT_UNDECLARED, WARNING );
+		return null;
+	}
+
+	$g_event_cache[$p_name]['callbacks'][$p_callback] = $p_plugin;
+}
+
+/**
+ * Signal an event to execute and handle callbacks as necessary.
+ * @param string Event name
+ * @param multi Event parameters
+ * @param int Event type override
+ * @return multi Null if event undeclared, appropriate return value otherwise
+ */
+function event_signal( $p_name, $p_params=null, $p_type=null ) {
+	global $g_event_cache;
+
+	if ( !isset( $g_event_cache[$p_name] ) ) {
+		trigger_error( ERROR_EVENT_UNDECLARED, WARNING );
+		return null;
+	}
+
+	if ( is_null( $p_type ) ) {
+		$t_type = $g_event_cache[$p_name]['type'];
+	} else {
+		$t_type = $p_type;
+	}
+	$t_callbacks = $g_event_cache[$p_name]['callbacks'];
+
+	switch ( $t_type ) {
+		case EVENT_TYPE_EXECUTE:
+			return event_type_execute( $p_name, $t_callbacks );
+
+		case EVENT_TYPE_OUTPUT:
+			return event_type_output( $p_name, $t_callbacks, $p_params );
+
+		case EVENT_TYPE_PROCESS:
+			return event_type_process( $p_name, $t_callbacks, $p_params );
+		
+		default:
+			return event_type_default( $p_name, $t_callbacks, $p_params );
+	}
+}
+
+##### Event-handling functions #####
+
+/**
+ * Executes a plugin's callback function for a given event.
+ * @param string Event name
+ * @param string Callback name
+ * @param string Plugin basename
+ * @param multi Parameters for event callback
+ * @return multi Null if callback not found, value from callback otherwise
+ */
+function event_callback( $p_event, $p_callback, $p_plugin, $p_params=null ) {
+	if ( $p_plugin !== false ) {
+		plugin_include( $p_plugin, true );
+		plugin_push_current( $p_plugin );
+	}
+
+	$t_value = null;
+	if ( function_exists( $p_callback ) ) {
+		$t_value = $p_callback( $p_event, $p_params );
+	}
+
+	if ( $p_plugin !== false ) {
+		plugin_pop_current();
+	}
+
+	return $t_value;
+}
+
+/**
+ * Process an execute event type.
+ * All callbacks will be called with no parameters, and their
+ * return values will be ignored.
+ * @param string Event name
+ * @param array Array of callback function/plugin basename key/value pairs
+ */
+function event_type_execute( $p_event, $p_callbacks ) {
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		event_callback( $p_event, $t_callback, $t_plugin );
+	}
+}
+
+/**
+ * Process an output event type.
+ * All callbacks will be called with the given parameters, and their
+ * return values will be echoed to the client, separated by a given string.
+ * If there are no callbacks, then nothing will be sent as output.
+ * @param string Event name
+ * @param array Array of callback function/plugin basename key/value pairs
+ * @param multi Output separator (if single string) or indexed array of pre, mid, and post strings
+ */
+function event_type_output( $p_event, $p_callbacks, $p_params=null ) {
+	$t_prefix = '';
+	$t_separator = '';
+	$t_postfix = '';
+
+	if ( is_array( $p_params ) ) {
+		switch ( count( $p_params ) ) {
+			case 3:
+				$t_postfix = $p_params[2];
+			case 2:
+				$t_separator = $p_params[1];
+			case 1:
+				$t_prefix = $p_params[0];
+		}
+	} else {
+		$t_separator = $p_params;
+	}
+
+	$t_output = array();
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output[] = event_callback( $p_event, $t_callback, $t_plugin, $p_params );
+	}
+	if ( count( $p_callbacks ) > 0 ) {
+		echo $t_prefix, implode( $t_separator, $t_output ), $t_postfix;
+	}
+}
+
+/**
+ * Process a process event type.
+ * The first callback with be called with the given input.  All following
+ * callbacks will be called with the previous's output as its input.  The
+ * final callback's return value will be returned to the event origin.
+ * @param string Event name
+ * @param array Array of callback function/plugin basename key/value pairs
+ * @param string Input string
+ * @return string Output string
+ */
+function event_type_process( $p_event, $p_callbacks, $p_input ) {
+	$t_output = $p_input;
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output = event_callback( $p_event, $t_callback, $t_plugin, $t_output );
+	}
+	return $t_output;
+}
+
+/**
+ * Process a default event type.
+ * All callbacks will be called with the given data parameters.  The
+ * return value of each callback will be appended to an array with the callback's
+ * basename as the key.  This array will then be returned to the event origin.
+ * @param string Event name
+ * @param array Array of callback function/plugin basename key/value pairs
+ * @param multi Data
+ * @return array Array of callback/return key/value pairs
+ */
+function event_type_default( $p_event, $p_callbacks, $p_data ) {
+	$t_output = array();	
+	foreach( $p_callbacks as $t_callback => $t_plugin ) {
+		$t_output[$t_callback] = event_callback( $p_event, $t_callback, $t_plugin, $p_data );
+	}
+	return $t_output;
+}
+
diff --git a/core/events_inc.php b/core/events_inc.php
new file mode 100644
index 0000000..aa17dfe
--- /dev/null
+++ b/core/events_inc.php
@@ -0,0 +1,37 @@
+<?php
+
+# Declare supported plugin events
+event_declare_many( array(
+
+	##### Events specific to plugins #####
+
+	# Called when all plugins have been initialized
+	'EVENT_PLUGIN_INIT' 		=> EVENT_TYPE_EXECUTE,
+
+	##### Events for processing data #####
+
+	# Called to process generic strings for output
+	'EVENT_TEXT_GENERAL'		=> EVENT_TYPE_PROCESS,
+
+	# Called to process strings with linkable content
+	'EVENT_TEXT_LINKS'			=> EVENT_TYPE_PROCESS,
+
+	# Called to process RSS output
+	'EVENT_TEXT_RSS'			=> EVENT_TYPE_PROCESS,
+	
+	##### Events for layout additions #####
+
+	# Called just before ending the <head> tag
+	'EVENT_PAGE_HEAD' 			=> EVENT_TYPE_OUTPUT,
+
+	# Called after the page logo has been included
+	'EVENT_PAGE_TOP' 			=> EVENT_TYPE_OUTPUT,
+
+	# Called before the page footer
+	'EVENT_PAGE_BOTTOM'			=> EVENT_TYPE_OUTPUT,
+
+	# Called just before ending the <body> tag
+	'EVENT_PAGE_END'			=> EVENT_TYPE_OUTPUT,
+
+) );
+
diff --git a/core/html_api.php b/core/html_api.php
index cb50e51..bc1d696 100644
--- a/core/html_api.php
+++ b/core/html_api.php
@@ -283,6 +283,8 @@
 	# --------------------
 	# (7) End the <head> section
 	function html_head_end() {
+		event_signal( 'EVENT_PAGE_HEAD' );
+
 		echo '</head>', "\n";
 	}
 
@@ -317,6 +319,8 @@
 			echo '<a href="http://www.mantisbt.org" title="Free Web Based Bug Tracker"><img border="0" width="242" height="102" alt="Mantis Bugtracker" src="images/mantis_logo.gif" /></a>';
 			echo '</div>';
 		}
+
+		event_signal( 'EVENT_PAGE_TOP' );
 	}
 
 	# --------------------
@@ -389,6 +393,8 @@
 		if ( !is_blank( $t_page ) && file_exists( $t_page ) && !is_dir( $t_page ) ) {
 			include( $t_page );
 		}
+
+		event_signal( 'EVENT_PAGE_BOTTOM' );
 	}
 
 	# --------------------
@@ -474,6 +480,8 @@
 	# --------------------
 	# (14) End the <body> section
 	function html_body_end() {
+		event_signal( 'EVENT_PAGE_END' );
+
 		echo '</body>', "\n";
 	}
 
@@ -672,7 +680,8 @@
 		$t_manage_user_page 		= 'manage_user_page.php';
 		$t_manage_project_menu_page = 'manage_proj_page.php';
 		$t_manage_custom_field_page = 'manage_custom_field_page.php';
-		$t_manage_config_page = 'adm_config_report.php';
+		$t_manage_plugin_page		= 'manage_plugin_page.php';
+		$t_manage_config_page		= 'adm_config_report.php';
 		$t_manage_prof_menu_page    = 'manage_prof_menu_page.php';
 		# $t_documentation_page 		= 'documentation_page.php';
 
@@ -689,6 +698,9 @@
 			case $t_manage_config_page:
 				$t_manage_config_page = '';
 				break;
+			case $t_manage_plugin_page:
+				$t_manage_plugin_page = '';
+				break;
 			case $t_manage_prof_menu_page:
 				$t_manage_prof_menu_page = '';
 				break;
@@ -710,6 +722,9 @@
 		if ( access_has_global_level( config_get( 'manage_global_profile_threshold' ) ) ) {
 			print_bracket_link( $t_manage_prof_menu_page, lang_get( 'manage_global_profiles_link' ) );
 		}
+		if ( access_has_global_level( config_get( 'manage_plugin_threshold' ) ) ) {
+			print_bracket_link( $t_manage_plugin_page, lang_get( 'manage_plugin_link' ) );
+		}
 		if ( access_has_project_level( config_get( 'view_configuration_threshold' ) ) ) {
 			print_bracket_link( $t_manage_config_page, lang_get( 'manage_config_link' ) );
 		}
diff --git a/core/lang_api.php b/core/lang_api.php
index 69cedd4..c8a7ade 100644
--- a/core/lang_api.php
+++ b/core/lang_api.php
@@ -36,17 +36,25 @@
 	# ------------------
 	# Loads the specified language and stores it in $g_lang_strings,
 	# to be used by lang_get
-	function lang_load( $p_lang ) {
+	function lang_load( $p_lang, $p_dir=null ) {
 		global $g_lang_strings, $g_active_language;
 
 		$g_active_language  = $p_lang;
-		if ( isset( $g_lang_strings[ $p_lang ] ) ) {
+		if ( isset( $g_lang_strings[ $p_lang ] ) && is_null( $p_dir ) ) {
 			return;
 		}
 
-		$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+		$t_lang_dir = $p_dir;
+
+		if ( is_null( $t_lang_dir ) ) {
+			$t_lang_dir = dirname ( dirname ( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
+			require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+		} else {
+			if ( is_file( $t_lang_dir . 'strings_' . $p_lang . '.txt' ) ) {
+				include_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
+			}
+		}
 
-		require_once( $t_lang_dir . 'strings_' . $p_lang . '.txt' );
 
 		# Allow overriding strings declared in the language file.
 		# custom_strings_inc.php can use $g_active_language
@@ -231,6 +239,14 @@
 		if ( lang_exists( $p_string, $t_lang ) ) {
 			return $g_lang_strings[ $t_lang ][ $p_string];
 		} else {
+			$t_plugin_current = plugin_get_current();
+			if ( !is_null( $t_plugin_current ) ) {
+				lang_load( $t_lang, config_get( 'plugin_path' ).$t_plugin_current.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR );
+				if ( lang_exists( $p_string, $t_lang ) ) {
+					return $g_lang_strings[ $t_lang ][ $p_string];
+				}
+			}
+
 			if ( $t_lang == 'english' ) {
 				error_parameters( $p_string );
 				trigger_error( ERROR_LANG_STRING_NOT_FOUND, WARNING );
diff --git a/core/plugin_api.php b/core/plugin_api.php
new file mode 100644
index 0000000..d937591
--- /dev/null
+++ b/core/plugin_api.php
@@ -0,0 +1,487 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+/**
+ * Plugin API
+ * Handles the initialisation, management, and execution of plugins.
+ *
+ * @package PluginAPI
+ */
+
+##### Cache variables #####
+
+$g_plugin_cache = array();
+$g_plugin_current = array();
+
+### Public API functions
+
+/**
+ * Determine if a given plugin basename has been registered.
+ * @return boolean True if registered
+ */
+function plugin_is_registered( $p_basename ) {
+	global $g_plugin_cache;
+	return ( isset( $g_plugin_cache[$p_basename] ) && !is_null( $g_plugin_cache[$p_basename] ) );
+}
+
+/**
+ * Make sure a given plugin basename has been registered.
+ * Triggers ERROR_PLUGIN_NOT_REGISTERED otherwise.
+ */
+function plugin_ensure_registered( $p_basename ) {
+	if ( !plugin_is_registered( $p_basename ) ) {
+		trigger_error( ERROR_PLUGIN_NOT_REGISTERED, ERROR );
+	}
+}
+
+/**
+ * Get the currently executing plugin's basename.
+ * @return string Plugin basename, or null if no current plugin
+ */
+function plugin_get_current() {
+	global $g_plugin_current;
+	return ( isset( $g_plugin_current[0] ) ? $g_plugin_current[0] : null );
+}
+
+/**
+ * Add the current plugin to the stack
+ * @param string Plugin basename
+ */
+function plugin_push_current( $p_basename ) {
+	global $g_plugin_current;
+	array_unshift( $g_plugin_current, $p_basename );
+}
+
+/**
+ * Remove the current plugin from the stack
+ * @return string Plugin basename, or null if no current plugin
+ */
+function plugin_pop_current() {
+	global $g_plugin_current;
+	return ( isset( $g_plugin_current[0] ) ? array_shift( $g_plugin_current ) : null );
+}
+
+/**
+ * Get the information array registered by the given plugin.
+ * @param string Plugin basename (defaults to current plugin)
+ * @return array Plugin info (null if unregistered)
+ */
+function plugin_info( $p_basename=null ) {
+	global $g_plugin_cache;
+
+	if ( is_null( $p_basename ) ) {
+		$p_basename = plugin_get_current();
+	}
+
+	return ( isset( $g_plugin_cache[$p_basename] ) ? $g_plugin_cache[$p_basename] : null );
+}
+
+/**
+ * Get the URL to the plugin wrapper page.
+ * @param string Page name
+ * @param string Plugin basename (defaults to current plugin)
+ */
+function plugin_page( $p_page, $p_basename=null ) {
+	if ( is_null( $p_basename ) ) {
+		$t_current = plugin_get_current();
+	} else {
+		$t_current = $p_basename;
+	}
+	return 'plugin.php?page='.$t_current.'/'.$p_page;
+}
+
+/**
+ * Given a base table name for a plugin, add appropriate prefix and suffix.
+ * Convenience for plugin schema definitions.
+ * @param string Table name
+ * @param string Plugin basename (defaults to current plugin)
+ * @return string Full table name
+ */
+function plugin_table( $p_name, $p_basename=null ) {
+	if ( is_null( $p_basename ) ) {
+		$t_current = plugin_get_current();
+	} else {
+		$t_current = $p_basename;
+	}
+	return config_get_global( 'db_table_prefix' ) .
+		'_plugin_' . $t_current . '_' . $p_name .
+		config_get_global( 'db_table_suffix' );
+}
+
+/**
+ * Hook a plugin's callback function to an event.
+ * @param string Event name
+ * @param string Callback function
+ */
+function plugin_event_hook( $p_name, $p_callback ) {
+	$t_basename = plugin_get_current();
+	$t_function = 'plugin_event_' . $t_basename . '_' . $p_callback;
+	event_hook( $p_name, $t_function, $t_basename );
+}
+
+### Plugin management functions
+
+/**
+ * Include the appropriate script for a plugin.
+ * @param srting Plugin basename
+ * @param boolean Include events script
+ */
+function plugin_include( $p_basename, $p_include_events=false ) {
+	$t_path = config_get_global( 'plugin_path' );
+
+	$t_register_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'register.php';
+	if ( is_file( $t_register_file ) ) {
+		include_once( $t_register_file );
+	}
+
+	if ( $p_include_events ) {
+		$t_events_file = $t_path.$p_basename.DIRECTORY_SEPARATOR.'events.php';
+		if ( is_file( $t_events_file ) ) {
+			include_once( $t_events_file );
+		}
+	}
+}
+
+/**
+ * Get the script information from the script file or cache.
+ * @param string Plugin basename
+ * @return array Script information
+ */
+function plugin_get_info( $p_basename ) {
+	global $g_plugin_cache;
+
+	if ( plugin_is_registered( $p_basename ) ) {
+		return $g_plugin_cache[$p_basename];
+	}
+
+	plugin_push_current( $p_basename );
+
+	plugin_include( $p_basename );
+
+	$t_plugin_info = null;
+
+	$t_info_function = 'plugin_callback_'.$p_basename.'_info';
+	if ( function_exists( $t_info_function ) ) {
+		$t_plugin_info = $t_info_function();
+	}
+
+	plugin_pop_current();
+
+	return $t_plugin_info;
+}
+
+/**
+ * Get the upgrade schema for a given plugin.
+ * @param string Plugin basename
+ * @return array Upgrade schema in same format as Mantis schema
+ */
+function plugin_get_schema( $p_basename ) {
+
+	plugin_include( $p_basename );
+
+	$t_schema_function = 'plugin_callback_'.$p_basename.'_schema';
+	if ( !function_exists( $t_schema_function ) ) {
+		return null;
+	}
+
+	plugin_push_current( $p_basename );
+	$t_schema = $t_schema_function();
+	plugin_pop_current();
+
+	if ( is_array( $t_schema ) ) {
+		return $t_schema;
+	}
+
+	return null;
+}
+
+/**
+ * List all installed plugins.
+ * @return array Installed plugins
+ */
+function plugin_get_installed() {
+	$t_plugin_table = config_get_global( 'mantis_plugin_table' );
+
+	$t_query = "SELECT * FROM $t_plugin_table";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array( 'mantis' => '1' );
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_basename = $t_row['basename'];
+		$t_plugins[$t_basename] = $t_row['enabled'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * List enabled plugins.
+ * @return array Enabled plugin basenames
+ */
+function plugin_get_enabled() {
+	$t_plugin_table = config_get_global( 'mantis_plugin_table' );
+
+	$t_query = "SELECT basename FROM $t_plugin_table WHERE enabled=1";
+	$t_result = db_query( $t_query );
+
+	$t_plugins = array();
+	while( $t_row = db_fetch_array( $t_result ) ) {
+		$t_plugins[] = $t_row['basename'];
+	}
+
+	return $t_plugins;
+}
+
+/**
+ * Search the plugins directory for plugins.
+ * @return array Plugin basename/info key/value pairs.
+ */
+function plugin_find_all() {
+	$t_plugin_path = config_get_global( 'plugin_path' );
+	$t_plugins = array( 'mantis' => plugin_get_info( 'mantis' ) );
+
+	if ( $t_dir = opendir( $t_plugin_path ) ) {
+		while ( ($t_file = readdir( $t_dir )) !== false ) {
+			if ( '.' == $t_file || '..' == $t_file ) {
+				continue;
+			}
+			if ( is_dir( $t_plugin_path.$t_file ) ) {
+				$t_plugin_info = plugin_get_info( $t_file );
+				if ( !is_null( $t_plugin_info ) ) {
+					$t_plugins[$t_file] = $t_plugin_info;
+				}
+			}
+		}
+		closedir( $t_dir );
+	}
+	return $t_plugins;
+}
+
+/**
+ * Determine if a given plugin is installed.
+ * @param string Plugin basename
+ * @retrun boolean True if plugin is installed
+ */
+function plugin_is_installed( $p_basename ) {
+	$t_plugin_table	= config_get_global( 'mantis_plugin_table' );
+	$c_basename 	= db_prepare_string( $p_basename );
+
+	$t_query = "SELECT COUNT(*) FROM $t_plugin_table WHERE basename=" . db_param(0);
+	$t_result = db_query_bound( $t_query, array( $c_basename ) );
+	return ( 0 < db_result( $t_result ) );
+}
+
+/**
+ * Install a plugin to the database.
+ * @param string Plugin basename
+ */
+function plugin_install( $p_basename ) {
+	access_ensure_global_level( config_get_global( 'manage_plugin_threshold' ) );
+
+	if ( plugin_is_installed( $p_basename ) ) {
+		trigger_error( ERROR_PLUGIN_ALREADY_INSTALLED, WARNING );
+		return null;
+	}
+
+	$t_plugin_table	= config_get_global( 'mantis_plugin_table' );
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "INSERT INTO $t_plugin_table ( basename, enabled )
+				VALUES ( ".db_param(0).", 1 )";
+	db_query_bound( $t_query, array( $c_basename ) );
+
+	if ( false === ( config_get_global( 'schema_plugin_'.$p_basename, false ) ) ) {
+		config_set( 'plugin_' . $p_basename . '_schema', -1 );
+	}
+	plugin_upgrade( $p_basename );
+}
+
+/**
+ * Determine if an installed plugin needs to upgrade its schema.
+ * @param string Plugin basename
+ * @return boolean True if plugin needs schema ugrades.
+ */
+function plugin_needs_upgrade( $p_basename ) {
+	$t_plugin = plugin_get_info( $p_basename );
+
+	$t_plugin_schema = plugin_get_schema( $p_basename );
+	if ( is_null( $t_plugin_schema ) ) {
+		return false;
+	}
+
+	$t_plugin_schema_version = config_get_global( 'plugin_schema_'.$p_basename, -1 );
+
+	return ( $t_plugin_schema_version < count( $t_plugin_schema ) - 1 );
+}
+
+/**
+ * Upgrade an installed plugin's schema.
+ * @param string Plugin basename
+ * @return multi True if upgrade completed, null if problem
+ */
+function plugin_upgrade( $p_basename ) {
+	access_ensure_global_level( config_get_global( 'manage_plugin_threshold' ) );
+
+	$t_schema_version = config_get_global( 'plugin_' . $p_basename . '_schema', -1 );
+	$t_schema = plugin_get_schema( $p_basename );
+
+	global $g_db;
+	$t_dict = NewDataDictionary( $g_db );
+
+	$i = $t_schema_version + 1;
+	while ( $i < count( $t_schema ) ) {
+		$t_target = $t_schema[$i][1][0];
+
+		if ( $t_schema[$i][0] == 'InsertData' ) {
+			$t_sqlarray = call_user_func_array( $t_schema[$i][0], $t_schema[$i][1] );
+		} else if ( $t_schema[$i][0] == 'UpdateSQL' ) {
+			$t_sqlarray = array( $t_schema[$i][1] );
+			$t_target = $t_schema[$i][1];
+		} else {
+			$t_sqlarray = call_user_func_array( Array( $t_dict, $t_schema[$i][0] ), $t_schema[$i][1] );
+		}
+		$t_status = $t_dict->ExecuteSQLArray( $t_sqlarray );
+
+		if ( 2 == $t_status ) {
+			config_set( 'plugin_' . $p_basename . '_schema', $i );
+		} else {
+			return null;
+		}
+
+		$i++;
+	}
+
+	return true;
+}
+
+/**
+ * Uninstall a plugin from the database.
+ * @param string Plugin basename
+ */
+function plugin_uninstall( $p_basename ) {
+	access_ensure_global_level( config_get_global( 'manage_plugin_threshold' ) );
+
+	if ( !plugin_is_installed( $p_basename ) ) {
+		return;
+	}
+
+	$t_plugin_table	= config_get_global( 'mantis_plugin_table' );
+	$c_basename = db_prepare_string( $p_basename );
+
+	$t_query = "DELETE FROM $t_plugin_table WHERE basename=" . db_param(0);
+	db_query_bound( $t_query, array( $c_basename ) );
+}
+
+### Core usage only.
+
+/**
+ * Initialize all enabled plugins.
+ * Post-signals EVENT_PLUGIN_INIT.
+ */
+function plugin_init_all() {
+	if ( OFF == config_get_global( 'plugins_enabled' ) ) {
+		return;
+	}
+
+	global $g_plugin_cache;
+	if ( !isset( $g_plugin_cache ) ) {
+		$g_plugin_cache = array();
+	}
+
+	global $g_plugin_current;
+	if ( !isset( $g_plugin_current ) ) {
+		$g_plugin_current = array();
+	}
+
+	# Initial plugin for version dependencies
+	$g_plugin_cache['mantis'] = array(
+		'name' => 'Mantis Bug Tracker',
+		'description' => 'The Mantis plugin core API.',
+		'version' => MANTIS_VERSION,
+		'requires' => array(),
+		'author' => 'Mantis Team',
+		'url' => 'http://www.mantisbt.org',
+	);
+
+	plugin_init_array( plugin_get_enabled() );
+
+	event_signal( 'EVENT_PLUGIN_INIT' );
+}
+
+/**
+ * Recursive plugin initialization to handle dependencies.
+ * @param array Plugin basenames to initialize.
+ */
+function plugin_init_array( $p_plugins, $p_depth=0 ) {
+	$t_plugins_retry = array();
+
+	foreach( $p_plugins as $t_basename ) {
+		if ( !plugin_init( $t_basename ) ) {
+			# Dependent plugin
+			$t_plugins_retry[] = $t_basename;
+		}
+	}
+
+	# Recurse on dependent plugins
+	if ( $p_depth < count( $p_plugins ) ) {
+		plugin_init_array( $t_plugins_retry, $p_depth + 1 );
+	}
+}
+
+/**
+ * Initialize a single plugin.
+ * @param string Plugin basename
+ * @return boolean True if plugin initialized, false otherwise.
+ */
+function plugin_init( $p_basename ) {
+	global $g_plugin_cache;
+
+	$t_plugin_info = plugin_get_info( $p_basename );
+
+	if ( $t_plugin_info !== null ) {
+		$g_plugin_cache[$p_basename] = $t_plugin_info;
+
+		# handle dependent plugins
+		if ( isset( $t_plugin_info['requires'] ) ) {
+			foreach ( $t_plugin_info['requires'] as $t_required => $t_version ) {
+				if ( !isset( $g_plugin_cache[$t_required] ) ||
+					( !is_null( $t_version ) &&
+					$g_plugin_cache[$t_required]['version'] < $t_version ) ) {
+					return false;
+				}
+			}
+		}
+
+		$t_init_function = 'plugin_callback_'.$p_basename.'_init';
+		if ( function_exists( $t_init_function ) ) {
+			plugin_push_current( $p_basename );
+			$t_init_function();
+			plugin_pop_current();
+		}
+	}
+
+	return true;
+}
+
diff --git a/core/string_api.php b/core/string_api.php
index e418d46..a7d2baf 100644
--- a/core/string_api.php
+++ b/core/string_api.php
@@ -101,7 +101,7 @@
 		$p_string = string_preserve_spaces_at_bol( $p_string );
 		$p_string = string_nl2br( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -111,7 +111,7 @@
 		$p_string = string_html_specialchars( $p_string );
 		$p_string = string_restore_valid_html_tags( $p_string, /* multiline = */ false );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_GENERAL', $p_string );
 	}
 
 	# --------------------
@@ -124,7 +124,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -137,7 +137,7 @@
 		$p_string = string_process_bugnote_link( $p_string );
 		$p_string = string_process_cvs_link( $p_string );
 
-		return $p_string;
+		return event_signal( 'EVENT_TEXT_LINKS', $p_string );
 	}
 
 	# --------------------
@@ -159,7 +159,7 @@
 		# another escaping to escape the special characters created by the generated links
 		$t_string = string_html_specialchars( $t_string );
 
-		return $t_string;
+		return event_signal( 'EVENT_TEXT_RSS', $p_string );
 	}
 
 	# --------------------
diff --git a/css/default.css b/css/default.css
index 3cf870c..3d6ed07 100644
--- a/css/default.css
+++ b/css/default.css
@@ -32,6 +32,10 @@ span.small 			{ font-size: 8pt; font-weight: normal; }
 span.pagetitle		{ font-size: 12pt; font-weight: bold; text-align: center }
 span.bracket-link	{ white-space: nowrap; }
 
+span.text_brown		{ color: brown; }
+span.text_green		{ color: green; }
+span.text_red		{ color: red; }
+
 table				{ }
 table.hide			{ width: 100%; border: solid 0px #ffffff; }
 table.width100		{ width: 100%; border: solid 1px #000000; }
diff --git a/lang/strings_english.txt b/lang/strings_english.txt
index e17a93a..2831d5e 100644
--- a/lang/strings_english.txt
+++ b/lang/strings_english.txt
@@ -296,6 +296,10 @@ $MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
 $MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
 $MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 $MANTIS_ERROR[ERROR_TOKEN_NOT_FOUND] = 'Token could not be found.';
+$MANTIS_ERROR[ERROR_EVENT_UNDECLARED] = 'Event has not yet been declared.';
+$MANTIS_ERROR[ERROR_PLUGIN_NOT_REGISTERED] = 'Plugin is not registered with Mantis.';
+$MANTIS_ERROR[ERROR_PLUGIN_ALREADY_INSTALLED] = 'Plugin is already installed.';
+$MANTIS_ERROR[ERROR_PLUGIN_PAGE_NOT_FOUND] = 'Plugin page not found.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -706,6 +710,7 @@ $s_manage_users_link = 'Manage Users';
 $s_manage_projects_link = 'Manage Projects';
 $s_manage_custom_field_link = 'Manage Custom Fields';
 $s_manage_global_profiles_link = 'Manage Global Profiles';
+$s_manage_plugin_link = 'Manage Plugins';
 $s_permissions_summary_report = 'Permissions Report';
 $s_manage_config_link = 'Manage Configuration';
 $s_manage_threshold_config = 'Workflow Thresholds';
@@ -798,6 +803,21 @@ $s_all_users = 'All Users';
 $s_set_configuration_option = 'Set Configuration Option';
 $s_delete_config_sure_msg = 'Are you sure you wish to delete this configuration option?';
 
+# manage_plugin_page.php
+$s_plugin = 'Plugin';
+$s_plugins_installed = 'Installed Plugins';
+$s_plugins_available = 'Available Plugins';
+$s_plugin_description = 'Description';
+$s_plugin_author = 'Author: %s';
+$s_plugin_url = 'Website: %s';
+$s_plugin_depends = 'Dependencies';
+$s_plugin_no_depends = 'No dependencies';
+$s_plugin_actions = 'Actions';
+$s_plugin_install = 'Install';
+$s_plugin_upgrade = 'Upgrade';
+$s_plugin_uninstall = 'Uninstall';
+$s_plugin_uninstall_message = 'Are you sure you want to uninstall the \'%s\' plugin?';
+
 # manage_proj_add.php
 $s_project_added_msg = 'Project has been successfully added...';
 
diff --git a/manage_plugin_install.php b/manage_plugin_install.php
new file mode 100644
index 0000000..998ad70
--- /dev/null
+++ b/manage_plugin_install.php
@@ -0,0 +1,33 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+plugin_install( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_page.php b/manage_plugin_page.php
new file mode 100644
index 0000000..23e2a30
--- /dev/null
+++ b/manage_plugin_page.php
@@ -0,0 +1,235 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+html_page_top1( lang_get( 'manage_plugin_link' ) );
+html_page_top2();
+
+print_manage_menu( 'manage_plugin_page.php' );
+
+$t_plugins = plugin_find_all();
+$t_plugins_installed = plugin_get_installed();
+
+$t_plugins_available = array();
+foreach( $t_plugins as $t_basename => $t_info ) {
+	if ( !isset( $t_plugins_installed[$t_basename] ) ) {
+		$t_plugins_available[$t_basename] = $t_info;
+	}
+}
+
+?>
+
+<?php if ( 0 < count( $t_plugins_installed ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_installed' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'plugin_actions' ) ?></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_installed as $t_basename => $t_enabled ) {
+	$t_description = string_display_links( $t_plugins[$t_basename]['description'] );
+	$t_author = $t_plugins[$t_basename]['author'];
+	$t_contact = $t_plugins[$t_basename]['contact'];
+	$t_page = $t_plugins[$t_basename]['page'] ;
+	$t_url = $t_plugins[$t_basename]['url'] ;
+	$t_requires = $t_plugins[$t_basename]['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_plugins[$t_basename]['name'].' '.$t_plugins[$t_basename]['version'] );
+	if ( !is_null( $t_page ) && !is_blank( $t_page ) ) {
+		$t_name = '<a href="' . string_attribute( plugin_page( $t_page, $t_basename ) ) . '">' . $t_name . '</a>';
+	}
+
+	if ( !is_null( $t_author ) && !is_blank( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_attribute( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<span class="text_green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</span>';
+					} else {
+						$t_depends[] = '<span class="text_brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</span>';
+					}
+				} else {
+					$t_depends[] = '<span class="text_red">'.string_display( $t_plugin.' '.$t_version ).'</span>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<span class="text_green">' . lang_get( 'plugin_no_depends' ) . '</span>';
+	}
+
+	$t_upgrade = '';
+	if ( plugin_needs_upgrade( $t_basename ) ) {
+		$t_upgrade = '<form action="manage_plugin_upgrade.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_upgrade' ).'"></form>';
+	}
+	
+	$t_uninstall = '';
+	if ( 'mantis' != $t_basename ) {
+		$t_uninstall = '<form action="manage_plugin_uninstall.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_uninstall' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_upgrade,$t_uninstall,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php if ( 0 < count( $t_plugins_available ) ) { ?>
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="7">
+		<?php echo lang_get( 'plugins_available' ) ?>
+
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="20%"><?php echo lang_get( 'plugin' ) ?></td>
+	<td width="50%"><?php echo lang_get( 'plugin_description' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'plugin_depends' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'plugin_actions' ) ?></td>
+</tr>
+
+<?php 
+foreach ( $t_plugins_available as $t_basename => $t_info ) {
+	$t_description = string_display_links( $t_info['description'] );
+	$t_author = $t_info['author'];
+	$t_contact = $t_info['contact'];
+	$t_url = $t_info['url'] ;
+	$t_requires = $t_info['requires'];
+	$t_depends = array();
+
+	$t_name = string_display( $t_info['name'].' '.$t_info['version'] );
+
+	if ( !is_null( $t_author ) ) {
+		if ( is_array( $t_author ) ) {
+			$t_author = implode( $t_author, ', ' );
+		}
+		if ( !is_null( $t_contact ) && !is_blank( $t_contact ) ) {
+			$t_author = '<br/>' . sprintf( lang_get( 'plugin_author' ), 
+				'<a href="mailto:' . string_display( $t_contact ) . '">' . string_display( $t_author ) . '</a>' );
+		} else {
+			$t_author = '<br/>' . string_display( sprintf( lang_get( 'plugin_author' ), $t_author ) );
+		}
+	}
+
+	if ( !is_null( $t_url ) && !is_blank( $t_url ) ) {
+		$t_url = '<br/>' . string_display_links( sprintf( lang_get( 'plugin_url' ), $t_url ) );
+	}
+
+	$t_ready = true;
+	if ( !is_null( $t_requires ) ) {
+		if ( is_array( $t_requires ) ) {
+			foreach( $t_requires as $t_plugin => $t_version ) {
+				if ( isset( $t_plugins[$t_plugin] ) ) {
+					if ( isset( $t_plugins_installed[$t_plugin] ) &&
+						$t_plugins[$t_plugin]['version'] >= $t_version ) {
+						$t_depends[] = '<font color="green">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					} else {
+						$t_ready = false;
+						$t_depends[] = '<font color="brown">'.string_display( $t_plugins[$t_plugin]['name'].' '.$t_version ).'</font>';
+					}
+				} else {
+					$t_ready = false;
+					$t_depends[] = '<font color="red">'.string_display( $t_plugin.' '.$t_version ).'</font>';
+				}
+			}
+		}
+	}
+
+	if ( 0 < count( $t_depends ) ) {
+		$t_depends = implode( $t_depends, '<br/>' );
+	} else {
+		$t_depends = '<font color="green">' . lang_get( 'plugin_no_depends' ) . '</font>';
+	}
+
+	$t_install = '';
+	if ( $t_ready ) {
+		$t_install = '<form action="manage_plugin_install.php?name='.$t_basename.'" method="post">'.
+			'<input type="submit" value="'.lang_get( 'plugin_install' ).'"></form>';
+	}
+
+	echo '<tr ',helper_alternate_class(),'>';
+	echo '<td class="center">',$t_name,'</td>';
+	echo '<td>',$t_description,$t_author,$t_url,'</td>';
+	echo '<td class="center">',$t_depends,'</td>';
+	echo '<td class="center">',$t_install,'</td>';
+	echo '</tr>';
+} ?>
+
+</table>
+<?php } ?>
+
+<?php
+html_page_bottom1();
+
diff --git a/manage_plugin_uninstall.php b/manage_plugin_uninstall.php
new file mode 100644
index 0000000..af511da
--- /dev/null
+++ b/manage_plugin_uninstall.php
@@ -0,0 +1,36 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+$t_plugin_info = plugin_get_info( $f_basename );
+
+helper_ensure_confirmed( sprintf( lang_get( 'plugin_uninstall_message' ), $t_plugin_info['name'] ), lang_get( 'plugin_uninstall' ) );
+
+plugin_uninstall( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/manage_plugin_upgrade.php b/manage_plugin_upgrade.php
new file mode 100644
index 0000000..95947af
--- /dev/null
+++ b/manage_plugin_upgrade.php
@@ -0,0 +1,33 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------
+# $Id: $
+# --------------------------------------------------------
+
+require_once( 'core.php' );
+
+auth_reauthenticate();
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_basename = gpc_get_string( 'name' );
+
+$t_status = plugin_upgrade( $f_basename );
+
+print_successful_redirect( 'manage_plugin_page.php' );
diff --git a/plugin.php b/plugin.php
new file mode 100644
index 0000000..4083835
--- /dev/null
+++ b/plugin.php
@@ -0,0 +1,43 @@
+<?php
+# Mantis - a php based bugtracking system
+
+# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once( 'core.php' );
+$t_plugin_path = config_get( 'plugin_path' );
+
+$f_page= gpc_get_string( 'page' );
+$t_matches = array();
+
+if ( ! preg_match( '/^([a-zA-Z0-9_-]*)\/([a-zA-Z0-9_-]*)/', $f_page, $t_matches ) ) {
+		trigger_error( ERROR_GENERIC, ERROR );
+}
+
+$t_basename = $t_matches[1];
+$t_action = $t_matches[2];
+
+plugin_ensure_registered( $t_basename );
+
+$t_page = $t_plugin_path.$t_basename.DIRECTORY_SEPARATOR.
+		'pages'.DIRECTORY_SEPARATOR.$t_action.'.php';
+
+if ( !is_file( $t_page ) ) {
+		trigger_error( ERROR_PLUGIN_PAGE_NOT_FOUND, ERROR );
+}
+
+include( $t_page );
+
diff --git a/plugins/debug/events.php b/plugins/debug/events.php
new file mode 100644
index 0000000..f2e3968
--- /dev/null
+++ b/plugins/debug/events.php
@@ -0,0 +1,9 @@
+<?php
+
+function plugin_event_debug_dump() {
+	global $g_plugin_cache, $g_event_cache;
+
+	echo '<pre>';
+	var_dump( $g_plugin_cache, $g_event_cache );
+	echo '</pre>';
+}
diff --git a/plugins/debug/register.php b/plugins/debug/register.php
new file mode 100644
index 0000000..9fd8077
--- /dev/null
+++ b/plugins/debug/register.php
@@ -0,0 +1,16 @@
+<?php
+
+function plugin_callback_debug_info() {
+	return array( 
+		'name' => 'Plugin Debugger',
+		'version' => '1.0',
+		'description' => 'Outputs useful debugging information for plugin developers.',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+function plugin_callback_debug_init() {
+	plugin_event_hook( 'EVENT_PAGE_END', 'dump' );
+}
diff --git a/plugins/supercow/events.php b/plugins/supercow/events.php
new file mode 100644
index 0000000..d90f5e0
--- /dev/null
+++ b/plugins/supercow/events.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * Handle the EVENT_PLUGIN_INIT callback.
+ */
+function plugin_event_supercow_header() {
+	header( 'X-Mantis: This Mantis has super cow powers.' );
+}
diff --git a/plugins/supercow/register.php b/plugins/supercow/register.php
new file mode 100644
index 0000000..c4905fd
--- /dev/null
+++ b/plugins/supercow/register.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Return plugin details to the API.
+ * @return array Plugin details
+ */
+function plugin_callback_supercow_info() {
+	return array(
+		'name' => 'Super Cow Powers',
+		'description' => 'Gives your Mantis installation super cow powers.',
+		'version' => '1.0',
+		'author' => 'John Reese',
+		'contact' => 'jreese@leetcode.net',
+		'url' => 'http://leetcode.net',
+	);
+}
+
+/**
+ * Register callback methods for any events necessary.
+ */
+function plugin_callback_supercow_init() {
+	plugin_event_hook( 'EVENT_PLUGIN_INIT', 'header' );
+}
mantis-plugins-2007-11-01.patch (50,196 bytes)   

Relationships

has duplicate 0003361 closedjreese How about plugins?! 

Activities

jreese

jreese

2007-10-28 12:57

reporter   ~0016005

I've attached a new version of the patch that conforms to the new Mantis version as changed in 0008063, and that uses the new db_query_bound() introduced by Paul.

jreese

jreese

2007-10-30 15:34

reporter   ~0016032

Recent insights have lead me to realize that the event system that I created for plugins should really be separated into its own separate API. I have also moved initialization of the new event system before the rest of the feature API's. This will allow for other API's and future additions to take advantage of event-based processing for various reasons.

I've refactored the Plugin API to make use of the separated event system, and testing shows that I have not (yet) introduced any new problems with this decision. This actually makes both pieces simpler to maintain, and there is almost negligible increase in diff size.

jreese

jreese

2007-11-01 20:44

reporter   ~0016059

Latest patch generated from old CVS Head (don't have time to move all my changes between Git repos =\ ). This has most of the suggestions made by Victor in it. The exceptions are ones that I felt either were inconsistent with the rest of Mantis, or made the code less readable with no functional change. I'm hoping this will be the last large set of changes, and that I can get this submitted to SVN by the end of the week.

jreese

jreese

2008-03-05 11:34

reporter   ~0017262

At this point, I think we can consider the plugin system stable enough to mark this as resolved.