PageRenderTime 223ms CodeModel.GetById 78ms app.highlight 86ms RepoModel.GetById 6ms app.codeStats 0ms

/core/history_api.php

http://github.com/mantisbt/mantisbt
PHP | 1002 lines | 716 code | 84 blank | 202 comment | 133 complexity | a20a333235484541cc7508e5089d2b9b 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 * History API
  19 *
  20 * @package CoreAPI
  21 * @subpackage HistoryAPI
  22 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
  23 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
  24 * @link http://www.mantisbt.org
  25 *
  26 * @uses access_api.php
  27 * @uses authentication_api.php
  28 * @uses bug_api.php
  29 * @uses bug_revision_api.php
  30 * @uses bugnote_api.php
  31 * @uses columns_api.php
  32 * @uses config_api.php
  33 * @uses constant_inc.php
  34 * @uses custom_field_api.php
  35 * @uses database_api.php
  36 * @uses gpc_api.php
  37 * @uses helper_api.php
  38 * @uses lang_api.php
  39 * @uses project_api.php
  40 * @uses relationship_api.php
  41 * @uses sponsorship_api.php
  42 * @uses user_api.php
  43 * @uses utility_api.php
  44 */
  45
  46require_api( 'access_api.php' );
  47require_api( 'authentication_api.php' );
  48require_api( 'bug_api.php' );
  49require_api( 'bug_revision_api.php' );
  50require_api( 'bugnote_api.php' );
  51require_api( 'columns_api.php' );
  52require_api( 'config_api.php' );
  53require_api( 'constant_inc.php' );
  54require_api( 'custom_field_api.php' );
  55require_api( 'database_api.php' );
  56require_api( 'gpc_api.php' );
  57require_api( 'helper_api.php' );
  58require_api( 'lang_api.php' );
  59require_api( 'project_api.php' );
  60require_api( 'relationship_api.php' );
  61require_api( 'sponsorship_api.php' );
  62require_api( 'user_api.php' );
  63require_api( 'utility_api.php' );
  64
  65/**
  66 * log the changes (old / new value are supplied to reduce db access)
  67 * events should be logged *after* the modification
  68 * @param integer $p_bug_id     The bug identifier of the bug being modified.
  69 * @param string  $p_field_name The field name of the field being modified.
  70 * @param string  $p_old_value  The old value of the field.
  71 * @param string  $p_new_value  The new value of the field.
  72 * @param integer $p_user_id    The user identifier of the user modifying the bug.
  73 * @param integer $p_type       The type of the modification.
  74 * @return void
  75 */
  76function history_log_event_direct( $p_bug_id, $p_field_name, $p_old_value, $p_new_value, $p_user_id = null, $p_type = 0 ) {
  77	# Only log events that change the value
  78	if( $p_new_value != $p_old_value ) {
  79		if( null === $p_user_id ) {
  80			$p_user_id = auth_get_current_user_id();
  81		}
  82
  83		$c_field_name = $p_field_name;
  84
  85		if( is_null( $p_old_value ) ) {
  86			$c_old_value = '';
  87		} else {
  88			$c_old_value = mb_strimwidth( $p_old_value, 0, DB_FIELD_SIZE_HISTORY_VALUE, '...' );
  89		}
  90		if( is_null( $p_new_value ) ) {
  91			$c_new_value = '';
  92		} else {
  93			$c_new_value = mb_strimwidth( $p_new_value, 0, DB_FIELD_SIZE_HISTORY_VALUE, '...' );
  94		}
  95
  96		db_param_push();
  97		$t_query = 'INSERT INTO {bug_history}
  98						( user_id, bug_id, date_modified, field_name, old_value, new_value, type )
  99					VALUES
 100						( ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ' )';
 101		db_query( $t_query, array( $p_user_id, $p_bug_id, db_now(), $c_field_name, $c_old_value, $c_new_value, $p_type ) );
 102	}
 103}
 104
 105/**
 106 * log the changes
 107 * events should be logged *after* the modification
 108 * @param integer $p_bug_id     The bug identifier of the bug being modified.
 109 * @param string  $p_field_name The field name of the field being modified.
 110 * @param string  $p_old_value  The old value of the field.
 111 * @return void
 112 */
 113function history_log_event( $p_bug_id, $p_field_name, $p_old_value ) {
 114	history_log_event_direct( $p_bug_id, $p_field_name, $p_old_value, bug_get_field( $p_bug_id, $p_field_name ) );
 115}
 116
 117/**
 118 * log the changes
 119 * events should be logged *after* the modification
 120 * These are special case logs (new bug, deleted bugnote, etc.)
 121 * @param integer $p_bug_id    The bug identifier of the bug being modified.
 122 * @param integer $p_type      The type of the modification.
 123 * @param string  $p_old_value The optional value to store in the old_value field.
 124 * @param string  $p_new_value The optional value to store in the new_value field.
 125 * @return void
 126 */
 127function history_log_event_special( $p_bug_id, $p_type, $p_old_value = '', $p_new_value = '' ) {
 128	$t_user_id = auth_get_current_user_id();
 129
 130	if( is_null( $p_old_value ) ) {
 131		$c_old_value = '';
 132	} else {
 133		$c_old_value = mb_strimwidth( $p_old_value, 0, DB_FIELD_SIZE_HISTORY_VALUE, '...' );
 134	}
 135
 136	if( is_null( $p_new_value ) ) {
 137		$c_new_value = '';
 138	} else {
 139		$c_new_value = mb_strimwidth( $p_new_value, 0, DB_FIELD_SIZE_HISTORY_VALUE, '...' );
 140	}
 141
 142	db_param_push();
 143	$t_query = 'INSERT INTO {bug_history}
 144					( user_id, bug_id, date_modified, type, old_value, new_value, field_name )
 145				VALUES
 146					( ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ',' . db_param() . ', ' . db_param() . ')';
 147	db_query( $t_query, array( $t_user_id, $p_bug_id, db_now(), $p_type, $c_old_value, $c_new_value, '' ) );
 148}
 149
 150/**
 151 * Retrieves the history events for the specified bug id and returns it in an array
 152 * The array is indexed from 0 to N-1.  The second dimension is: 'date', 'username',
 153 * 'note', 'change'.
 154 * @param integer $p_bug_id  A valid bug identifier.
 155 * @param integer $p_user_id A valid user identifier.
 156 * @return array
 157 */
 158function history_get_events_array( $p_bug_id, $p_user_id = null ) {
 159	$t_normal_date_format = config_get( 'normal_date_format' );
 160
 161	$t_raw_history = history_get_raw_events_array( $p_bug_id, $p_user_id );
 162	$t_history = array();
 163
 164	foreach( $t_raw_history as $k => $t_item ) {
 165		extract( $t_item, EXTR_PREFIX_ALL, 'v' );
 166		$t_history[$k] = history_localize_item( $v_field, $v_type, $v_old_value, $v_new_value );
 167		$t_history[$k]['date'] = date( $t_normal_date_format, $v_date );
 168		$t_history[$k]['userid'] = $v_userid;
 169		$t_history[$k]['username'] = $v_username;
 170	}
 171
 172	return( $t_history );
 173}
 174
 175/**
 176 * Counts the number of changes done by the specified user within specified time window.
 177 * @param  integer $p_duration_in_seconds The time window in seconds.
 178 * @param  [type]  $p_user_id             The user id or null for logged in user.
 179 * @return integer The number of changes done by user in the specified time window.
 180 */
 181function history_count_user_recent_events( $p_duration_in_seconds, $p_user_id = null ) {
 182	$t_user_id = ( ( null === $p_user_id ) ? auth_get_current_user_id() : $p_user_id );
 183
 184	$t_params = array( db_now() - $p_duration_in_seconds, $t_user_id );
 185
 186	db_param_push();
 187	$t_query = 'SELECT count(*) as event_count FROM {bug_history} WHERE date_modified > ' . db_param() .
 188				' AND user_id = ' . db_param();
 189	$t_result = db_query( $t_query, $t_params );
 190
 191	$t_row = db_fetch_array( $t_result );
 192	return $t_row['event_count'];
 193}
 194
 195/**
 196 * Creates and executes a query for the history rows, returning a database result object.
 197 * Query options is an array with parameters to build the query.
 198 *
 199 * Supported options are:
 200 * - "bug_id" => integer | array    Limit search to these bug ids.
 201 * - "start_time" => integer        Timestamp for start time to filter by (inclusive)
 202 * - "end_time" => integer          Timestamp for end time to filter by (exclusive)
 203 * - "user_id" => integer | array   Limit search to actions by these user ids.
 204 * - "filter" => filter array       A filter array to limit history to bugs matched by this filter.
 205 * - "order" => Sort order          'ASC' or 'DESC' for result order.
 206 *
 207 * Any option can be omitted.
 208 *
 209 * @param array $p_query_options	Array of query options
 210 * @return IteratorAggregate|boolean database result to pass into history_get_event_from_row().
 211 */
 212function history_query_result( array $p_query_options ) {
 213	# check query order by
 214	if( isset( $p_query_options['order'] ) ) {
 215		$t_history_order = $p_query_options['order'];
 216	} else {
 217		$t_history_order = config_get( 'history_order' );
 218	}
 219
 220	$t_query = new DbQuery();
 221	$t_where = array();
 222
 223	# With bug filter
 224	if( isset( $p_query_options['filter'] ) ) {
 225		$t_subquery = new BugFilterQuery( $p_query_options['filter'], BugFilterQuery::QUERY_TYPE_IDS );
 226		$t_where[] = '{bug_history}.bug_id IN ' . $t_query->param( $t_subquery );
 227	}
 228
 229	# Start time
 230	if( isset( $p_query_options['start_time'] ) ) {
 231		$t_where[] = '{bug_history}.date_modified >= ' . $t_query->param( (int)$p_query_options['start_time'] );
 232	}
 233
 234	# End time
 235	if( isset( $p_query_options['end_time'] ) ) {
 236		$t_where[] = '{bug_history}.date_modified < ' . $t_query->param( (int)$p_query_options['end_time'] );
 237	}
 238
 239	# Bug ids
 240	if( isset( $p_query_options['bug_id'] ) ) {
 241		$c_ids = array();
 242		if( is_array( $p_query_options['bug_id'] ) ) {
 243			foreach( $p_query_options['bug_id'] as $t_id ) {
 244				$c_ids[] = (int)$t_id;
 245			}
 246		} else {
 247			$c_ids[] = (int)$p_query_options['bug_id'];
 248		}
 249		$t_where[] = $t_query->sql_in( '{bug_history}.bug_id', $c_ids );
 250	}
 251
 252	# User ids
 253	if( isset( $p_query_options['user_id'] ) ) {
 254		$c_ids = array();
 255		if( is_array( $p_query_options['user_id'] ) ) {
 256			foreach( $p_query_options['user_id'] as $t_id ) {
 257				$c_ids[] = (int)$t_id;
 258			}
 259		} else {
 260			$c_ids[] = (int)$p_query_options['user_id'];
 261		}
 262		$t_where[] = $t_query->sql_in( '{bug_history}.user_id', $c_ids );
 263	}
 264
 265	$t_query->append_sql( 'SELECT * FROM {bug_history}' );
 266	if ( count( $t_where ) > 0 ) {
 267		$t_query->append_sql( ' WHERE ' . implode( ' AND ', $t_where ) );
 268	}
 269
 270	# Order history lines by date. Use the storing sequence as 2nd order field for lines with the same date.
 271	$t_query->append_sql( ' ORDER BY {bug_history}.date_modified ' . $t_history_order . ', {bug_history}.id ' . $t_history_order );
 272	$t_result = $t_query->execute();
 273	return $t_result;
 274}
 275
 276/**
 277 * Creates and executes a query for the history rows related to bugs matched by the provided filter
 278 * @param  array $p_filter           Filter array
 279 * @param  integer $p_start_time     The start time to filter by, or null for all.
 280 * @param  integer $p_end_time       The end time to filter by, or null for all.
 281 * @param  string  $p_history_order  The sort order.
 282 * @return IteratorAggregate|boolean database result to pass into history_get_event_from_row().
 283 * @deprecated		Use history_query_result() instead
 284 */
 285function history_get_range_result_filter( $p_filter, $p_start_time = null, $p_end_time = null, $p_history_order = null ) {
 286	error_parameters( __FUNCTION__ . '()', 'history_query_result()' );
 287	trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
 288
 289	$t_query_options = array();
 290	if ( $p_history_order !== null ) {
 291		$t_query_options['order'] = $p_history_order;
 292	}
 293	if ( $p_start_time !== null ) {
 294		$t_query_options['start_time'] = $p_start_time;
 295	}
 296	if ( $p_end_time !== null ) {
 297		$t_query_options['end_time'] = $p_end_time;
 298	}
 299	if ( $p_filter !== null ) {
 300		$t_query_options['filter'] = $p_filter;
 301	}
 302	return history_query_result( $t_query_options );
 303}
 304
 305/**
 306 * Creates and executes a query for the history rows matching the specified criteria.
 307 * @param  integer $p_bug_id         The bug id or null for matching any bug.
 308 * @param  integer $p_start_time     The start time to filter by, or null for all.
 309 * @param  integer $p_end_time       The end time to filter by, or null for all.
 310 * @param  string  $p_history_order  The sort order.
 311 * @return IteratorAggregate|boolean database result to pass into history_get_event_from_row().
 312 * @deprecated		Use history_query_result() instead
 313 */
 314function history_get_range_result( $p_bug_id = null, $p_start_time = null, $p_end_time = null, $p_history_order = null ) {
 315	error_parameters( __FUNCTION__ . '()', 'history_query_result()' );
 316	trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
 317
 318	$t_query_options = array();
 319	if ( $p_history_order !== null ) {
 320		$t_query_options['order'] = $p_history_order;
 321	}
 322	if ( $p_start_time !== null ) {
 323		$t_query_options['start_time'] = $p_start_time;
 324	}
 325	if ( $p_end_time !== null ) {
 326		$t_query_options['end_time'] = $p_end_time;
 327	}
 328	if ( $p_bug_id !== null ) {
 329		$t_query_options['bug_id'] = $p_bug_id;
 330	}
 331	return history_query_result( $t_query_options );
 332}
 333
 334/**
 335 * Gets the next accessible history event for current user and specified db result.
 336 * @param  object  $p_result      The database result.
 337 * @param  integer $p_user_id     The user id or null for logged in user.
 338 * @param  boolean $p_check_access_to_issue true: check that user has access to bugs,
 339 *                                          false otherwise.
 340 * @return array containing the history event or false if no more matches.
 341 */
 342function history_get_event_from_row( $p_result, $p_user_id = null, $p_check_access_to_issue = true ) {
 343	static $s_bug_visible = array();
 344	$t_user_id = ( null === $p_user_id ) ? auth_get_current_user_id() : $p_user_id;
 345
 346	while ( $t_row = db_fetch_array( $p_result ) ) {
 347		extract( $t_row, EXTR_PREFIX_ALL, 'v' );
 348
 349		# Ignore entries related to non-existing bugs (see #20727)
 350		if( !bug_exists( $v_bug_id ) ) {
 351			continue;
 352		}
 353
 354		if( $p_check_access_to_issue ) {
 355			if( !isset( $s_bug_visible[$v_bug_id] ) ) {
 356				$s_bug_visible[$v_bug_id] = access_has_bug_level( VIEWER, $v_bug_id );
 357			}
 358
 359			if( !$s_bug_visible[$v_bug_id] ) {
 360				continue;
 361			}
 362		}
 363
 364		$t_project_id = bug_get_field( $v_bug_id, 'project_id' );
 365
 366		if( $v_type == NORMAL_TYPE ) {
 367			if( !in_array( $v_field_name, columns_get_standard() ) ) {
 368				# check that the item should be visible to the user
 369				$t_field_id = custom_field_get_id_from_name( $v_field_name );
 370				if( false !== $t_field_id && !custom_field_has_read_access( $t_field_id, $v_bug_id, $t_user_id ) ) {
 371					continue;
 372				}
 373			}
 374
 375			if( ( $v_field_name == 'target_version' ) &&
 376				!access_has_bug_level( config_get( 'roadmap_view_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) ) {
 377				continue;
 378			}
 379
 380			if( ( $v_field_name == 'due_date' ) &&
 381				!access_has_bug_level( config_get( 'due_date_view_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) ) {
 382				continue;
 383			}
 384
 385			if( ( $v_field_name == 'handler_id' ) &&
 386				!access_has_bug_level( config_get( 'view_handler_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) ) {
 387				continue;
 388			}
 389		}
 390
 391		# bugnotes
 392		if( $t_user_id != $v_user_id ) {
 393			# bypass if user originated note
 394			if( ( $v_type == BUGNOTE_ADDED ) || ( $v_type == BUGNOTE_UPDATED ) || ( $v_type == BUGNOTE_DELETED ) ) {
 395				if( !bugnote_exists( $v_old_value ) ) {
 396					continue;
 397				}
 398
 399				if( !access_has_bug_level( config_get( 'private_bugnote_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) && ( bugnote_get_field( $v_old_value, 'view_state' ) == VS_PRIVATE ) ) {
 400					continue;
 401				}
 402			}
 403
 404			if( $v_type == BUGNOTE_STATE_CHANGED ) {
 405				if( !bugnote_exists( $v_new_value ) ) {
 406					continue;
 407				}
 408
 409				if( !access_has_bug_level( config_get( 'private_bugnote_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) && ( bugnote_get_field( $v_new_value, 'view_state' ) == VS_PRIVATE ) ) {
 410					continue;
 411				}
 412			}
 413		}
 414
 415		# tags
 416		if( $v_type == TAG_ATTACHED || $v_type == TAG_DETACHED || $v_type == TAG_RENAMED ) {
 417			if( !access_has_bug_level( config_get( 'tag_view_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) ) {
 418				continue;
 419			}
 420		}
 421
 422		# attachments
 423		if( $v_type == FILE_ADDED || $v_type == FILE_DELETED ) {
 424			if( !access_has_bug_level( config_get( 'view_attachments_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) ) {
 425				continue;
 426			}
 427
 428			# Files were originally just associated with the issue, then association with specific bugnotes
 429			# was added, so handled legacy and new way of handling attachments.
 430			if( !empty( $v_new_value ) && (int)$v_new_value != 0 ) {
 431				if( !bugnote_exists( $v_new_value ) ) {
 432					continue;
 433				}
 434	
 435				if( !access_has_bug_level( config_get( 'private_bugnote_threshold', null, $t_user_id, $t_project_id ), $v_bug_id, $t_user_id ) && ( bugnote_get_field( $v_new_value, 'view_state' ) == VS_PRIVATE ) ) {
 436					continue;
 437				}
 438			}
 439		}
 440
 441		# monitoring
 442		if( $v_type == BUG_MONITOR || $v_type == BUG_UNMONITOR ) {
 443			if( !access_has_bug_level( config_get( 'show_monitor_list_threshold' ), $v_bug_id, $t_user_id ) ) {
 444				continue;
 445			}
 446		}
 447
 448		# relationships
 449		if( $v_type == BUG_ADD_RELATIONSHIP || $v_type == BUG_DEL_RELATIONSHIP || $v_type == BUG_REPLACE_RELATIONSHIP ) {
 450			$t_related_bug_id = $v_new_value;
 451
 452			# If bug doesn't exist, then we don't know whether to expose it or not based on the fact whether it was
 453			# accessible to user or not.  This also simplifies client code that is accessing the history log.
 454			if( !bug_exists( $t_related_bug_id ) || !access_has_bug_level( config_get( 'view_bug_threshold' ), $t_related_bug_id, $t_user_id ) ) {
 455				continue;
 456			}
 457		}
 458
 459		$t_event = array();
 460		$t_event['bug_id'] = $v_bug_id;
 461		$t_event['date'] = $v_date_modified;
 462		$t_event['userid'] = $v_user_id;
 463		$t_event['username'] = user_get_name( $v_user_id );
 464		$t_event['field'] = $v_field_name;
 465		$t_event['type'] = $v_type;
 466		$t_event['old_value'] = $v_old_value;
 467		$t_event['new_value'] = $v_new_value;
 468
 469		return $t_event;
 470	}
 471
 472	return false;
 473}
 474
 475/**
 476 * Retrieves the raw history events for the specified bug id and returns it in an array
 477 * The array is indexed from 0 to N-1.  The second dimension is: 'date', 'userid', 'username',
 478 * 'field','type','old_value','new_value'
 479 * @param integer $p_bug_id  A valid bug identifier or null to not filter by bug.  If no bug id is specified,
 480 *                           then returned array will have a field for bug_id, otherwise it won't.
 481 * @param integer $p_user_id A valid user identifier.
 482 * @param integer $p_start_time The start time to filter by, or null for all.
 483 * @param integer $p_end_time   The end time to filter by, or null for all.
 484 * @return array
 485 */
 486function history_get_raw_events_array( $p_bug_id, $p_user_id = null, $p_start_time = null, $p_end_time = null ) {
 487	$t_user_id = (( null === $p_user_id ) ? auth_get_current_user_id() : $p_user_id );
 488
 489	$t_query_options = array(
 490		'bug_id' => $p_bug_id,
 491		'start_time' => $p_start_time,
 492		'end_time' => $p_end_time
 493		);
 494	$t_result = history_query_result( $t_query_options );
 495
 496	$t_raw_history = array();
 497
 498	$j = 0;
 499	while( true ) {
 500		$t_event = history_get_event_from_row( $t_result, $t_user_id, /* check access */ true );
 501		if ( $t_event === false ) {
 502			break;
 503		}
 504
 505		$t_raw_history[$j] = $t_event;
 506		$j++;
 507	}
 508
 509	# end for loop
 510
 511	return $t_raw_history;
 512}
 513
 514/**
 515 * Localize the specified field name for native or custom fields.
 516 *
 517 * @param string $p_field_name The field name.
 518 * @return string The localized field name.
 519 */
 520function history_localize_field_name( $p_field_name ) {
 521	switch( $p_field_name ) {
 522		case 'category':
 523			$t_field_localized = lang_get( 'category' );
 524			break;
 525		case 'status':
 526			$t_field_localized = lang_get( 'status' );
 527			break;
 528		case 'severity':
 529			$t_field_localized = lang_get( 'severity' );
 530			break;
 531		case 'reproducibility':
 532			$t_field_localized = lang_get( 'reproducibility' );
 533			break;
 534		case 'resolution':
 535			$t_field_localized = lang_get( 'resolution' );
 536			break;
 537		case 'priority':
 538			$t_field_localized = lang_get( 'priority' );
 539			break;
 540		case 'eta':
 541			$t_field_localized = lang_get( 'eta' );
 542			break;
 543		case 'view_state':
 544			$t_field_localized = lang_get( 'view_status' );
 545			break;
 546		case 'projection':
 547			$t_field_localized = lang_get( 'projection' );
 548			break;
 549		case 'sticky':
 550			$t_field_localized = lang_get( 'sticky_issue' );
 551			break;
 552		case 'project_id':
 553			$t_field_localized = lang_get( 'email_project' );
 554			break;
 555		case 'handler_id':
 556			$t_field_localized = lang_get( 'assigned_to' );
 557			break;
 558		case 'reporter_id':
 559			$t_field_localized = lang_get( 'reporter' );
 560			break;
 561		case 'version':
 562			$t_field_localized = lang_get( 'product_version' );
 563			break;
 564		case 'fixed_in_version':
 565			$t_field_localized = lang_get( 'fixed_in_version' );
 566			break;
 567		case 'target_version':
 568			$t_field_localized = lang_get( 'target_version' );
 569			break;
 570		case 'date_submitted':
 571			$t_field_localized = lang_get( 'date_submitted' );
 572			break;
 573		case 'last_updated':
 574			$t_field_localized = lang_get( 'last_update' );
 575			break;
 576		case 'os':
 577			$t_field_localized = lang_get( 'os' );
 578			break;
 579		case 'os_build':
 580			$t_field_localized = lang_get( 'os_build' );
 581			break;
 582		case 'build':
 583			$t_field_localized = lang_get( 'build' );
 584			break;
 585		case 'platform':
 586			$t_field_localized = lang_get( 'platform' );
 587			break;
 588		case 'summary':
 589			$t_field_localized = lang_get( 'summary' );
 590			break;
 591		case 'duplicate_id':
 592			$t_field_localized = lang_get( 'duplicate_id' );
 593			break;
 594		case 'sponsorship_total':
 595			$t_field_localized = lang_get( 'sponsorship_total' );
 596			break;
 597		case 'due_date':
 598			$t_field_localized = lang_get( 'due_date' );
 599			break;
 600		default:
 601			# assume it's a custom field name
 602			$t_field_localized = lang_get_defaulted( $p_field_name );
 603			break;
 604	}
 605
 606	return $t_field_localized;
 607}
 608
 609/**
 610 * Get name of the change type.
 611 *
 612 * @param integer $p_type The type code.
 613 * @return string The type name.
 614 */
 615function history_get_type_name( $p_type ) {
 616	$t_type = (int)$p_type;
 617
 618	switch( $t_type ) {
 619		case NORMAL_TYPE:
 620			$t_type_name = 'field-updated';
 621			break;
 622		case NEW_BUG:
 623			$t_type_name = 'issue-new';
 624			break;
 625		case BUGNOTE_ADDED:
 626			$t_type_name = 'note-added';
 627			break;
 628		case BUGNOTE_UPDATED:
 629			$t_type_name = 'note-updated';
 630			break;
 631		case BUGNOTE_DELETED:
 632			$t_type_name = 'note-deleted';
 633			break;
 634		case DESCRIPTION_UPDATED:
 635			$t_type_name = 'issue-description-updated';
 636			break;
 637		case ADDITIONAL_INFO_UPDATED:
 638			$t_type_name = 'issue-additional-info-updated';
 639			break;
 640		case STEP_TO_REPRODUCE_UPDATED:
 641			$t_type_name = 'issue-steps-to-reproduce-updated';
 642			break;
 643		case FILE_ADDED:
 644			$t_type_name = 'file-added';
 645			break;
 646		case FILE_DELETED:
 647			$t_type_name = 'file-deleted';
 648			break;
 649		case BUGNOTE_STATE_CHANGED:
 650			$t_type_name = 'note-view-state-updated';
 651			break;
 652		case BUG_MONITOR:
 653			$t_type_name = 'monitor-added';
 654			break;
 655		case BUG_UNMONITOR:
 656			$t_type_name = 'monitor-deleted';
 657			break;
 658		case BUG_DELETED:
 659			$t_type_name = 'issue-deleted';
 660			break;
 661		case BUG_ADD_SPONSORSHIP:
 662			$t_type_name = 'sponsorship-added';
 663			break;
 664		case BUG_UPDATE_SPONSORSHIP:
 665			$t_type_name = 'sponsorship-updated';
 666			break;
 667		case BUG_DELETE_SPONSORSHIP:
 668			$t_type_name = 'sponsorship-deleted';
 669			break;
 670		case BUG_PAID_SPONSORSHIP:
 671			$t_type_name = 'sponsorship-paid';
 672			break;
 673		case BUG_ADD_RELATIONSHIP:
 674			$t_type_name = 'relationship-added';
 675			break;
 676		case BUG_REPLACE_RELATIONSHIP:
 677			$t_type_name = 'relationship-updated';
 678			break;
 679		case BUG_DEL_RELATIONSHIP:
 680			$t_type_name = 'relationship-deleted';
 681			break;
 682		case BUG_CLONED_TO:
 683			$t_type_name = 'issue-cloned-to';
 684			break;
 685		case BUG_CREATED_FROM:
 686			$t_type_name = 'issue-cloned-from';
 687			break;
 688		case TAG_ATTACHED:
 689			$t_type_name = 'tag-added';
 690			break;
 691		case TAG_DETACHED:
 692			$t_type_name = 'tag-deleted';
 693			break;
 694		case TAG_RENAMED:
 695			$t_type_name = 'tag-updated';
 696			break;
 697		case BUG_REVISION_DROPPED:
 698			$t_type_name = 'revision-deleted';
 699			break;
 700		case BUGNOTE_REVISION_DROPPED:
 701			$t_type_name = 'note-revision-deleted';
 702			break;
 703		default:
 704			$t_type_name = '';
 705			break;
 706	}
 707
 708	return $t_type_name;
 709}
 710
 711/**
 712 * Localizes one raw history item specified by set the next parameters: $p_field_name, $p_type, $p_old_value, $p_new_value
 713 * Returns array with two elements indexed as 'note' and 'change'
 714 * @param string  $p_field_name The field name of the field being localized.
 715 * @param integer $p_type       The type of the history entry.
 716 * @param string  $p_old_value  The old value of the field.
 717 * @param string  $p_new_value  The new value of the field.
 718 * @param boolean $p_linkify    Whether to return a string containing hyperlinks.
 719 * @return array
 720 */
 721function history_localize_item( $p_field_name, $p_type, $p_old_value, $p_new_value, $p_linkify = true ) {
 722	$t_note = '';
 723	$t_change = '';
 724	$t_raw = true;
 725
 726	if( PLUGIN_HISTORY == $p_type ) {
 727		$t_note = lang_get_defaulted( 'plugin_' . $p_field_name, $p_field_name );
 728		$t_change = ( isset( $p_new_value ) ? $p_old_value . ' => ' . $p_new_value : $p_old_value );
 729
 730		return array( 'note' => $t_note, 'change' => $t_change, 'raw' => true );
 731	}
 732
 733	$t_field_localized = history_localize_field_name( $p_field_name );
 734	switch( $p_field_name ) {
 735		case 'status':
 736			$p_old_value = get_enum_element( 'status', $p_old_value );
 737			$p_new_value = get_enum_element( 'status', $p_new_value );
 738			break;
 739		case 'severity':
 740			$p_old_value = get_enum_element( 'severity', $p_old_value );
 741			$p_new_value = get_enum_element( 'severity', $p_new_value );
 742			break;
 743		case 'reproducibility':
 744			$p_old_value = get_enum_element( 'reproducibility', $p_old_value );
 745			$p_new_value = get_enum_element( 'reproducibility', $p_new_value );
 746			break;
 747		case 'resolution':
 748			$p_old_value = get_enum_element( 'resolution', $p_old_value );
 749			$p_new_value = get_enum_element( 'resolution', $p_new_value );
 750			break;
 751		case 'priority':
 752			$p_old_value = get_enum_element( 'priority', $p_old_value );
 753			$p_new_value = get_enum_element( 'priority', $p_new_value );
 754			break;
 755		case 'eta':
 756			$p_old_value = get_enum_element( 'eta', $p_old_value );
 757			$p_new_value = get_enum_element( 'eta', $p_new_value );
 758			break;
 759		case 'view_state':
 760			$p_old_value = get_enum_element( 'view_state', $p_old_value );
 761			$p_new_value = get_enum_element( 'view_state', $p_new_value );
 762			break;
 763		case 'projection':
 764			$p_old_value = get_enum_element( 'projection', $p_old_value );
 765			$p_new_value = get_enum_element( 'projection', $p_new_value );
 766			break;
 767		case 'sticky':
 768			$p_old_value = gpc_string_to_bool( $p_old_value ) ? lang_get( 'yes' ) : lang_get( 'no' );
 769			$p_new_value = gpc_string_to_bool( $p_new_value ) ? lang_get( 'yes' ) : lang_get( 'no' );
 770			break;
 771		case 'project_id':
 772			if( project_exists( $p_old_value ) ) {
 773				$p_old_value = project_get_field( $p_old_value, 'name' );
 774			} else {
 775				$p_old_value = '@' . $p_old_value . '@';
 776			}
 777
 778			# Note that the new value maybe an intermediately project and not the
 779			# current one.
 780			if( project_exists( $p_new_value ) ) {
 781				$p_new_value = project_get_field( $p_new_value, 'name' );
 782			} else {
 783				$p_new_value = '@' . $p_new_value . '@';
 784			}
 785			break;
 786		case 'handler_id':
 787		case 'reporter_id':
 788			if( 0 == $p_old_value ) {
 789				$p_old_value = '';
 790			} else {
 791				$p_old_value = user_get_name( $p_old_value );
 792			}
 793
 794			if( 0 == $p_new_value ) {
 795				$p_new_value = '';
 796			} else {
 797				$p_new_value = user_get_name( $p_new_value );
 798			}
 799			break;
 800		case 'date_submitted':
 801			$p_old_value = date( config_get( 'normal_date_format' ), $p_old_value );
 802			$p_new_value = date( config_get( 'normal_date_format' ), $p_new_value );
 803			break;
 804		case 'last_updated':
 805			$p_old_value = date( config_get( 'normal_date_format' ), $p_old_value );
 806			$p_new_value = date( config_get( 'normal_date_format' ), $p_new_value );
 807			break;
 808		case 'due_date':
 809			if( $p_old_value !== '' ) {
 810				$p_old_value = date( config_get( 'normal_date_format' ), (int)$p_old_value );
 811			}
 812			if( $p_new_value !== '' ) {
 813				$p_new_value = date( config_get( 'normal_date_format' ), (int)$p_new_value );
 814			}
 815			break;
 816		default:
 817			# assume it's a custom field name
 818			$t_field_id = custom_field_get_id_from_name( $p_field_name );
 819			if( false !== $t_field_id ) {
 820				$t_cf_type = custom_field_type( $t_field_id );
 821				if( '' != $p_old_value ) {
 822					$p_old_value = string_custom_field_value_for_email( $p_old_value, $t_cf_type );
 823				}
 824
 825				$p_new_value = string_custom_field_value_for_email( $p_new_value, $t_cf_type );
 826			}
 827		}
 828
 829		if( NORMAL_TYPE != $p_type ) {
 830			switch( $p_type ) {
 831				case NEW_BUG:
 832					$t_note = lang_get( 'new_bug' );
 833					break;
 834				case BUGNOTE_ADDED:
 835					$t_note = lang_get( 'bugnote_added' ) . ': ' . $p_old_value;
 836					break;
 837				case BUGNOTE_UPDATED:
 838					$t_note = lang_get( 'bugnote_edited' ) . ': ' . $p_old_value;
 839					$t_old_value = (int)$p_old_value;
 840					$t_new_value = (int)$p_new_value;
 841					if( $p_linkify && bug_revision_exists( $t_new_value ) ) {
 842						if( bugnote_exists( $t_old_value ) ) {
 843							$t_bug_revision_view_page_argument = 'bugnote_id=' . $t_old_value . '#r' . $t_new_value;
 844						} else {
 845							$t_bug_revision_view_page_argument = 'rev_id=' . $t_new_value;
 846						}
 847						$t_change = '<a href="bug_revision_view_page.php?' . $t_bug_revision_view_page_argument . '">' .
 848							lang_get( 'view_revisions' ) . '</a>';
 849						$t_raw = false;
 850					}
 851					break;
 852				case BUGNOTE_DELETED:
 853					$t_note = lang_get( 'bugnote_deleted' ) . ': ' . $p_old_value;
 854					break;
 855				case DESCRIPTION_UPDATED:
 856					$t_note = lang_get( 'description_updated' );
 857					$t_old_value = (int)$p_old_value;
 858					if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
 859						$t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
 860							lang_get( 'view_revisions' ) . '</a>';
 861						$t_raw = false;
 862					}
 863					break;
 864				case ADDITIONAL_INFO_UPDATED:
 865					$t_note = lang_get( 'additional_information_updated' );
 866					$t_old_value = (int)$p_old_value;
 867					if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
 868						$t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
 869							lang_get( 'view_revisions' ) . '</a>';
 870						$t_raw = false;
 871					}
 872					break;
 873				case STEP_TO_REPRODUCE_UPDATED:
 874					$t_note = lang_get( 'steps_to_reproduce_updated' );
 875					$t_old_value = (int)$p_old_value;
 876					if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
 877						$t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
 878							lang_get( 'view_revisions' ) . '</a>';
 879						$t_raw = false;
 880					}
 881					break;
 882				case FILE_ADDED:
 883					$t_note = lang_get( 'file_added' ) . ': ' . $p_old_value;
 884					break;
 885				case FILE_DELETED:
 886					$t_note = lang_get( 'file_deleted' ) . ': ' . $p_old_value;
 887					break;
 888				case BUGNOTE_STATE_CHANGED:
 889					$p_old_value = get_enum_element( 'view_state', $p_old_value );
 890					$t_note = lang_get( 'bugnote_view_state' ) . ': ' . $p_new_value . ': ' . $p_old_value;
 891					break;
 892				case BUG_MONITOR:
 893					$p_old_value = user_get_name( $p_old_value );
 894					$t_note = lang_get( 'bug_monitor' ) . ': ' . $p_old_value;
 895					break;
 896				case BUG_UNMONITOR:
 897					if( $p_old_value !== '' ) {
 898						$p_old_value = user_get_name( $p_old_value );
 899					}
 900					$t_note = lang_get( 'bug_end_monitor' ) . ': ' . $p_old_value;
 901					break;
 902				case BUG_DELETED:
 903					$t_note = lang_get( 'bug_deleted' ) . ': ' . $p_old_value;
 904					break;
 905				case BUG_ADD_SPONSORSHIP:
 906					$t_note = lang_get( 'sponsorship_added' );
 907					$t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
 908					break;
 909				case BUG_UPDATE_SPONSORSHIP:
 910					$t_note = lang_get( 'sponsorship_updated' );
 911					$t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
 912					break;
 913				case BUG_DELETE_SPONSORSHIP:
 914					$t_note = lang_get( 'sponsorship_deleted' );
 915					$t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
 916					break;
 917				case BUG_PAID_SPONSORSHIP:
 918					$t_note = lang_get( 'sponsorship_paid' );
 919					$t_change = user_get_name( $p_old_value ) . ': ' . get_enum_element( 'sponsorship', $p_new_value );
 920					break;
 921				case BUG_ADD_RELATIONSHIP:
 922					$t_note = lang_get( 'relationship_added' );
 923					$t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
 924					break;
 925				case BUG_REPLACE_RELATIONSHIP:
 926					$t_note = lang_get( 'relationship_replaced' );
 927					$t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
 928					break;
 929				case BUG_DEL_RELATIONSHIP:
 930					$t_note = lang_get( 'relationship_deleted' );
 931
 932					# Fix for #7846: There are some cases where old value is empty, this may be due to an old bug.
 933					if( !is_blank( $p_old_value ) && $p_old_value > 0 ) {
 934						$t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
 935					} else {
 936						$t_change = bug_format_id( $p_new_value );
 937					}
 938					break;
 939				case BUG_CLONED_TO:
 940					$t_note = lang_get( 'bug_cloned_to' ) . ': ' . bug_format_id( $p_new_value );
 941					break;
 942				case BUG_CREATED_FROM:
 943					$t_note = lang_get( 'bug_created_from' ) . ': ' . bug_format_id( $p_new_value );
 944					break;
 945				case TAG_ATTACHED:
 946					$t_note = lang_get( 'tag_history_attached' ) . ': ' . $p_old_value;
 947					break;
 948				case TAG_DETACHED:
 949					$t_note = lang_get( 'tag_history_detached' ) . ': ' . $p_old_value;
 950					break;
 951				case TAG_RENAMED:
 952					$t_note = lang_get( 'tag_history_renamed' );
 953					$t_change = $p_old_value . ' => ' . $p_new_value;
 954					break;
 955				case BUG_REVISION_DROPPED:
 956					$t_note = lang_get( 'bug_revision_dropped_history' ) . ': ' . bug_revision_get_type_name( $p_new_value ) . ': ' . $p_old_value;
 957					break;
 958				case BUGNOTE_REVISION_DROPPED:
 959					$t_note = lang_get( 'bugnote_revision_dropped_history' ) . ': ' . $p_new_value . ': ' . $p_old_value;
 960					break;
 961			}
 962	}
 963
 964	# output special cases
 965	if( NORMAL_TYPE == $p_type ) {
 966		$t_note = $t_field_localized;
 967		$t_change = $p_old_value . ' => ' . $p_new_value;
 968	}
 969
 970	# end if DEFAULT
 971	return array( 'note' => $t_note, 'change' => $t_change, 'raw' => $t_raw );
 972}
 973
 974/**
 975 * delete all history associated with a bug
 976 * @param integer $p_bug_id A valid bug identifier.
 977 * @return void
 978 */
 979function history_delete( $p_bug_id ) {
 980	db_param_push();
 981	$t_query = 'DELETE FROM {bug_history} WHERE bug_id=' . db_param();
 982	db_query( $t_query, array( $p_bug_id ) );
 983}
 984
 985/**
 986 * Link the file added/deleted history events that match the specified bug_id and filename
 987 * with the specified bugnote id.
 988 *
 989 * @param integer $p_bug_id The bug id.
 990 * @param string $p_filename The filename dot extension (display name).
 991 * @param integer $p_bugnote_id The bugnote id.
 992 * @return void
 993 */
 994function history_link_file_to_bugnote( $p_bug_id, $p_filename, $p_bugnote_id ) {
 995	db_param_push();
 996	$t_query = 'UPDATE {bug_history} SET new_value = ' . db_param() .
 997		' WHERE bug_id=' . db_param() . ' AND old_value=' . db_param() .
 998		' AND (type=' . db_param() . ' OR type=' . db_param() . ')';
 999
1000	db_query( $t_query, array( (int)$p_bugnote_id, (int)$p_bug_id, $p_filename, FILE_ADDED, FILE_DELETED ) );
1001}
1002