<?php
# MantisBT - A PHP based bugtracking system

# MantisBT 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.
#
# MantisBT 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 MantisBT.  If not, see <http://www.gnu.org/licenses/>.


/**
 * Plugin API
 *
 * Handles the initialisation, management, and execution of plugins.
 *
 * @package CoreAPI
 * @subpackage PluginAPI
 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 *
 * @uses access_api.php
 * @uses config_api.php
 * @uses constant_inc.php
 * @uses database_api.php
 * @uses error_api.php
 * @uses event_api.php
 * @uses file_api.php
 * @uses helper_api.php
 * @uses history_api.php
 * @uses lang_api.php
 * @uses logging_api.php
 */

use Mantis\classes\MissingHooksPlugin;

require_api( 'access_api.php' );
require_api( 'config_api.php' );
require_api( 'constant_inc.php' );
require_api( 'database_api.php' );
require_api( 'error_api.php' );
require_api( 'event_api.php' );
require_api( 'file_api.php' );
require_api( 'helper_api.php' );
require_api( 'history_api.php' );
require_api( 'lang_api.php' );
require_api( 'logging_api.php' );

/**
 * Installed Plugins cache
 * @global MantisPlugin[] $g_plugin_cache Basename is used as key.
 */
$g_plugin_cache = array();

/**
 * Initialized Plugins cache
 * @global bool[] $g_plugin_cache True if plugin is loaded; Basename is used as key.
 * @see plugin_is_loaded()
 */
$g_plugin_cache_init = array();

/**
 * Plugins priority cache
 * @global int[] $g_plugin_cache_priority Basename is used as key.
 */
$g_plugin_cache_priority = array();

/**
 * Plugins protected status cache
 * @global bool[] $g_plugin_cache_protected Basename is used as key.
 */
$g_plugin_cache_protected = array();

/**
 * Current plugin stack.
 *
 * Stores the plugin basename.
 * @see plugin_push_current(), plugin_get_current(), plugin_pop_current()
 * @global string[] $g_plugin_current
 */
$g_plugin_current = array();


/**
 * 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( $g_plugin_current[0] ?? null );
}

/**
 * Add the current plugin to the stack.
 *
 * @param string $p_base_name Plugin base name.
 *
 * @return void
 */
function plugin_push_current( $p_base_name ) {
	global $g_plugin_current;
	array_unshift( $g_plugin_current, $p_base_name );
}

/**
 * Remove the current plugin from the stack.
 *
 * @return string|null 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 );
}

/**
 * Returns the list of force-installed plugins.
 *
 * @see $g_plugins_force_installed
 *
 * @return array List of plugins (basename => priority)
 */
function plugin_get_force_installed() {
	$t_forced_plugins = config_get_global( 'plugins_force_installed' );

	# MantisCore pseudo-plugin is force-installed by definition
	$t_forced_plugins['MantisCore'] = PLUGIN_PRIORITY_HIGH;

	return $t_forced_plugins;
}

/**
 * Returns an object representing the specified plugin.
 *
 * Triggers an error if the plugin is not registered
 *
 * @param string|null $p_basename Plugin base name (defaults to current plugin).
 *
 * @return MantisPlugin Plugin Object
 */
