PageRenderTime 172ms CodeModel.GetById 60ms app.highlight 61ms RepoModel.GetById 40ms app.codeStats 0ms

/core/plugin_api.php

http://github.com/mantisbt/mantisbt
PHP | 1102 lines | 603 code | 165 blank | 334 comment | 120 complexity | 6d067fba590b83f0c91e9a59125a5b38 MD5 | raw file
   1<?php
   2# MantisBT - A PHP based bugtracking system
   3
   4# MantisBT is free software: you can redistribute it and/or modify
   5# it under the terms of the GNU General Public License as published by
   6# the Free Software Foundation, either version 2 of the License, or
   7# (at your option) any later version.
   8#
   9# MantisBT is distributed in the hope that it will be useful,
  10# but WITHOUT ANY WARRANTY; without even the implied warranty of
  11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12# GNU General Public License for more details.
  13#
  14# You should have received a copy of the GNU General Public License
  15# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.
  16
  17
  18/**
  19 * Plugin API
  20 *
  21 * Handles the initialisation, management, and execution of plugins.
  22 *
  23 * @package CoreAPI
  24 * @subpackage PluginAPI
  25 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
  26 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
  27 * @link http://www.mantisbt.org
  28 *
  29 * @uses access_api.php
  30 * @uses config_api.php
  31 * @uses constant_inc.php
  32 * @uses database_api.php
  33 * @uses error_api.php
  34 * @uses event_api.php
  35 * @uses file_api.php
  36 * @uses helper_api.php
  37 * @uses history_api.php
  38 * @uses lang_api.php
  39 * @uses logging_api.php
  40 */
  41
  42require_api( 'access_api.php' );
  43require_api( 'config_api.php' );
  44require_api( 'constant_inc.php' );
  45require_api( 'database_api.php' );
  46require_api( 'error_api.php' );
  47require_api( 'event_api.php' );
  48require_api( 'file_api.php' );
  49require_api( 'helper_api.php' );
  50require_api( 'history_api.php' );
  51require_api( 'lang_api.php' );
  52require_api( 'logging_api.php' );
  53
  54# Cache variables #####
  55
  56$g_plugin_cache = array();
  57$g_plugin_cache_priority = array();
  58$g_plugin_cache_protected = array();
  59$g_plugin_current = array();
  60
  61# Public API #####
  62/**
  63 * Get the currently executing plugin's basename.
  64 * @return string Plugin basename, or null if no current plugin
  65 */
  66function plugin_get_current() {
  67	global $g_plugin_current;
  68	return( isset( $g_plugin_current[0] ) ? $g_plugin_current[0] : null );
  69}
  70
  71/**
  72 * Add the current plugin to the stack
  73 * @param string $p_base_name Plugin base name.
  74 * @return void
  75 */
  76function plugin_push_current( $p_base_name ) {
  77	global $g_plugin_current;
  78	array_unshift( $g_plugin_current, $p_base_name );
  79}
  80
  81/**
  82 * Remove the current plugin from the stack
  83 * @return string Plugin basename, or null if no current plugin
  84 */
  85function plugin_pop_current() {
  86	global $g_plugin_current;
  87	return( isset( $g_plugin_current[0] ) ? array_shift( $g_plugin_current ) : null );
  88}
  89
  90/**
  91 * Returns the list of force-installed plugins
  92 * @see $g_plugins_force_installed
  93 * @return array List of plugins (basename => priority)
  94 */
  95function plugin_get_force_installed() {
  96	$t_forced_plugins = config_get_global( 'plugins_force_installed' );
  97
  98	# MantisCore pseudo-plugin is force-installed by definition, with priority 3
  99	$t_forced_plugins['MantisCore'] = 3;
 100
 101	return $t_forced_plugins;
 102}
 103
 104/**
 105 * Returns an object representing the specified plugin
 106 * Triggers an error if the plugin is not registered
 107 * @param string|null $p_basename Plugin base name (defaults to current plugin).
 108 * @return object Plugin Object
 109 */
 110function plugin_get( $p_basename = null ) {
 111	global $g_plugin_cache;
 112
 113	if( is_null( $p_basename ) ) {
 114		$t_current = plugin_get_current();
 115	} else {
 116		$t_current = $p_basename;
 117	}
 118
 119	if( !plugin_is_registered( $t_current ) ) {
 120		error_parameters( $t_current );
 121		trigger_error( ERROR_PLUGIN_NOT_REGISTERED, ERROR );
 122	}
 123
 124	return $g_plugin_cache[$t_current];
 125}
 126
 127/**
 128 * Get the URL to the plugin wrapper page.
 129 * @param string  $p_page      Page name.
 130 * @param boolean $p_redirect  Return url for redirection.
 131 * @param string  $p_base_name Plugin base name (defaults to current plugin).
 132 * @return string
 133 */
 134function plugin_page( $p_page, $p_redirect = false, $p_base_name = null ) {
 135	if( is_null( $p_base_name ) ) {
 136		$t_current = plugin_get_current();
 137	} else {
 138		$t_current = $p_base_name;
 139	}
 140	if( $p_redirect ) {
 141		return 'plugin.php?page=' . $t_current . '/' . $p_page;
 142	} else {
 143		return helper_mantis_url( 'plugin.php?page=' . $t_current . '/' . $p_page );
 144	}
 145}
 146
 147/**
 148 * Gets the route group (base path under '/api/rest', e.g. /plugins/Example
 149 *
 150 * @param string $p_base_name The basename for plugin or null for current plugin.
 151 * @return string The route group path to use.
 152 */
 153function plugin_route_group( $p_base_name = null ) {
 154	if( is_null( $p_base_name ) ) {
 155		$t_current = plugin_get_current();
 156	} else {
 157		$t_current = $p_base_name;
 158	}
 159
 160	return '/plugins/' . $t_current;
 161}
 162
 163/**
 164 * Return a path to a plugin file.
 165 * @param string $p_filename  File name.
 166 * @param string $p_base_name Plugin base name.
 167 * @return mixed File path or false if FNF
 168 */
 169function plugin_file_path( $p_filename, $p_base_name ) {
 170	$t_file_path = config_get_global( 'plugin_path' );
 171	$t_file_path .= $p_base_name . DIRECTORY_SEPARATOR;
 172	$t_file_path .= 'files' . DIRECTORY_SEPARATOR . $p_filename;
 173
 174	return( is_file( $t_file_path ) ? $t_file_path : false );
 175}
 176
 177/**
 178 * Get the URL to the plugin wrapper file page.
 179 * @param string  $p_file      File name.
 180 * @param boolean $p_redirect  Return url for redirection.
 181 * @param string  $p_base_name Plugin base name (defaults to current plugin).
 182 * @return string
 183 */
 184function plugin_file( $p_file, $p_redirect = false, $p_base_name = null ) {
 185	if( is_null( $p_base_name ) ) {
 186		$t_current = plugin_get_current();
 187	} else {
 188		$t_current = $p_base_name;
 189	}
 190	if( $p_redirect ) {
 191		return 'plugin_file.php?file=' . $t_current . '/' . $p_file;
 192	} else {
 193		return helper_mantis_url( 'plugin_file.php?file=' . $t_current . '/' . $p_file );
 194	}
 195}
 196
 197/**
 198 * Include the contents of a file as output.
 199 * @param string $p_filename File name.
 200 * @param string $p_basename Plugin basename.
 201 * @return void
 202 */
 203function plugin_file_include( $p_filename, $p_basename = null ) {
 204	global $g_plugin_mime_types;
 205
 206	if( is_null( $p_basename ) ) {
 207		$t_current = plugin_get_current();
 208	} else {
 209		$t_current = $p_basename;
 210	}
 211
 212	$t_file_path = plugin_file_path( $p_filename, $t_current );
 213	if( false === $t_file_path ) {
 214		error_parameters( $t_current, $p_filename );
 215		trigger_error( ERROR_PLUGIN_FILE_NOT_FOUND, ERROR );
 216	}
 217
 218	$t_content_type = '';
 219	$t_file_info_type = file_get_mime_type( $t_file_path );
 220	if( $t_file_info_type !== false ) {
 221		$t_content_type = $t_file_info_type;
 222	}
 223
 224	# allow overriding the content type for specific text and image extensions
 225	# see bug #13193 for details
 226	if( strpos( $t_content_type, 'text/' ) === 0 || strpos( $t_content_type, 'image/' ) === 0 ) {
 227		$t_extension = pathinfo( $t_file_path, PATHINFO_EXTENSION );
 228		if( $t_extension && array_key_exists( $t_extension, $g_plugin_mime_types ) ) {
 229			$t_content_type =  $g_plugin_mime_types[$t_extension];
 230		}
 231	}
 232
 233	if( $t_content_type ) {
 234		header( 'Content-Type: ' . $t_content_type );
 235	}
 236
 237	readfile( $t_file_path );
 238}
 239
 240/**
 241 * Given a base table name for a plugin, add appropriate prefix and suffix.
 242 * Convenience for plugin schema definitions.
 243 * @param string $p_name     Table name.
 244 * @param string $p_basename Plugin basename (defaults to current plugin).
 245 * @return string Full table name
 246 */
 247function plugin_table( $p_name, $p_basename = null ) {
 248	if( is_null( $p_basename ) ) {
 249		$t_current = plugin_get_current();
 250	} else {
 251		$t_current = $p_basename;
 252	}
 253
 254	# Determine plugin table prefix including trailing '_'
 255	$t_prefix = trim( config_get_global( 'db_table_plugin_prefix' ) );
 256	if( !empty( $t_prefix ) && '_' != substr( $t_prefix, -1 ) ) {
 257		$t_prefix .= '_';
 258	}
 259
 260	return db_get_table( $t_prefix . $t_current . '_' . $p_name );
 261}
 262
 263/**
 264 * Get a plugin configuration option.
 265 * @param string  $p_option  Configuration option name.
 266 * @param mixed   $p_default Default option value.
 267 * @param boolean $p_global  Get global config variables only.
 268 * @param integer $p_user    A user identifier.
 269 * @param integer $p_project A Project identifier.
 270 * @return string
 271 */
 272function plugin_config_get( $p_option, $p_default = null, $p_global = false, $p_user = null, $p_project = null ) {
 273	$t_basename = plugin_get_current();
 274	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;
 275
 276	if( $p_global ) {
 277		return config_get_global( $t_full_option, $p_default );
 278	} else {
 279		return config_get( $t_full_option, $p_default, $p_user, $p_project );
 280	}
 281}
 282
 283/**
 284 * Set a plugin configuration option in the database.
 285 * @param string  $p_option  Configuration option name.
 286 * @param mixed   $p_value   Option value.
 287 * @param integer $p_user    A user identifier.
 288 * @param integer $p_project A project identifier.
 289 * @param integer $p_access  Access threshold.
 290 * @return void
 291 */
 292function plugin_config_set( $p_option, $p_value, $p_user = NO_USER, $p_project = ALL_PROJECTS, $p_access = DEFAULT_ACCESS_LEVEL ) {
 293	if( $p_access == DEFAULT_ACCESS_LEVEL ) {
 294		$p_access = config_get_global( 'admin_site_threshold' );
 295	}
 296
 297	$t_basename = plugin_get_current();
 298	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;
 299
 300	config_set( $t_full_option, $p_value, $p_user, $p_project, $p_access );
 301}
 302
 303/**
 304 * Delete a plugin configuration option from the database.
 305 * @param string  $p_option  Configuration option name.
 306 * @param integer $p_user    A user identifier.
 307 * @param integer $p_project A project identifier.
 308 * @return void
 309 */
 310function plugin_config_delete( $p_option, $p_user = ALL_USERS, $p_project = ALL_PROJECTS ) {
 311	$t_basename = plugin_get_current();
 312	$t_full_option = 'plugin_' . $t_basename . '_' . $p_option;
 313
 314	config_delete( $t_full_option, $p_user, $p_project );
 315}
 316
 317/**
 318 * Set plugin default values to global values without overriding anything.
 319 * @param array $p_options Array of configuration option name/value pairs.
 320 * @return void
 321 */
 322function plugin_config_defaults( array $p_options ) {
 323	if( !is_array( $p_options ) ) {
 324		return;
 325	}
 326
 327	$t_basename = plugin_get_current();
 328	$t_option_base = 'plugin_' . $t_basename . '_';
 329
 330	foreach( $p_options as $t_option => $t_value ) {
 331		$t_full_option = $t_option_base . $t_option;
 332
 333		config_set_global( $t_full_option, $t_value, false );
 334	}
 335}
 336
 337/**
 338 * Get a language string for the plugin.
 339 * Automatically prepends plugin_<basename> to the string requested.
 340 * @param string $p_name     Language string name.
 341 * @param string $p_basename Plugin basename.
 342 * @return string Language string
 343 */
 344function plugin_lang_get( $p_name, $p_basename = null ) {
 345	if( !is_null( $p_basename ) ) {
 346		plugin_push_current( $p_basename );
 347	}
 348
 349	$t_basename = plugin_get_current();
 350	$t_name = 'plugin_' . $t_basename . '_' . $p_name;
 351	$t_string = lang_get( $t_name );
 352
 353	if( !is_null( $p_basename ) ) {
 354		plugin_pop_current();
 355	}
 356	return $t_string;
 357}
 358
 359/**
 360 * Get a defaulted language string for the plugin.
 361 * - If found, return the appropriate string.
 362 * - If not found, no default supplied, return the supplied string as is.
 363 * - If not found, default supplied, return default.
 364 * Automatically prepends plugin_<basename> to the string requested.
 365 * @see lang_get_defaulted()
 366 *
 367 * @param string $p_name     Language string name.
 368 * @param string $p_default  The default value to return.
 369 * @param string $p_basename Plugin basename.
 370 *
 371 * @return string Language string
 372 */
 373function plugin_lang_get_defaulted( $p_name, $p_default = null, $p_basename = null ) {
 374	if( !is_null( $p_basename ) ) {
 375		plugin_push_current( $p_basename );
 376	}
 377	$t_basename = plugin_get_current();
 378	$t_name = 'plugin_' . $t_basename . '_' . $p_name;
 379	$t_string = lang_get_defaulted( $t_name, $p_default );
 380
 381	if( !is_null( $p_basename ) ) {
 382		plugin_pop_current();
 383	}
 384	return $t_string;
 385}
 386
 387/**
 388 * log history event from plugin
 389 * @param integer $p_bug_id     A bug identifier.
 390 * @param string  $p_field_name A field name.
 391 * @param string  $p_old_value  The old value.
 392 * @param string  $p_new_value  The new value.
 393 * @param integer $p_user_id    A user identifier.
 394 * @param string  $p_basename   The plugin basename (or current plugin if null).
 395 * @return void
 396 */
 397function plugin_history_log( $p_bug_id, $p_field_name, $p_old_value, $p_new_value = '', $p_user_id = null, $p_basename = null ) {
 398	if( is_null( $p_basename ) ) {
 399		$t_basename = plugin_get_current();
 400	} else {
 401		$t_basename = $p_basename;
 402	}
 403
 404	$t_field_name = $t_basename . '_' . $p_field_name;
 405
 406	history_log_event_direct( $p_bug_id, $t_field_name, $p_old_value, $p_new_value, $p_user_id, PLUGIN_HISTORY );
 407}
 408
 409/**
 410 * Trigger a plugin-specific error with the given name and type.
 411 * @param string  $p_error_name Error name.
 412 * @param integer $p_error_type Error type.
 413 * @param string  $p_basename   The plugin basename (or current plugin if null).
 414 * @return void
 415 */
 416function plugin_error( $p_error_name, $p_error_type = ERROR, $p_basename = null ) {
 417	if( is_null( $p_basename ) ) {
 418		$t_basename = plugin_get_current();
 419	} else {
 420		$t_basename = $p_basename;
 421	}
 422
 423	$t_error_code = "plugin_${t_basename}_${p_error_name}";
 424
 425	trigger_error( $t_error_code, $p_error_type );
 426}
 427
 428/**
 429 * Hook a plugin's callback function to an event.
 430 * @param string $p_name     Event name.
 431 * @param string $p_callback Callback function.
 432 * @return void
 433 */
 434function plugin_event_hook( $p_name, $p_callback ) {
 435	$t_basename = plugin_get_current();
 436	event_hook( $p_name, $p_callback, $t_basename );
 437}
 438
 439/**
 440 * Hook multiple plugin callbacks at once.
 441 * @param array $p_hooks Array of event name/callback key/value pairs.
 442 * @return void
 443 */
 444function plugin_event_hook_many( array $p_hooks ) {
 445	if( !is_array( $p_hooks ) ) {
 446		return;
 447	}
 448
 449	$t_basename = plugin_get_current();
 450
 451	foreach( $p_hooks as $t_event => $t_callbacks ) {
 452		if( !is_array( $t_callbacks ) ) {
 453			event_hook( $t_event, $t_callbacks, $t_basename );
 454			continue;
 455		}
 456
 457		foreach( $t_callbacks as $t_callback ) {
 458			event_hook( $t_event, $t_callback, $t_basename );
 459		}
 460	}
 461}
 462
 463/**
 464 * Allows a plugin to declare a 'child plugin' that
 465 * can be loaded from the same parent directory.
 466 * @param string $p_child Child plugin basename.
 467 * @return mixed
 468 */
 469function plugin_child( $p_child ) {
 470	$t_base_name = plugin_get_current();
 471
 472	$t_plugin = plugin_register( $t_base_name, false, $p_child );
 473
 474	if( !is_null( $t_plugin ) ) {
 475		plugin_init( $p_child );
 476	}
 477
 478	return $t_plugin;
 479}
 480
 481/**
 482 * Checks if a given plugin has been registered and initialized,
 483 * and returns a boolean value representing the "loaded" state.
 484 * @param string $p_base_name Plugin basename.
 485 * @return boolean Plugin loaded
 486 */
 487function plugin_is_loaded( $p_base_name ) {
 488	global $g_plugin_cache_init;
 489
 490	return ( isset( $g_plugin_cache_init[$p_base_name] ) && $g_plugin_cache_init[$p_base_name] );
 491}
 492
 493/**
 494 * Checks two versions for minimum or maximum version dependencies.
 495 * @param string  $p_version  Version number to check.
 496 * @param string  $p_required Version number required.
 497 * @param boolean $p_maximum  Minimum (false) or maximum (true) version check.
 498 * @return integer 1 if the version dependency succeeds, -1 if it fails
 499 */
 500function plugin_version_check( $p_version, $p_required, $p_maximum = false ) {
 501	if( $p_maximum ) {
 502		$t_operator = '<';
 503	} else {
 504		$t_operator = '>=';
 505	}
 506	$t_result = version_compare( $p_version, $p_required, $t_operator );
 507	return $t_result ? 1 : -1;
 508}
 509
 510/**
 511 * Check a plugin dependency given a basename and required version.
 512 * Versions are checked using PHP's library version_compare routine
 513 * and allows both minimum and maximum version requirements.
 514 * Returns 1 if plugin dependency is met, 0 if dependency not met,
 515 * or -1 if dependency is the wrong version.
 516 * @param string  $p_base_name   Plugin base name.
 517 * @param string  $p_required    Required version.
 518 * @param boolean $p_initialized Whether plugin is initialized.
 519 * @return integer Plugin dependency status
 520 */
 521function plugin_dependency( $p_base_name, $p_required, $p_initialized = false ) {
 522	global $g_plugin_cache, $g_plugin_cache_init;
 523
 524	# check for registered dependency
 525	if( isset( $g_plugin_cache[$p_base_name] ) ) {
 526
 527		# require dependency initialized?
 528		if( $p_initialized && !isset( $g_plugin_cache_init[$p_base_name] ) ) {
 529			return 0;
 530		}
 531
 532		$t_plugin_version = $g_plugin_cache[$p_base_name]->version;
 533
 534		$t_required_array = explode( ',', $p_required );
 535
 536		# Set maximum dependency for MantisCore if none is specified.
 537		# This effectively disables plugins which have not been specifically
 538		# designed for a new major Mantis release to force authors to review
 539		# their code, adapt it if necessary, and release a new version of the
 540		# plugin with updated dependencies.
 541		if( $p_base_name == 'MantisCore' && strpos( $p_required, '<' ) === false ) {
 542			$t_version_core = mb_substr( $t_plugin_version, 0, strpos( $t_plugin_version, '.' ) );
 543			$t_is_current_core_supported = false;
 544			foreach( $t_required_array as $t_version_required ) {
 545				$t_is_current_core_supported = $t_is_current_core_supported
 546					|| version_compare( trim( $t_version_required ), $t_version_core, '>=' );
 547			}
 548			if( !$t_is_current_core_supported ) {
 549				# Add current major version as maximum
 550				$t_required_array[] = '<' . $t_version_core;
 551			}
 552		}
 553
 554		foreach( $t_required_array as $t_required ) {
 555			$t_required = trim( $t_required );
 556			$t_maximum = false;
 557
 558			# check for a less-than-or-equal version requirement
 559			$t_ltpos = strpos( $t_required, '<=' );
 560			if( $t_ltpos !== false ) {
 561				$t_required = trim( mb_substr( $t_required, $t_ltpos + 2 ) );
 562				$t_maximum = true;
 563			} else {
 564				$t_ltpos = strpos( $t_required, '<' );
 565				if( $t_ltpos !== false ) {
 566					$t_required = trim( mb_substr( $t_required, $t_ltpos + 1 ) );
 567					$t_maximum = true;
 568				}
 569			}
 570
 571			$t_check = plugin_version_check( $t_plugin_version, $t_required, $t_maximum );
 572
 573			if( $t_check < 1 ) {
 574				return $t_check;
 575			}
 576		}
 577
 578		return 1;
 579	} else {
 580		return 0;
 581	}
 582}
 583
 584/**
 585 * Checks to see if a plugin is 'protected' from uninstall.
 586 * @param string $p_base_name Plugin base name.
 587 * @return boolean True if plugin is protected
 588 */
 589function plugin_protected( $p_base_name ) {
 590	global $g_plugin_cache_protected;
 591
 592	return $g_plugin_cache_protected[$p_base_name];
 593}
 594
 595/**
 596 * Gets a plugin's priority.
 597 * @param string $p_base_name Plugin base name.
 598 * @return int Plugin priority
 599 */
 600function plugin_priority( $p_base_name ) {
 601	global $g_plugin_cache_priority;
 602
 603	return $g_plugin_cache_priority[$p_base_name];
 604}
 605
 606/**
 607 * Determine if a given plugin is installed.
 608 * @param string $p_basename Plugin basename.
 609 * @return boolean True if plugin is installed
 610 */
 611function plugin_is_installed( $p_basename ) {
 612	foreach( plugin_get_force_installed() as $t_basename => $t_priority ) {
 613		if( $t_basename == $p_basename ) {
 614			return true;
 615		}
 616	}
 617
 618	db_param_push();
 619	$t_query = 'SELECT COUNT(*) FROM {plugin} WHERE basename=' . db_param();
 620	$t_result = db_query( $t_query, array( $p_basename ) );
 621	return( 0 < db_result( $t_result ) );
 622}
 623
 624/**
 625 * Install a plugin to the database.
 626 * @param MantisPlugin $p_plugin Plugin basename.
 627 * @return null
 628 */
 629function plugin_install( MantisPlugin $p_plugin ) {
 630	if( plugin_is_installed( $p_plugin->basename ) ) {
 631		error_parameters( $p_plugin->basename );
 632		trigger_error( ERROR_PLUGIN_ALREADY_INSTALLED, WARNING );
 633		return null;
 634	}
 635
 636	plugin_push_current( $p_plugin->basename );
 637
 638	if( !$p_plugin->install() ) {
 639		plugin_pop_current();
 640		return null;
 641	}
 642
 643	db_param_push();
 644	$t_query = 'INSERT INTO {plugin} ( basename, enabled )
 645				VALUES ( ' . db_param() . ', ' . db_param() . ' )';
 646	db_query( $t_query, array( $p_plugin->basename, true ) );
 647
 648	if( false === ( plugin_config_get( 'schema', false ) ) ) {
 649		plugin_config_set( 'schema', -1 );
 650	}
 651
 652	plugin_upgrade( $p_plugin );
 653
 654	plugin_pop_current();
 655}
 656
 657/**
 658 * Determine if an installed plugin needs to upgrade its schema.
 659 * @param MantisPlugin $p_plugin Plugin basename.
 660 * @return boolean True if plugin needs schema upgrades.
 661 */
 662function plugin_needs_upgrade( MantisPlugin $p_plugin ) {
 663	plugin_push_current( $p_plugin->name );
 664	$t_plugin_schema = $p_plugin->schema();
 665	plugin_pop_current();
 666	if( is_null( $t_plugin_schema ) ) {
 667		return false;
 668	}
 669
 670	$t_config_option = 'plugin_' . $p_plugin->basename . '_schema';
 671	$t_plugin_schema_version = config_get( $t_config_option, -1, ALL_USERS, ALL_PROJECTS );
 672
 673	return( $t_plugin_schema_version < count( $t_plugin_schema ) - 1 );
 674}
 675
 676/**
 677 * Upgrade an installed plugin's schema.
 678 * This is mostly identical to the code in the MantisBT installer, and should
 679 * be reviewed and updated accordingly whenever that changes.
 680 * @param MantisPlugin $p_plugin Plugin basename.
 681 * @return boolean|null True if upgrade completed, null if problem
 682 */
 683function plugin_upgrade( MantisPlugin $p_plugin ) {
 684	if( !plugin_is_installed( $p_plugin->basename ) ) {
 685		return null;
 686	}
 687
 688	require_api( 'install_helper_functions_api.php' );
 689
 690	plugin_push_current( $p_plugin->basename );
 691
 692	$t_schema_version = (int)plugin_config_get( 'schema', -1 );
 693	$t_schema = $p_plugin->schema();
 694
 695	global $g_db;
 696	$t_dict = NewDataDictionary( $g_db );
 697
 698	$i = $t_schema_version + 1;
 699	while( $i < count( $t_schema ) ) {
 700		if( !$p_plugin->upgrade( $i ) ) {
 701			plugin_pop_current();
 702			return false;
 703		}
 704
 705		switch( $t_schema[$i][0] ) {
 706			case 'InsertData':
 707				$t_sqlarray = array(
 708					'INSERT INTO ' . $t_schema[$i][1][0] . $t_schema[$i][1][1],
 709				);
 710				break;
 711
 712			case 'UpdateSQL':
 713				$t_sqlarray = array(
 714					'UPDATE ' . $t_schema[$i][1][0] . $t_schema[$i][1][1],
 715				);
 716				break;
 717
 718			case 'UpdateFunction':
 719				$t_sqlarray = false;
 720				if( isset( $t_schema[$i][2] ) ) {
 721					$t_status = call_user_func( 'install_' . $t_schema[$i][1], $t_schema[$i][2] );
 722				} else {
 723					$t_status = call_user_func( 'install_' . $t_schema[$i][1] );
 724				}
 725				break;
 726
 727			case null:
 728				# No-op upgrade step
 729				$t_sqlarray = false;
 730				$t_status = 2;
 731				break;
 732
 733			default:
 734				$t_sqlarray = call_user_func_array(
 735					array( $t_dict, $t_schema[$i][0] ),
 736					$t_schema[$i][1]
 737				);
 738				$t_status = false;
 739		}
 740
 741		if( $t_sqlarray ) {
 742			$t_status = $t_dict->ExecuteSQLArray( $t_sqlarray );
 743		}
 744
 745		if( 2 == $t_status ) {
 746			plugin_config_set( 'schema', $i );
 747		} else {
 748			error_parameters( 
 749				$i, 
 750				$g_db->ErrorMsg(), 
 751				implode( '<br>', $t_sqlarray ) 
 752			);
 753			trigger_error( ERROR_PLUGIN_UPGRADE_FAILED, ERROR );
 754			return null;
 755		}
 756
 757		$i++;
 758	}
 759
 760	plugin_pop_current();
 761
 762	return true;
 763}
 764
 765/**
 766 * Uninstall a plugin from the database.
 767 * @param MantisPlugin $p_plugin Plugin basename.
 768 * @return void
 769 */
 770function plugin_uninstall( MantisPlugin $p_plugin ) {
 771	access_ensure_global_level( config_get_global( 'manage_plugin_threshold' ) );
 772
 773	if( !plugin_is_installed( $p_plugin->basename ) || plugin_protected( $p_plugin->basename ) ) {
 774		return;
 775	}
 776
 777	db_param_push();
 778	$t_query = 'DELETE FROM {plugin} WHERE basename=' . db_param();
 779	db_query( $t_query, array( $p_plugin->basename ) );
 780
 781	plugin_push_current( $p_plugin->basename );
 782
 783	$p_plugin->uninstall();
 784
 785	plugin_pop_current();
 786}
 787
 788/**
 789 * Search the plugins directory for plugins.
 790 * @return array Plugin basename/info key/value pairs.
 791 */
 792function plugin_find_all() {
 793	$t_plugin_path = config_get_global( 'plugin_path' );
 794	$t_plugins = array(
 795		'MantisCore' => new MantisCorePlugin( 'MantisCore' ),
 796	);
 797
 798	if( $t_dir = opendir( $t_plugin_path ) ) {
 799		while( ( $t_file = readdir( $t_dir ) ) !== false ) {
 800			if( '.' == $t_file || '..' == $t_file ) {
 801				continue;
 802			}
 803			if( is_dir( $t_plugin_path . $t_file ) ) {
 804				$t_plugin = plugin_register( $t_file, true );
 805
 806				if( !is_null( $t_plugin ) ) {
 807					$t_plugins[$t_file] = $t_plugin;
 808				}
 809			}
 810		}
 811		closedir( $t_dir );
 812	}
 813	return $t_plugins;
 814}
 815
 816/**
 817 * Load a plugin's core class file.
 818 * @param string $p_basename Plugin basename.
 819 * @param string $p_child    Child filename.
 820 * @return boolean
 821 */
 822function plugin_include( $p_basename, $p_child = null ) {
 823	$t_path = config_get_global( 'plugin_path' ) . $p_basename . DIRECTORY_SEPARATOR;
 824
 825	if( is_null( $p_child ) ) {
 826		$t_plugin_file = $t_path . $p_basename . '.php';
 827	} else {
 828		$t_plugin_file = $t_path . $p_child . '.php';
 829	}
 830	$t_included = false;
 831	if( is_file( $t_plugin_file ) ) {
 832		include_once( $t_plugin_file );
 833		$t_included = true;
 834	}
 835
 836	return $t_included;
 837}
 838
 839/**
 840 * Allows a plugin page to require a plugin-specific API
 841 * @param string $p_file     The API to be included.
 842 * @param string $p_basename Plugin's basename (defaults to current plugin).
 843 * @return void
 844 */
 845function plugin_require_api( $p_file, $p_basename = null ) {
 846	if( is_null( $p_basename ) ) {
 847		$t_current = plugin_get_current();
 848	} else {
 849		$t_current = $p_basename;
 850	}
 851
 852	$t_path = config_get_global( 'plugin_path' ) . $t_current . '/';
 853
 854	require_once( $t_path . $p_file );
 855}
 856
 857/**
 858 * Determine if a given plugin is registered.
 859 * @param string $p_basename Plugin basename.
 860 * @return boolean True if plugin is registered
 861 */
 862function plugin_is_registered( $p_basename ) {
 863	global $g_plugin_cache;
 864
 865	return isset( $g_plugin_cache[$p_basename] );
 866}
 867
 868/**
 869 * Register a plugin with MantisBT.
 870 * The plugin class must already be loaded before calling.
 871 * @param string  $p_basename Plugin classname without 'Plugin' postfix.
 872 * @param boolean $p_return   Return.
 873 * @param string  $p_child    Child filename.
 874 * @return mixed
 875 */
 876function plugin_register( $p_basename, $p_return = false, $p_child = null ) {
 877	global $g_plugin_cache;
 878
 879	$t_basename = is_null( $p_child ) ? $p_basename : $p_child;
 880	if( !isset( $g_plugin_cache[$t_basename] ) ) {
 881		$t_classname = $t_basename . 'Plugin';
 882
 883		# Include the plugin script if the class is not already declared.
 884		if( !class_exists( $t_classname ) ) {
 885			if( !plugin_include( $p_basename, $p_child ) ) {
 886				return null;
 887			}
 888		}
 889
 890		# Make sure the class exists and that it's of the right type.
 891		if( class_exists( $t_classname ) && is_subclass_of( $t_classname, 'MantisPlugin' ) ) {
 892			plugin_push_current( is_null( $p_child ) ? $p_basename : $p_child );
 893
 894			$t_plugin = new $t_classname( is_null( $p_child ) ? $p_basename : $p_child );
 895
 896			plugin_pop_current();
 897
 898			# Final check on the class
 899			if( is_null( $t_plugin->name ) || is_null( $t_plugin->version ) ) {
 900				return null;
 901			}
 902
 903			if( $p_return ) {
 904				return $t_plugin;
 905			} else {
 906				$g_plugin_cache[$t_basename] = $t_plugin;
 907			}
 908		} else {
 909			error_parameters( $t_basename, $t_classname );
 910			trigger_error( ERROR_PLUGIN_CLASS_NOT_FOUND, ERROR );
 911		}
 912	}
 913
 914	return $g_plugin_cache[$t_basename];
 915}
 916
 917/**
 918 * Find and register all installed plugins.
 919 * This includes the MantisCore pseudo-plugin.
 920 * @return void
 921 */
 922function plugin_register_installed() {
 923	global $g_plugin_cache_priority, $g_plugin_cache_protected;
 924
 925	# register plugins specified in the site configuration
 926	foreach( plugin_get_force_installed() as $t_basename => $t_priority ) {
 927		plugin_register( $t_basename );
 928		$g_plugin_cache_priority[$t_basename] = $t_priority;
 929		$g_plugin_cache_protected[$t_basename] = true;
 930	}
 931
 932	# register plugins installed via the interface/database
 933	db_param_push();
 934	$t_query = 'SELECT basename, priority, protected FROM {plugin} WHERE enabled=' . db_param() . ' ORDER BY priority DESC';
 935	$t_result = db_query( $t_query, array( true ) );
 936
 937	while( $t_row = db_fetch_array( $t_result ) ) {
 938		$t_basename = $t_row['basename'];
 939		if( !plugin_is_registered( $t_basename ) ) {
 940			plugin_register( $t_basename );
 941			$g_plugin_cache_priority[$t_basename] = (int)$t_row['priority'];
 942			$g_plugin_cache_protected[$t_basename] = (bool)$t_row['protected'];
 943		}
 944	}
 945}
 946
 947/**
 948 * Initialize all installed plugins.
 949 * Post-signals EVENT_PLUGIN_INIT.
 950 * @return void
 951 */
 952function plugin_init_installed() {
 953	if( OFF == config_get_global( 'plugins_enabled' ) || !db_table_exists( db_get_table( 'plugin' ) ) ) {
 954		return;
 955	}
 956
 957	global $g_plugin_cache, $g_plugin_current, $g_plugin_cache_priority, $g_plugin_cache_protected, $g_plugin_cache_init;
 958	$g_plugin_cache = array();
 959	$g_plugin_current = array();
 960	$g_plugin_cache_init = array();
 961	$g_plugin_cache_priority = array();
 962	$g_plugin_cache_protected = array();
 963
 964	plugin_register_installed();
 965
 966	$t_plugins = array_keys( $g_plugin_cache );
 967
 968	do {
 969		$t_continue = false;
 970		$t_plugins_retry = array();
 971
 972		foreach( $t_plugins as $t_basename ) {
 973			if( plugin_init( $t_basename ) ) {
 974				$t_continue = true;
 975
 976			} else {
 977				# Dependent plugin
 978				$t_plugins_retry[] = $t_basename;
 979			}
 980		}
 981
 982		$t_plugins = $t_plugins_retry;
 983	} while( $t_continue );
 984
 985	event_signal( 'EVENT_PLUGIN_INIT' );
 986}
 987
 988/**
 989 * Initialize a single plugin.
 990 * @param string $p_basename Plugin basename.
 991 * @return boolean True if plugin initialized, false otherwise.
 992 */
 993function plugin_init( $p_basename ) {
 994	global $g_plugin_cache, $g_plugin_cache_init;
 995
 996	# handle dependent plugins
 997	if( isset( $g_plugin_cache[$p_basename] ) ) {
 998		$t_plugin = $g_plugin_cache[$p_basename];
 999
1000		# hard dependencies; return false if the dependency is not registered,
1001		# does not meet the version requirement, or is not yet initialized.
1002		if( is_array( $t_plugin->requires ) ) {
1003			foreach( $t_plugin->requires as $t_required => $t_version ) {
1004				if( plugin_dependency( $t_required, $t_version, true ) !== 1 ) {
1005					return false;
1006				}
1007			}
1008		}
1009
1010		# soft dependencies; only return false if the soft dependency is
1011		# registered, but not yet initialized.
1012		if( is_array( $t_plugin->uses ) ) {
1013			foreach( $t_plugin->uses as $t_used => $t_version ) {
1014				if( isset( $g_plugin_cache[$t_used] ) && !isset( $g_plugin_cache_init[$t_used] ) ) {
1015					return false;
1016				}
1017			}
1018		}
1019
1020		# if plugin schema needs an upgrade, do not initialize
1021		if( plugin_needs_upgrade( $t_plugin ) ) {
1022			return false;
1023		}
1024
1025		plugin_push_current( $p_basename );
1026
1027		# load plugin error strings
1028		global $g_lang_strings;
1029		$t_lang = lang_get_current();
1030		$t_plugin_errors = $t_plugin->errors();
1031
1032		foreach( $t_plugin_errors as $t_error_name => $t_error_string ) {
1033			$t_error_code = "plugin_${p_basename}_${t_error_name}";
1034			$g_lang_strings[$t_lang]['MANTIS_ERROR'][$t_error_code] = $t_error_string;
1035		}
1036
1037		# finish initializing the plugin
1038		$t_plugin->__init();
1039		$g_plugin_cache_init[$p_basename] = true;
1040
1041		plugin_pop_current();
1042
1043		return true;
1044	} else {
1045		return false;
1046	}
1047}
1048
1049/**
1050 * Log a plugin-specific event.
1051 *
1052 * @param string|array $p_msg       Either a string, or an array structured as
1053 *                                  (string,execution time).
1054 * @param string        $p_basename Plugin's basename (defaults to current plugin)
1055 */
1056function plugin_log_event( $p_msg, $p_basename = null ) {
1057	$t_current_plugin = plugin_get_current();
1058	if( is_null( $p_basename ) ) {
1059		$t_basename = $t_current_plugin;
1060	} else {
1061		$t_basename = $p_basename;
1062	}
1063
1064	if( $t_basename != $t_current_plugin ) {
1065		plugin_push_current( $t_basename );
1066		log_event( LOG_PLUGIN, $p_msg);
1067		plugin_pop_current();
1068	} else {
1069		log_event( LOG_PLUGIN, $p_msg);
1070	}
1071}
1072
1073/**
1074 * Retrieve plugin-defined menu items for a given event.
1075 *
1076 * These are HTML hyperlinks (<a> tags).
1077 *
1078 * @param string $p_event Plugin event to signal
1079 * @return array
1080 */
1081function plugin_menu_items( $p_event ) {
1082	$t_items = array();
1083
1084	if( $p_event ) {
1085		$t_event_items = event_signal( $p_event );
1086
1087		foreach( $t_event_items as $t_plugin => $t_plugin_items ) {
1088			foreach( $t_plugin_items as $t_callback => $t_callback_items ) {
1089				if( is_array( $t_callback_items ) ) {
1090					$t_items = array_merge( $t_items, $t_callback_items );
1091				}
1092				else {
1093					if( $t_callback_items !== null ) {
1094						$t_items[] = $t_callback_items;
1095					}
1096				}
1097			}
1098		}
1099	}
1100
1101	return $t_items;
1102}