function plugin_get( $p_basename = null ) {
	global $g_plugin_cache;

	if( is_null( $p_basename ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_basename;
	}

	if( !plugin_is_registered( $t_current ) ) {
		error_parameters( $t_current );
		trigger_error( ERROR_PLUGIN_NOT_REGISTERED, ERROR );
	}

	return $g_plugin_cache[$t_current];
}

/**
 * Get the URL to the plugin wrapper page.
 *
 * @param string $p_page      Page name.
 * @param bool   $p_redirect  Return url for redirection.
 * @param string $p_base_name Plugin base name (defaults to current plugin).
 *
 * @return string
 */
function plugin_page( $p_page, $p_redirect = false, $p_base_name = null ) {
	if( is_null( $p_base_name ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_base_name;
	}
	if( $p_redirect ) {
		return 'plugin.php?page=' . $t_current . '/' . $p_page;
	} else {
		return helper_mantis_url( 'plugin.php?page=' . $t_current . '/' . $p_page );
	}
}

/**
 * Gets the Route Group.
 *
 * The Route Group is the base path under '/api/rest', e.g. /plugins/Example.
 *
 * @param string $p_base_name The basename for plugin or null for current plugin.
 *
 * @return string The route group path to use.
 */
function plugin_route_group( $p_base_name = null ) {
	if( is_null( $p_base_name ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_base_name;
	}

	return '/plugins/' . $t_current;
}

/**
 * Returns the path to a plugin file, or the plugin's files directory.
 *
 * @param string $p_filename  File name; if not set, the function will return the plugin's files directory.
 * @param string $p_base_name Plugin base name (defaults to the current plugin).
 *
 * @return string|false File path or false if file cannot be found.
 */
function plugin_file_path( $p_filename = '', $p_base_name = '' ) {
	$t_file_path = config_get_global( 'plugin_path' )
		. ( $p_base_name ?: plugin_get_current() ) . DIRECTORY_SEPARATOR
		. 'files' . DIRECTORY_SEPARATOR;

	if( !$p_filename ) {
		return $t_file_path;
	}
	$t_file_path .= $p_filename;

	return( is_file( $t_file_path ) ? $t_file_path : false );
}

/**
 * Get the URL to the plugin wrapper file page.
 *
 * @param string $p_file      File name.
 * @param bool   $p_redirect  Return url for redirection.
 * @param string $p_base_name Plugin base name (defaults to current plugin).
 *
 * @return string
 */
function plugin_file( $p_file, $p_redirect = false, $p_base_name = null ) {
	if( is_null( $p_base_name ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_base_name;
	}
	if( $p_redirect ) {
		return 'plugin_file.php?file=' . $t_current . '/' . $p_file;
	} else {
		return helper_mantis_url( 'plugin_file.php?file=' . $t_current . '/' . $p_file );
	}
}

/**
 * Include the contents of a file as output.
 *
 * @param string $p_filename File name.
 * @param string $p_basename Plugin basename.
 *
 * @return void
 */
function plugin_file_include( $p_filename, $p_basename = null ) {
	global $g_plugin_mime_types;

	if( is_null( $p_basename ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_basename;
	}

	$t_file_path = plugin_file_path( $p_filename, $t_current );
	if( false === $t_file_path ) {
		error_parameters( $t_current, $p_filename );
		trigger_error( ERROR_PLUGIN_FILE_NOT_FOUND, ERROR );
	}

	$t_content_type = '';

	# allow overriding the content type for specific text and image extensions
	# see bug #13193 for details
	$t_extension = pathinfo( $t_file_path, PATHINFO_EXTENSION );
	if( $t_extension && array_key_exists( $t_extension, $g_plugin_mime_types ) ) {
		$t_content_type = $g_plugin_mime_types[$t_extension];
	}

	if( !$t_content_type ) {
		$t_file_info_type = file_get_mime_type( $t_file_path );
		if( $t_file_info_type !== false ) {
			$t_content_type = $t_file_info_type;
		}
	}

	if( $t_content_type ) {
		header( 'Content-Type: ' . $t_content_type );
	}

	$t_mtime = @filemtime( $t_file_path );
	header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s \G\M\T', $t_mtime ) );
	if( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] )
		&& ( $t_mtime <= strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ) {
		http_response_code( HTTP_STATUS_NOT_MODIFIED );
	} else {
		readfile( $t_file_path );
	}
}

/**
 * Given a base table name for a plugin, add appropriate prefix and suffix.
 *
 * Convenience for plugin schema definitions.
 *
 * @param string $p_name     Table name.
 * @param string $p_basename 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;
	}

	# Determine plugin table prefix including trailing '_'
	$t_prefix = trim( config_get_global( 'db_table_plugin_prefix' ) );
	if( !empty( $t_prefix ) && '_' != substr( $t_prefix, -1 ) ) {
		$t_prefix .= '_';
	}

	return db_get_table( $t_prefix . $t_current . '_' . $p_name );
}

/**
 * Get a plugin configuration option.
 *
 * @param string $p_option  Configuration option name.
 * @param mixed  $p_default Default option value.
 * @param bool   $p_global  Get global config variables only.
 * @param int    $p_user    A user identifier.
 * @param int    $p_project A Project identifier.
 *
 * @return mixed
 */
function plugin_config_get( $p_option, $p_default = null, $p_global = false, $p_user = null, $p_project = null ) {
	$t_basename = plugin_get_current();
	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;

	if( $p_global ) {
		return config_get_global( $t_full_option, $p_default );
	} else {
		return config_get( $t_full_option, $p_default, $p_user, $p_project );
	}
}

/**
 * Set a plugin configuration option in the database.
 *
 * @param string $p_option  Configuration option name.
 * @param mixed  $p_value   Option value.
 * @param int    $p_user    A user identifier.
 * @param int    $p_project A project identifier.
 * @param int    $p_access  Access threshold.
 *
 * @return void
 */
function plugin_config_set( $p_option, $p_value, $p_user = NO_USER, $p_project = ALL_PROJECTS, $p_access = DEFAULT_ACCESS_LEVEL ) {
	if( $p_access == DEFAULT_ACCESS_LEVEL ) {
		$p_access = config_get_global( 'admin_site_threshold' );
	}

	$t_basename = plugin_get_current();
	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;

	config_set( $t_full_option, $p_value, $p_user, $p_project, $p_access );
}

/**
 * Delete a plugin configuration option from the database.
 *
 * @param string $p_option  Configuration option name.
 * @param int    $p_user    A user identifier.
 * @param int    $p_project A project identifier.
 *
 * @return void
 */
function plugin_config_delete( $p_option, $p_user = ALL_USERS, $p_project = ALL_PROJECTS ) {
	$t_basename = plugin_get_current();
	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;

	config_delete( $t_full_option, $p_user, $p_project );
}

/**
 * Set plugin default values to global values without overriding anything.
 *
 * @param array $p_options Array of configuration option name/value pairs.
 *
 * @return void
 */
function plugin_config_defaults( array $p_options ) {
	$t_basename = plugin_get_current();
	$t_option_base = 'plugin_' . $t_basename . '_';

	foreach( $p_options as $t_option => $t_value ) {
		$t_full_option = $t_option_base . $t_option;

		config_set_global( $t_full_option, $t_value, false );
	}
}

/**
 * Get a language string for the plugin.
 *
 * Automatically prepends plugin_<basename> to the string requested.
 * @param string $p_name     Language string name.
 * @param string $p_basename Plugin basename.
 *
 * @return string Language string
 */
function plugin_lang_get( $p_name, $p_basename = null ) {
	if( !is_null( $p_basename ) ) {
		plugin_push_current( $p_basename );
	}

	$t_basename = plugin_get_current();
	$t_name = 'plugin_' . $t_basename . '_' . $p_name;
	$t_string = lang_get( $t_name );

	if( !is_null( $p_basename ) ) {
		plugin_pop_current();
	}
	return $t_string;
}

/**
 * Get a defaulted language string for the plugin.
 *
 * Automatically prepends `plugin_<basename>` to the string requested.
 * - If found, return the appropriate string.
 * - If not found, no default supplied, return the supplied string as is.
 * - If not found, default supplied, return default.
 * @see lang_get_defaulted()
 *
 * @param string $p_name     Language string name.
 * @param string $p_default  The default value to return.
 * @param string $p_basename Plugin basename.
 *
 * @return string Language string
 */
function plugin_lang_get_defaulted( $p_name, $p_default = null, $p_basename = null ) {
	if( !is_null( $p_basename ) ) {
		plugin_push_current( $p_basename );
	}
	$t_basename = plugin_get_current();
	$t_name = 'plugin_' . $t_basename . '_' . $p_name;
	$t_string = lang_get_defaulted( $t_name, $p_default );

	if( !is_null( $p_basename ) ) {
		plugin_pop_current();
	}
	return $t_string;
}

/**
 * log history event from plugin.
 *
 * @param int    $p_bug_id     A bug identifier.
 * @param string $p_field_name A field name.
 * @param string $p_old_value  The old value.
 * @param string $p_new_value  The new value.
 * @param int    $p_user_id    A user identifier.
 * @param string $p_basename   The plugin basename (or current plugin if null).
 *
 * @return void
 */
function plugin_history_log( $p_bug_id, $p_field_name, $p_old_value, $p_new_value = '', $p_user_id = null, $p_basename = null ) {
	if( is_null( $p_basename ) ) {
		$t_basename = plugin_get_current();
	} else {
		$t_basename = $p_basename;
	}

	$t_field_name = $t_basename . '_' . $p_field_name;

	history_log_event_direct( $p_bug_id, $t_field_name, $p_old_value, $p_new_value, $p_user_id, PLUGIN_HISTORY );
}

/**
 * Trigger a plugin-specific error with the given name and type.
 *
 * @param string $p_error_name Error name.
 * @param int    $p_error_type Error type.
 * @param string $p_basename   The plugin basename (or current plugin if null).
 *
 * @return void
 */
function plugin_error( $p_error_name, $p_error_type = ERROR, $p_basename = null ) {
	if( is_null( $p_basename ) ) {
		$t_basename = plugin_get_current();
	} else {
		$t_basename = $p_basename;
	}

	$t_error_code = "plugin_{$t_basename}_$p_error_name";

	trigger_error( $t_error_code, $p_error_type );
}

/**
 * Hook a plugin's callback function to an event.
 *
 * Plugin hooks are expected to be methods of the plugin's base class. Regular
 * functions will not be called.
 *
 * @param string $p_name     Event name.
 * @param string $p_callback Callback function.
 *
 * @return void
 */
function plugin_event_hook( $p_name, $p_callback ) {
	$t_basename = plugin_get_current();
	event_hook( $p_name, $p_callback, $t_basename );
}

/**
 * Hook multiple plugin callbacks at once.
 *
 * @param array $p_hooks Array of event name/callback key/value pairs.
 *
 * @return bool True if all events were successfully hooked, false otherwise.
 */
function plugin_event_hook_many( array $p_hooks ) {
	$t_basename = plugin_get_current();

	$t_return = true;
	foreach( $p_hooks as $t_event => $t_callbacks ) {
		if( !is_array( $t_callbacks ) ) {
			$t_callbacks = array( $t_callbacks );
		}
		foreach( $t_callbacks as $t_callback ) {
			if( !event_hook( $t_event, $t_callback, $t_basename ) ) {
				$t_return = false;
			}
		}
	}
	return $t_return;
}

/**
 * Allows a plugin to declare a 'child plugin' that
 * can be loaded from the same parent directory.
 *
 * @param string $p_child Child plugin basename.
 *
 * @return MantisPlugin
 */
function plugin_child( $p_child ) {
	$t_base_name = plugin_get_current();

	$t_plugin = plugin_register( $t_base_name, false, $p_child );

	if( !is_null( $t_plugin ) ) {
		plugin_init( $p_child );
	}

	return $t_plugin;
}

/**
 * Checks if a given plugin has been registered and initialized.
 *
 * @param string $p_base_name Plugin basename.
 *
 * @return bool True if Plugin was loaded.
 */
function plugin_is_loaded( $p_base_name ) {
	global $g_plugin_cache_init;

	return ( isset( $g_plugin_cache_init[$p_base_name] ) && $g_plugin_cache_init[$p_base_name] );
}

/**
 * Checks two versions for minimum or maximum version dependencies.
 *
 * @param string $p_version  Version number to check.
 * @param string $p_required Version number required.
 * @param bool   $p_maximum  Minimum (false) or maximum (true) version check.¨
 *
 * @return int 1 if the version dependency succeeds, -1 if it fails
 */
function plugin_version_check( $p_version, $p_required, $p_maximum = false ) {
	if( $p_maximum ) {
		$t_operator = '<';
	} else {
		$t_operator = '>=';
	}
	$t_result = version_compare( $p_version, $p_required, $t_operator );
	return $t_result ? 1 : -1;
}

/**
 * Check a plugin dependency given a basename and required version.
 *
 * Versions are checked using PHP's library version_compare routine
 * and allows both minimum and maximum version requirements.
 * Returns
 *
 * @param string $p_base_name   Plugin base name.
 * @param string $p_required    Required version.
 * @param bool   $p_initialized Whether plugin is initialized.
 *
 * @return int Plugin dependency status: 1 if plugin dependency is met,
 *                 0 if not met or -1 if dependency is the wrong version.
 */
function plugin_dependency( $p_base_name, $p_required, $p_initialized = false ) {
	global $g_plugin_cache, $g_plugin_cache_init;

	# check for registered dependency
	if( isset( $g_plugin_cache[$p_base_name] ) ) {

		# require dependency initialized?
		if( $p_initialized && !isset( $g_plugin_cache_init[$p_base_name] ) ) {
			return 0;
		}

		$t_plugin_version = $g_plugin_cache[$p_base_name]->version;

		$t_required_array = explode( ',', $p_required );

		# Set maximum dependency for MantisCore if none is specified.
		# This effectively disables plugins which have not been specifically
		# designed for a new major Mantis release to force authors to review
		# their code, adapt it if necessary, and release a new version of the
		# plugin with updated dependencies.
		if( $p_base_name == 'MantisCore' && strpos( $p_required, '<' ) === false ) {
			$t_version_core = mb_substr( $t_plugin_version, 0, strpos( $t_plugin_version, '.' ) );
			$t_is_current_core_supported = false;
			foreach( $t_required_array as $t_version_required ) {
				$t_is_current_core_supported = $t_is_current_core_supported
					|| version_compare( trim( $t_version_required ), $t_version_core, '>=' );
			}
			if( !$t_is_current_core_supported ) {
				# Add current major version as maximum
				$t_required_array[] = '<' . $t_version_core;
			}
		}

		foreach( $t_required_array as $t_required ) {
			$t_required = trim( $t_required );
			$t_maximum = false;

			# check for a less-than-or-equal version requirement
			$t_ltpos = strpos( $t_required, '<=' );
			if( $t_ltpos !== false ) {
				$t_required = trim( mb_substr( $t_required, $t_ltpos + 2 ) );
				$t_maximum = true;
			} else {
				$t_ltpos = strpos( $t_required, '<' );
				if( $t_ltpos !== false ) {
					$t_required = trim( mb_substr( $t_required, $t_ltpos + 1 ) );
					$t_maximum = true;
				}
			}

			$t_check = plugin_version_check( $t_plugin_version, $t_required, $t_maximum );

			if( $t_check < 1 ) {
				return $t_check;
			}
		}

		return 1;
	} else {
		return 0;
	}
}

/**
 * Checks to see if a plugin is 'protected' from uninstall.
 *
 * @param string $p_base_name Plugin base name.
 *
 * @return bool True if plugin is protected
 */
function plugin_protected( $p_base_name ) {
	global $g_plugin_cache_protected;

	return $g_plugin_cache_protected[$p_base_name];
}

/**
 * Gets a plugin's priority.
 *
 * @param string $p_base_name Plugin base name.
 *
 * @return int Plugin priority
 */
function plugin_priority( $p_base_name ) {
	global $g_plugin_cache_priority;

	return $g_plugin_cache_priority[$p_base_name];
}

/**
 * Determine if a given plugin is installed.
 *
 * @param string $p_basename Plugin basename.
 *
 * @return bool True if plugin is installed
 */
function plugin_is_installed( $p_basename ) {
	if( array_key_exists( $p_basename, plugin_get_force_installed() ) ) {
		return true;
	}

	db_param_push();
	$t_query = 'SELECT COUNT(*) FROM {plugin} WHERE basename=' . db_param();
	$t_result = db_query( $t_query, array( $p_basename ) );
	return( 0 < db_result( $t_result ) );
}

/**
 * Install a plugin to the database.
 *
 * @param MantisPlugin $p_plugin Plugin basename.
 *
 * @return void
 */
function plugin_install( MantisPlugin $p_plugin ) {
	if( plugin_is_installed( $p_plugin->basename ) ) {
		error_parameters( $p_plugin->basename );
		trigger_error( ERROR_PLUGIN_ALREADY_INSTALLED, WARNING );
	}

	plugin_push_current( $p_plugin->basename );

	if( !$p_plugin->install() ) {
		plugin_pop_current();
		return;
	}

	db_param_push();
	$t_query = 'INSERT INTO {plugin} ( basename, enabled )
				VALUES ( ' . db_param() . ', ' . db_param() . ' )';
	db_query( $t_query, array( $p_plugin->basename, true ) );

	if( false === ( plugin_config_get( 'schema', false ) ) ) {
		plugin_config_set( 'schema', -1 );
	}

	plugin_upgrade( $p_plugin );

	plugin_pop_current();
}

/**
 * Determine if an installed plugin needs to upgrade its schema.
 *
 * @param MantisPlugin $p_plugin Plugin basename.
 *
 * @return bool True if plugin needs schema upgrades.
 */
function plugin_needs_upgrade( MantisPlugin $p_plugin ) {
	plugin_push_current( $p_plugin->basename );
	$t_plugin_schema = $p_plugin->schema();
	plugin_pop_current();
	if( is_null( $t_plugin_schema ) ) {
		return false;
	}

	$t_config_option = 'plugin_' . $p_plugin->basename . '_schema';
	$t_plugin_schema_version = config_get( $t_config_option, -1, ALL_USERS, ALL_PROJECTS );

	return( $t_plugin_schema_version < count( $t_plugin_schema ) - 1 );
}

/**
 * Upgrade an installed plugin's schema.
 *
 * This is mostly identical to the code in the MantisBT installer, and should
 * be reviewed and updated accordingly whenever that changes.
 *
 * @param MantisPlugin $p_plugin Plugin basename.
 *
 * @return bool|null True if upgrade completed, null if problem
 */
function plugin_upgrade( MantisPlugin $p_plugin ) {
	if( !plugin_is_installed( $p_plugin->basename ) ) {
		return null;
	}

	require_api( 'install_helper_functions_api.php' );

	plugin_push_current( $p_plugin->basename );

	$t_schema_version = (int)plugin_config_get( 'schema', -1 );
	$t_schema = $p_plugin->schema();

	global $g_db;
	$t_dict = NewDataDictionary( $g_db );

	$i = $t_schema_version + 1;
	while( $i < count( $t_schema ) ) {
		if( !$p_plugin->upgrade( $i ) ) {
			plugin_pop_current();
			return false;
		}
		$t_status = false;

		switch( $t_schema[$i][0] ) {
			case 'InsertData':
				$t_sqlarray = array(
					'INSERT INTO ' . $t_schema[$i][1][0] . $t_schema[$i][1][1],
				);
				break;

			case 'UpdateSQL':
				$t_sqlarray = array(
					'UPDATE ' . $t_schema[$i][1][0] . $t_schema[$i][1][1],
				);
				break;

			case 'UpdateFunction':
				$t_sqlarray = false;
				if( isset( $t_schema[$i][2] ) ) {
					$t_status = call_user_func( 'install_' . $t_schema[$i][1], $t_schema[$i][2] );
				} else {
					$t_status = call_user_func( 'install_' . $t_schema[$i][1] );
				}
				break;

			case null:
				# No-op upgrade step
				$t_sqlarray = false;
				$t_status = 2;
				break;

			default:
				$t_sqlarray = call_user_func_array(
					array( $t_dict, $t_schema[$i][0] ),
					$t_schema[$i][1]
				);
		}

		if( $t_sqlarray ) {
			$t_status = $t_dict->ExecuteSQLArray( $t_sqlarray );
		}

		if( 2 == $t_status ) {
			plugin_config_set( 'schema', $i );
		} else {
			error_parameters( 
				$i, 
				$g_db->ErrorMsg(), 
				implode( '<br>', $t_sqlarray ) 
			);
			trigger_error( ERROR_PLUGIN_UPGRADE_FAILED, ERROR );
			return null;
		}

		$i++;
	}

	plugin_pop_current();

	return true;
}

/**
 * Uninstall a plugin from the database.
 *
 * Return without action if given plugin is protected or not installed, unless
 * it is a missing plugin, in which case we delete it regardless of protected
 * status.
 *
 * @param MantisPlugin $p_plugin Plugin basename.
 *
 * @return void
 */
function plugin_uninstall( MantisPlugin $p_plugin ) {
	access_ensure_global_level( config_get_global( 'manage_plugin_threshold' ) );

	if( !$p_plugin->status == MantisPlugin::STATUS_MISSING_BASE_CLASS &&
		( !plugin_is_installed( $p_plugin->basename ) || plugin_protected( $p_plugin->basename ) )
	) {
		return;
	}

	db_param_push();
	$t_query = 'DELETE FROM {plugin} WHERE basename=' . db_param();
	db_query( $t_query, array( $p_plugin->basename ) );

	plugin_push_current( $p_plugin->basename );

	$p_plugin->uninstall();

	plugin_pop_current();
}

/**
 * Search the system for plugins.
 *
 * Goes through all directories under plugin_path and builds a list of all
 * plugins found. Those that do not register successfully, (e.g. because of
 * missing source code) or that are registered in the database but their
 * code is no longer available, will have a special `MantisInvalid` class
 * (or one of its child classes) allowing special handling by the caller.
 *
 * @return MantisPlugin[] List of found plugins, with basename as key.
 */
function plugin_find_all() {
	static $s_plugins;
	if( !is_null( $s_plugins ) ) {
		return $s_plugins;
	}
	
	$t_plugin_path = config_get_global( 'plugin_path' );
	$s_plugins = array(
		'MantisCore' => new MantisCorePlugin( 'MantisCore' ),
	);

	# Get list of installed plugins
	$t_query = new DbQuery( 'SELECT basename FROM {plugin}' );
	$t_installed_plugins = array();
	while( $t_query->fetch() ) {
		$t_installed_plugins[] = $t_query->value();
	}

	# Register all plugins found in directory
	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 = plugin_register( $t_file, true );

				if( !is_null( $t_plugin ) ) {
					$s_plugins[$t_file] = $t_plugin;
				}
			}
		}
		closedir( $t_dir );
	}

	# Process missing plugins (i.e. installed without code in plugins directory)
	$t_missing_plugins = array_diff( $t_installed_plugins, array_keys( $s_plugins ) );
	foreach( $t_missing_plugins as $t_missing_plugin ) {
		$s_plugins[$t_missing_plugin] = new MissingPlugin( $t_missing_plugin );
	}

	return $s_plugins;
}

/**
 * Load a plugin's core class file.
 *
 * @param string $p_basename Plugin basename.
 * @param string $p_child    Child filename.
 *
 * @return bool
 */
function plugin_include( $p_basename, $p_child = null ) {
	$t_path = config_get_global( 'plugin_path' ) . $p_basename . DIRECTORY_SEPARATOR;

	if( is_null( $p_child ) ) {
		$t_plugin_file = $t_path . $p_basename . '.php';
	} else {
		$t_plugin_file = $t_path . $p_child . '.php';
	}
	$t_included = false;
	if( is_file( $t_plugin_file ) ) {
		include_once( $t_plugin_file );
		$t_included = true;
	}

	return $t_included;
}

/**
 * Allows a plugin page to require a plugin-specific API.
 *
 * This function's purpose is to include a plugin-specific API (e.g. helper
 * functions) without having to worry about the actual plugin path.
 *
 * NOTE: it is not intended to, and in fact will not register variables in the
 * global namespace.
 *
 * @param string $p_file     The API to be included.
 * @param string $p_basename Plugin's basename (defaults to current plugin).
 *
 * @return void
 */
function plugin_require_api( $p_file, $p_basename = null ) {
	if( is_null( $p_basename ) ) {
		$t_current = plugin_get_current();
	} else {
		$t_current = $p_basename;
	}

	$t_path = config_get_global( 'plugin_path' ) . $t_current . '/';

	require_once( $t_path . $p_file );
}

/**
 * Determine if a given plugin is registered.
 *
 * @param string $p_basename Plugin basename.
 *                           
 * @return bool True if plugin is registered
 */
function plugin_is_registered( $p_basename ) {
	global $g_plugin_cache;

	return isset( $g_plugin_cache[$p_basename] );
}

/**
 * Register a plugin with MantisBT.
 *
 * The plugin class must already be loaded before calling.
 * If the plugin could not be registered, the returned object will be a special
 * MantisPlugin child class:
 * - InvalidPlugin: if the plugin's name or version is not defined
 * - MissingClassPlugin: if the plugin's source code can't be found or loaded
 *
 * @param string $p_basename Plugin classname without 'Plugin' postfix.
 * @param bool   $p_return   Return.
 * @param string $p_child    Child filename.
 *
 * @return MantisPlugin
 */
function plugin_register( $p_basename, $p_return = false, $p_child = null ) {
	global $g_plugin_cache;

	$t_basename = is_null( $p_child ) ? $p_basename : $p_child;
	if( !isset( $g_plugin_cache[$t_basename] ) ) {
		$t_classname = $t_basename . 'Plugin';

		# Include the plugin script if the class is not already declared.
		if( !class_exists( $t_classname ) ) {
			if( !plugin_include( $p_basename, $p_child ) ) {
				log_event( LOG_PLUGIN, "Source code for Plugin '$t_basename' not found");
				return new MissingClassPlugin( $t_basename );
			}
		}

		# Make sure the class exists and that it's of the right type.
		if( class_exists( $t_classname ) && is_subclass_of( $t_classname, 'MantisPlugin' ) ) {
			plugin_push_current( $t_basename );
			/** @var MantisPlugin $t_plugin */
			$t_plugin = new $t_classname( $t_basename );
			plugin_pop_current();

			# Final check on the class
			if( !$t_plugin->isValid() ) {
				log_event(
					LOG_PLUGIN,
					"Plugin '$t_basename' is invalid (undefined Name or Version)"
				);
				return $t_plugin->getInvalidPlugin();
			}
		} elseif( basename( $_SERVER['SCRIPT_NAME'] ) == 'manage_plugin_page.php' ) {
			# We don't want to throw an error here, as this is the place where
			# information about the invalid Plugin is displayed.
			$t_plugin = new MissingClassPlugin( $t_basename );
			log_event(
				LOG_PLUGIN,
				"Plugin '$t_basename' is invalid ('$t_classname' class is not defined)"
			);
		} else {
			error_parameters( $t_basename, $t_classname );
			trigger_error( ERROR_PLUGIN_CLASS_NOT_FOUND, ERROR );
		}

		if( $p_return ) {
			/** @noinspection PhpUndefinedVariableInspection */
			return $t_plugin;
		} else {
			/** @noinspection PhpUndefinedVariableInspection */
			$g_plugin_cache[$t_basename] = $t_plugin;
		}
	}

	return $g_plugin_cache[$t_basename];
}

/**
 * Find and register all installed plugins.
 *
 * This includes the MantisCore pseudo-plugin.
 *
 * @return void
 */
function plugin_register_installed() {
	global $g_plugin_cache_priority, $g_plugin_cache_protected;

	# register plugins specified in the site configuration
	$t_plugins_to_register = plugin_get_force_installed();
	arsort( $t_plugins_to_register );
	foreach( $t_plugins_to_register as $t_basename => $t_priority ) {
		plugin_register( $t_basename );
		$g_plugin_cache_priority[$t_basename] = $t_priority;
		$g_plugin_cache_protected[$t_basename] = true;
	}

	# register plugins installed via the interface/database
	$t_query = new DbQuery( 'SELECT basename, priority, protected 
		FROM {plugin} 
		WHERE enabled=:enabled
		ORDER BY priority DESC'
	);
	$t_query->bind( 'enabled', true );
	foreach( $t_query->fetch_all() as $t_row ) {
		$t_basename = $t_row['basename'];
		if( !plugin_is_registered( $t_basename ) ) {
			plugin_register( $t_basename );
			$g_plugin_cache_priority[$t_basename] = (int)$t_row['priority'];
			$g_plugin_cache_protected[$t_basename] = (bool)$t_row['protected'];
		}
	}
}

/**
 * Initialize all installed plugins.
 *
 * Post-signals EVENT_PLUGIN_INIT.
 *
 * @return void
 */
function plugin_init_installed() {
	if( OFF == config_get_global( 'plugins_enabled' ) || !db_table_exists( db_get_table( 'plugin' ) ) ) {
		return;
	}

	global $g_plugin_cache, $g_plugin_current, $g_plugin_cache_priority, $g_plugin_cache_protected, $g_plugin_cache_init;
	$g_plugin_cache = array();
	$g_plugin_current = array();
	$g_plugin_cache_init = array();
	$g_plugin_cache_priority = array();
	$g_plugin_cache_protected = array();

	plugin_register_installed();

	$t_plugins = array_keys( $g_plugin_cache );

	do {
		$t_continue = false;
		$t_plugins_retry = array();

		foreach( $t_plugins as $t_basename ) {
			if( plugin_init( $t_basename ) ) {
				$t_continue = true;

			} else {
				# Dependent plugin
				$t_plugins_retry[] = $t_basename;
			}
		}

		$t_plugins = $t_plugins_retry;
	} while( $t_continue );

	event_signal( 'EVENT_PLUGIN_INIT' );
}

/**
 * Initialize a single plugin.
 *
 * @param string $p_basename Plugin basename.
 *
 * @return bool True if plugin initialized, false otherwise.
 */
function plugin_init( $p_basename ) {
	global $g_plugin_cache, $g_plugin_cache_init;

	# handle dependent plugins
	if( isset( $g_plugin_cache[$p_basename] ) ) {
		$t_plugin = $g_plugin_cache[$p_basename];

		# hard dependencies; return false if the dependency is not registered,
		# does not meet the version requirement, or is not yet initialized.
		if( is_array( $t_plugin->requires ) ) {
			foreach( $t_plugin->requires as $t_required => $t_version ) {
				if( plugin_dependency( $t_required, $t_version, true ) !== 1 ) {
					return false;
				}
			}
		}

		# soft dependencies; only return false if the soft dependency is
		# registered, but not yet initialized.
		if( is_array( $t_plugin->uses ) ) {
			foreach( $t_plugin->uses as $t_used => $t_version ) {
				if( isset( $g_plugin_cache[$t_used] ) && !isset( $g_plugin_cache_init[$t_used] ) ) {
					return false;
				}
			}
		}

		# if plugin schema needs an upgrade, do not initialize
		if( plugin_needs_upgrade( $t_plugin ) ) {
			return false;
		}

		plugin_push_current( $p_basename );

		# load plugin error strings
		global $g_lang_strings;
		$t_lang = lang_get_current();
		$t_plugin_errors = $t_plugin->errors();

		foreach( $t_plugin_errors as $t_error_name => $t_error_string ) {
			$t_error_code = "plugin_{$p_basename}_$t_error_name";
			$g_lang_strings[$t_lang]['MANTIS_ERROR'][$t_error_code] = $t_error_string;
		}

		# finish initializing the plugin
		if( !$t_plugin->__init() ) {
			$t_invalid = new MissingHooksPlugin( $p_basename );
			$t_invalid->setInvalidPlugin( $t_plugin );
			$g_plugin_cache[$p_basename] = $t_invalid;
			plugin_pop_current();
			return false;
		}
		$g_plugin_cache_init[$p_basename] = true;

		plugin_pop_current();

		return true;
	} else {
		return false;
	}
}

/**
 * Log a plugin-specific event.
 *
 * @param string|array $p_msg       Log message - either a string, or an array
 *                                  structured as (string,execution time).
 * @param string        $p_basename Plugin's basename (defaults to current plugin)
 */
function plugin_log_event( $p_msg, $p_basename = null ) {
	$t_current_plugin = plugin_get_current();
	if( is_null( $p_basename ) ) {
		$t_basename = $t_current_plugin;
	} else {
		$t_basename = $p_basename;
	}

	if( $t_basename != $t_current_plugin ) {
		plugin_push_current( $t_basename );
		log_event( LOG_PLUGIN, $p_msg);
		plugin_pop_current();
	} else {
		log_event( LOG_PLUGIN, $p_msg);
	}
}

/**
 * Retrieve plugin-defined menu items for a given event.
 *
 * These are HTML hyperlinks (<a> tags).
 *
 * @param string $p_event Plugin event to signal
 *
 * @return array
 */
function plugin_menu_items( $p_event ) {
	$t_items = array();

	if( $p_event ) {
		$t_event_items = event_signal( $p_event );

		foreach( $t_event_items as $t_plugin_items ) {
			foreach( $t_plugin_items as $t_callback_items ) {
				if( is_array( $t_callback_items ) ) {
					$t_items = array_merge( $t_items, $t_callback_items );
				}
				else {
					if( $t_callback_items !== null ) {
						$t_items[] = $t_callback_items;
					}
				}
			}
		}
	}

	return $t_items;
}
