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 * Bug API
19 *
20 * @package CoreAPI
21 * @subpackage BugAPI
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 antispam_api.php
28 * @uses authentication_api.php
29 * @uses bugnote_api.php
30 * @uses bug_revision_api.php
31 * @uses category_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 date_api.php
37 * @uses email_api.php
38 * @uses error_api.php
39 * @uses event_api.php
40 * @uses file_api.php
41 * @uses helper_api.php
42 * @uses history_api.php
43 * @uses lang_api.php
44 * @uses mention_api.php
45 * @uses relationship_api.php
46 * @uses sponsorship_api.php
47 * @uses tag_api.php
48 * @uses user_api.php
49 * @uses utility_api.php
50 */
51
52require_api( 'access_api.php' );
53require_api( 'antispam_api.php' );
54require_api( 'authentication_api.php' );
55require_api( 'bugnote_api.php' );
56require_api( 'bug_revision_api.php' );
57require_api( 'category_api.php' );
58require_api( 'config_api.php' );
59require_api( 'constant_inc.php' );
60require_api( 'custom_field_api.php' );
61require_api( 'database_api.php' );
62require_api( 'date_api.php' );
63require_api( 'email_api.php' );
64require_api( 'error_api.php' );
65require_api( 'event_api.php' );
66require_api( 'file_api.php' );
67require_api( 'helper_api.php' );
68require_api( 'history_api.php' );
69require_api( 'lang_api.php' );
70require_api( 'mention_api.php' );
71require_api( 'relationship_api.php' );
72require_api( 'sponsorship_api.php' );
73require_api( 'tag_api.php' );
74require_api( 'user_api.php' );
75require_api( 'utility_api.php' );
76
77use Mantis\Exceptions\ClientException;
78
79/**
80 * Bug Data Structure Definition
81 *
82 * @property int id
83 * @property int project_id
84 * @property int reporter_id
85 * @property int handler_id
86 * @property int duplicate_id
87 * @property int priority
88 * @property int severity
89 * @property int reproducibility
90 * @property int status
91 * @property int resolution
92 * @property int projection
93 * @property int category_id
94 * @property int date_submitted
95 * @property int last_updated
96 * @property int eta
97 * @property string os
98 * @property string os_build
99 * @property string platform
100 * @property string version
101 * @property string fixed_in_version
102 * @property string target_version
103 * @property string build
104 * @property int view_state
105 * @property string summary
106 * @property float sponsorship_total
107 * @property int sticky
108 * @property int due_date
109 * @property int profile_id
110 * @property string description
111 * @property string steps_to_reproduce
112 * @property string additional_information
113 */
114class BugData {
115	/**
116	 * Bug ID
117	 */
118	protected $id;
119
120	/**
121	 * Project ID
122	 */
123	protected $project_id = null;
124
125	/**
126	 * Reporter ID
127	 */
128	protected $reporter_id = 0;
129
130	/**
131	 * Bug Handler ID
132	 */
133	protected $handler_id = 0;
134
135	/**
136	 * Duplicate ID
137	 */
138	protected $duplicate_id = 0;
139
140	/**
141	 * Priority
142	 */
143	protected $priority = NORMAL;
144
145	/**
146	 * Severity
147	 */
148	protected $severity = MINOR;
149
150	/**
151	 * Reproducibility
152	 */
153	protected $reproducibility = 10;
154
155	/**
156	 * Status
157	 */
158	protected $status = NEW_;
159
160	/**
161	 * Resolution
162	 */
163	protected $resolution = OPEN;
164
165	/**
166	 * Projection
167	 */
168	protected $projection = 10;
169
170	/**
171	 * Category ID
172	 */
173	protected $category_id = 1;
174
175	/**
176	 * Date Submitted
177	 */
178	protected $date_submitted = '';
179
180	/**
181	 * Last Updated
182	 */
183	protected $last_updated = '';
184
185	/**
186	 * ETA
187	 */
188	protected $eta = 10;
189
190	/**
191	 * OS
192	 */
193	protected $os = '';
194
195	/**
196	 * OS Build
197	 */
198	protected $os_build = '';
199
200	/**
201	 * Platform
202	 */
203	protected $platform = '';
204
205	/**
206	 * Version
207	 */
208	protected $version = '';
209
210	/**
211	 * Fixed in version
212	 */
213	protected $fixed_in_version = '';
214
215	/**
216	 * Target Version
217	 */
218	protected $target_version = '';
219
220	/**
221	 * Build
222	 */
223	protected $build = '';
224
225	/**
226	 * View State
227	 */
228	protected $view_state = VS_PUBLIC;
229
230	/**
231	 * Summary
232	 */
233	protected $summary = '';
234
235	/**
236	 * Sponsorship Total
237	 */
238	protected $sponsorship_total = 0;
239
240	/**
241	 * Sticky
242	 */
243	protected $sticky = 0;
244
245	/**
246	 * Due Date
247	 */
248	protected $due_date = '';
249
250	/**
251	 * Profile ID
252	 */
253	protected $profile_id = 0;
254
255	/**
256	 * Description
257	 */
258	protected $description = '';
259
260	/**
261	 * Steps to reproduce
262	 */
263	protected $steps_to_reproduce = '';
264
265	/**
266	 * Additional Information
267	 */
268	protected $additional_information = '';
269
270	/**
271	 * Stats
272	 */
273	private $_stats = null;
274
275	/**
276	 * Attachment Count
277	 */
278	public $attachment_count = null;
279
280	/**
281	 * Bugnotes count
282	 */
283	public $bugnotes_count = null;
284
285	/**
286	 * Indicates if bug is currently being loaded from database
287	 */
288	private $loading = false;
289
290	/**
291	 * return number of file attachment's linked to current bug
292	 * @return integer
293	 */
294	public function get_attachment_count() {
295		if( $this->attachment_count === null ) {
296			$this->attachment_count = file_bug_attachment_count( $this->id );
297			return $this->attachment_count;
298		} else {
299			return $this->attachment_count;
300		}
301	}
302
303	/**
304	 * return number of bugnotes's linked to current bug
305	 * @return integer
306	 */
307	public function get_bugnotes_count() {
308		if( $this->bugnotes_count === null ) {
309			$this->bugnotes_count = self::bug_get_bugnote_count();
310			return $this->bugnotes_count;
311		} else {
312			return $this->bugnotes_count;
313		}
314	}
315
316	/**
317	 * Overloaded Function handling property sets
318	 *
319	 * @param string $p_name  Property name.
320	 * @param string $p_value Value to set.
321	 * @private
322	 * @return void
323	 */
324	public function __set( $p_name, $p_value ) {
325		switch( $p_name ) {
326			# integer types
327			case 'id':
328			case 'project_id':
329			case 'reporter_id':
330			case 'handler_id':
331			case 'duplicate_id':
332			case 'priority':
333			case 'severity':
334			case 'reproducibility':
335			case 'status':
336			case 'resolution':
337			case 'eta':
338			case 'projection':
339			case 'category_id':
340				$p_value = (int)$p_value;
341				break;
342			case 'target_version':
343				if( !$this->loading && $this->$p_name != $p_value ) {
344					# Only set target_version if user has access to do so
345					if( !access_has_project_level( config_get( 'roadmap_update_threshold' ) ) ) {
346						trigger_error( ERROR_ACCESS_DENIED, ERROR );
347					}
348				}
349				break;
350			case 'due_date':
351				if( !is_numeric( $p_value ) ) {
352					$p_value = strtotime( $p_value );
353				}
354				break;
355			case 'summary':
356				# MySQL 4-bytes UTF-8 chars workaround #21101
357				$p_value = db_mysql_fix_utf8( $p_value );
358				# Fall through
359			case 'build':
360				if ( !$this->loading ) {
361					$p_value = trim( $p_value );
362				}
363				break;
364			case 'description':
365			case 'steps_to_reproduce':
366			case 'additional_information':
367				# MySQL 4-bytes UTF-8 chars workaround #21101
368				$p_value = db_mysql_fix_utf8( $p_value );
369				break;
370
371		}
372		$this->$p_name = $p_value;
373	}
374
375	/**
376	 * Overloaded Function handling property get
377	 *
378	 * @param string $p_name Property name.
379	 * @private
380	 * @return string|integer|boolean
381	 */
382	public function __get( $p_name ) {
383		if( $this->is_extended_field( $p_name ) ) {
384			$this->fetch_extended_info();
385		}
386		return $this->{$p_name};
387	}
388
389	/**
390	 * Overloaded Function handling property isset
391	 *
392	 * @param string $p_name Property name.
393	 * @private
394	 * @return boolean
395	 */
396	public function __isset( $p_name ) {
397		return isset( $this->{$p_name} );
398	}
399
400	/**
401	 * fast-load database row into bugobject
402	 * @param array $p_row Database result to load into a bug object.
403	 * @return void
404	 */
405	public function loadrow( array $p_row ) {
406		$this->loading = true;
407
408		foreach( $p_row as $t_var => $t_val ) {
409			$this->__set( $t_var, $p_row[$t_var] );
410		}
411		$this->loading = false;
412	}
413
414	/**
415	 * Retrieves extended information for bug (e.g. bug description)
416	 * @return void
417	 */
418	private function fetch_extended_info() {
419		if( $this->description == '' ) {
420			$t_text = bug_text_cache_row( $this->id );
421
422			$this->description = $t_text['description'];
423			$this->steps_to_reproduce = $t_text['steps_to_reproduce'];
424			$this->additional_information = $t_text['additional_information'];
425		}
426	}
427
428	/**
429	 * Returns if the field is an extended field which needs fetch_extended_info()
430	 *
431	 * @param string $p_field_name Field Name.
432	 * @return boolean
433	 */
434	private function is_extended_field( $p_field_name ) {
435		switch( $p_field_name ) {
436			case 'description':
437			case 'steps_to_reproduce':
438			case 'additional_information':
439				return true;
440			default:
441				return false;
442		}
443	}
444
445	/**
446	 * Returns the number of bugnotes for the given bug_id
447	 * @return integer number of bugnotes
448	 * @access private
449	 * @uses database_api.php
450	 */
451	private function bug_get_bugnote_count() {
452		if( !access_has_project_level( config_get( 'private_bugnote_threshold' ), $this->project_id ) ) {
453			$t_restriction = 'AND view_state=' . VS_PUBLIC;
454		} else {
455			$t_restriction = '';
456		}
457
458		db_param_push();
459		$t_query = 'SELECT COUNT(*) FROM {bugnote}
460					  WHERE bug_id =' . db_param() . ' ' . $t_restriction;
461		$t_result = db_query( $t_query, array( $this->id ) );
462
463		return db_result( $t_result );
464	}
465
466	/**
467	 * validate current bug object for database insert/update
468	 * triggers error on failure
469	 * @param boolean $p_update_extended Whether to validate extended fields.
470	 * @return void
471	 */
472	function validate( $p_update_extended = true ) {
473		# Summary cannot be blank
474		if( is_blank( $this->summary ) ) {
475			error_parameters( lang_get( 'summary' ) );
476			trigger_error( ERROR_EMPTY_FIELD, ERROR );
477		}
478
479		if( $p_update_extended ) {
480			# Description field cannot be empty
481			if( is_blank( $this->description ) ) {
482				error_parameters( lang_get( 'description' ) );
483				trigger_error( ERROR_EMPTY_FIELD, ERROR );
484			}
485		}
486
487		# Make sure a category is set
488		if( 0 == $this->category_id && !config_get( 'allow_no_category' ) ) {
489			error_parameters( lang_get( 'category' ) );
490			trigger_error( ERROR_EMPTY_FIELD, ERROR );
491		}
492
493		# Ensure that category id is a valid category
494		if( $this->category_id > 0 ) {
495			category_ensure_exists( $this->category_id );
496		}
497
498		if( !is_blank( $this->duplicate_id ) && ( $this->duplicate_id != 0 ) && ( $this->id == $this->duplicate_id ) ) {
499			trigger_error( ERROR_BUG_DUPLICATE_SELF, ERROR );
500			# never returns
501		}
502	}
503
504	/**
505	 * Insert a new bug into the database
506	 * @return integer integer representing the bug identifier that was created
507	 * @access public
508	 * @uses database_api.php
509	 * @uses lang_api.php
510	 */
511	function create() {
512		self::validate( true );
513
514		antispam_check();
515
516		# check due_date format
517		if( is_blank( $this->due_date ) ) {
518			$this->due_date = date_get_null();
519		}
520		# check date submitted and last modified
521		if( is_blank( $this->date_submitted ) ) {
522			$this->date_submitted = db_now();
523		}
524		if( is_blank( $this->last_updated ) ) {
525			$this->last_updated = db_now();
526		}
527
528		# Insert text information
529		db_param_push();
530		$t_query = 'INSERT INTO {bug_text}
531					    ( description, steps_to_reproduce, additional_information )
532					  VALUES
533					    ( ' . db_param() . ',' . db_param() . ',' . db_param() . ')';
534		db_query( $t_query, array( $this->description, $this->steps_to_reproduce, $this->additional_information ) );
535
536		# Get the id of the text information we just inserted
537		# NOTE: this is guaranteed to be the correct one.
538		# The value LAST_INSERT_ID is stored on a per connection basis.
539
540		$t_text_id = db_insert_id( db_get_table( 'bug_text' ) );
541
542		# check to see if we want to assign this right off
543		$t_original_status = $this->status;
544
545		# if not assigned, check if it should auto-assigned.
546		if( 0 == $this->handler_id ) {
547			# if a default user is associated with the category and we know at this point
548			# that that the bug was not assigned to somebody, then assign it automatically.
549			db_param_push();
550			$t_query = 'SELECT user_id FROM {category} WHERE id=' . db_param();
551			$t_result = db_query( $t_query, array( $this->category_id ) );
552			$t_handler = db_result( $t_result );
553
554			if( $t_handler !== false && user_exists( $t_handler ) ) {
555				$this->handler_id = $t_handler;
556			}
557		}
558
559		# Check if bug was pre-assigned or auto-assigned.
560		$t_status = bug_get_status_for_assign( NO_USER, $this->handler_id, $this->status);
561
562		# Insert the rest of the data
563		db_param_push();
564		$t_query = 'INSERT INTO {bug}
565					    ( project_id,reporter_id, handler_id,duplicate_id,
566					      priority,severity, reproducibility,status,
567					      resolution,projection, category_id,date_submitted,
568					      last_updated,eta, bug_text_id,
569					      os, os_build,platform, version,build,
570					      profile_id, summary, view_state, sponsorship_total, sticky, fixed_in_version,
571					      target_version, due_date
572					    )
573					  VALUES
574					    ( ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
575					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
576					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
577					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
578					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
579					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ',
580					      ' . db_param() . ',' . db_param() . ',' . db_param() . ',' . db_param() . ')';
581		db_query( $t_query, array( $this->project_id, $this->reporter_id, $this->handler_id, $this->duplicate_id, $this->priority, $this->severity, $this->reproducibility, $t_status, $this->resolution, $this->projection, $this->category_id, $this->date_submitted, $this->last_updated, $this->eta, $t_text_id, $this->os, $this->os_build, $this->platform, $this->version, $this->build, $this->profile_id, $this->summary, $this->view_state, $this->sponsorship_total, $this->sticky, $this->fixed_in_version, $this->target_version, $this->due_date ) );
582
583		$this->id = db_insert_id( db_get_table( 'bug' ) );
584
585		# log new bug
586		history_log_event_special( $this->id, NEW_BUG );
587
588		# log changes, if any (compare happens in history_log_event_direct)
589		history_log_event_direct( $this->id, 'status', $t_original_status, $t_status );
590		history_log_event_direct( $this->id, 'handler_id', 0, $this->handler_id );
591
592		return $this->id;
593	}
594
595	/**
596	 * Process mentions in the current issue, for example, after the issue is created.
597	 * @return void
598	 * @access public
599	 */
600	function process_mentions() {
601		# Now that the issue is added process the @ mentions
602		$t_all_mentioned_user_ids = array();
603
604		$t_mentioned_user_ids = mention_get_users( $this->summary );
605		$t_all_mentioned_user_ids = array_merge( $t_all_mentioned_user_ids, $t_mentioned_user_ids );
606
607		$t_mentioned_user_ids = mention_get_users( $this->description );
608		$t_all_mentioned_user_ids = array_merge( $t_all_mentioned_user_ids, $t_mentioned_user_ids );
609
610		if( !is_blank( $this->steps_to_reproduce ) ) {
611			$t_mentioned_user_ids = mention_get_users( $this->steps_to_reproduce );
612			$t_all_mentioned_user_ids = array_merge( $t_all_mentioned_user_ids, $t_mentioned_user_ids );
613		}
614
615		if( !is_blank( $this->additional_information ) ) {
616			$t_mentioned_user_ids = mention_get_users( $this->additional_information );
617			$t_all_mentioned_user_ids = array_merge( $t_all_mentioned_user_ids, $t_mentioned_user_ids );
618		}
619
620		$t_filtered_mentioned_user_ids = access_has_bug_level_filter(
621			config_get( 'view_bug_threshold' ),
622			$this->id,
623			$t_all_mentioned_user_ids );
624
625		$t_removed_mentions_user_ids = array_diff( $t_all_mentioned_user_ids, $t_filtered_mentioned_user_ids );
626
627		if( !empty( $t_all_mentioned_user_ids ) ) {
628			$t_mention_text = $this->description . "\n\n";
629
630			if( !is_blank( $this->steps_to_reproduce ) ) {
631				$t_mention_text .= lang_get( 'email_steps_to_reproduce' ) . "\n\n";
632				$t_mention_text .= $this->steps_to_reproduce . "\n\n";
633			}
634
635			if( !is_blank( $this->additional_information ) ) {
636				$t_mention_text .= lang_get( 'email_additional_information' ) . "\n\n";
637				$t_mention_text .= $this->additional_information . "\n\n";
638			}
639
640			mention_process_user_mentions(
641				$this->id,
642				$t_filtered_mentioned_user_ids,
643				$t_mention_text,
644				$t_removed_mentions_user_ids );
645		}
646	}
647
648	/**
649     * Update a bug from the given data structure
650     *  If the third parameter is true, also update the longer strings table
651     * @param boolean $p_update_extended Whether to update extended fields.
652     * @param boolean $p_bypass_mail     Whether to bypass sending email notifications.
653     * @internal param boolean $p_bypass_email Default false, set to true to avoid generating emails (if sending elsewhere)
654     * @return boolean (always true)
655     * @access public
656	 */
657	function update( $p_update_extended = false, $p_bypass_mail = false ) {
658		self::validate( $p_update_extended );
659
660		$c_bug_id = $this->id;
661
662		if( is_blank( $this->due_date ) ) {
663			$this->due_date = date_get_null();
664		}
665
666		$t_old_data = bug_get( $this->id, true );
667
668		# Update all fields
669		# Ignore date_submitted and last_updated since they are pulled out
670		#  as unix timestamps which could confuse the history log and they
671		#  shouldn't get updated like this anyway.  If you really need to change
672		#  them use bug_set_field()
673		db_param_push();
674		$t_query = 'UPDATE {bug}
675					SET project_id=' . db_param() . ', reporter_id=' . db_param() . ',
676						handler_id=' . db_param() . ', duplicate_id=' . db_param() . ',
677						priority=' . db_param() . ', severity=' . db_param() . ',
678						reproducibility=' . db_param() . ', status=' . db_param() . ',
679						resolution=' . db_param() . ', projection=' . db_param() . ',
680						category_id=' . db_param() . ', eta=' . db_param() . ',
681						os=' . db_param() . ', os_build=' . db_param() . ',
682						platform=' . db_param() . ', version=' . db_param() . ',
683						build=' . db_param() . ', fixed_in_version=' . db_param() . ',';
684
685		$t_fields = array(
686			$this->project_id, $this->reporter_id,
687			$this->handler_id, $this->duplicate_id,
688			$this->priority, $this->severity,
689			$this->reproducibility, $this->status,
690			$this->resolution, $this->projection,
691			$this->category_id, $this->eta,
692			$this->os, $this->os_build,
693			$this->platform, $this->version,
694			$this->build, $this->fixed_in_version,
695		);
696		$t_roadmap_updated = false;
697		if( access_has_project_level( config_get( 'roadmap_update_threshold' ) ) ) {
698			$t_query .= '
699						target_version=' . db_param() . ',';
700			$t_fields[] = $this->target_version;
701			$t_roadmap_updated = true;
702		}
703
704		$t_query .= '
705						view_state=' . db_param() . ',
706						summary=' . db_param() . ',
707						sponsorship_total=' . db_param() . ',
708						sticky=' . db_param() . ',
709						due_date=' . db_param() . '
710					WHERE id=' . db_param();
711		$t_fields[] = $this->view_state;
712		$t_fields[] = $this->summary;
713		$t_fields[] = $this->sponsorship_total;
714		$t_fields[] = (bool)$this->sticky;
715		$t_fields[] = $this->due_date;
716		$t_fields[] = $this->id;
717
718		db_query( $t_query, $t_fields );
719
720		bug_clear_cache( $this->id );
721
722		# log changes
723		history_log_event_direct( $c_bug_id, 'project_id', $t_old_data->project_id, $this->project_id );
724		history_log_event_direct( $c_bug_id, 'reporter_id', $t_old_data->reporter_id, $this->reporter_id );
725		history_log_event_direct( $c_bug_id, 'handler_id', $t_old_data->handler_id, $this->handler_id );
726		history_log_event_direct( $c_bug_id, 'priority', $t_old_data->priority, $this->priority );
727		history_log_event_direct( $c_bug_id, 'severity', $t_old_data->severity, $this->severity );
728		history_log_event_direct( $c_bug_id, 'reproducibility', $t_old_data->reproducibility, $this->reproducibility );
729		history_log_event_direct( $c_bug_id, 'status', $t_old_data->status, $this->status );
730		history_log_event_direct( $c_bug_id, 'resolution', $t_old_data->resolution, $this->resolution );
731		history_log_event_direct( $c_bug_id, 'projection', $t_old_data->projection, $this->projection );
732		history_log_event_direct( $c_bug_id, 'category', category_full_name( $t_old_data->category_id, false ), category_full_name( $this->category_id, false ) );
733		history_log_event_direct( $c_bug_id, 'eta', $t_old_data->eta, $this->eta );
734		history_log_event_direct( $c_bug_id, 'os', $t_old_data->os, $this->os );
735		history_log_event_direct( $c_bug_id, 'os_build', $t_old_data->os_build, $this->os_build );
736		history_log_event_direct( $c_bug_id, 'platform', $t_old_data->platform, $this->platform );
737		history_log_event_direct( $c_bug_id, 'version', $t_old_data->version, $this->version );
738		history_log_event_direct( $c_bug_id, 'build', $t_old_data->build, $this->build );
739		history_log_event_direct( $c_bug_id, 'fixed_in_version', $t_old_data->fixed_in_version, $this->fixed_in_version );
740		if( $t_roadmap_updated ) {
741			history_log_event_direct( $c_bug_id, 'target_version', $t_old_data->target_version, $this->target_version );
742		}
743		history_log_event_direct( $c_bug_id, 'view_state', $t_old_data->view_state, $this->view_state );
744		history_log_event_direct( $c_bug_id, 'summary', $t_old_data->summary, $this->summary );
745		history_log_event_direct( $c_bug_id, 'sponsorship_total', $t_old_data->sponsorship_total, $this->sponsorship_total );
746		history_log_event_direct( $c_bug_id, 'sticky', $t_old_data->sticky, $this->sticky );
747
748		history_log_event_direct( $c_bug_id, 'due_date',
749			( $t_old_data->due_date != date_get_null() ) ? $t_old_data->due_date : null,
750			( $this->due_date != date_get_null() ) ? $this->due_date : null
751		);
752
753		# Update extended info if requested
754		if( $p_update_extended ) {
755			$t_bug_text_id = bug_get_field( $c_bug_id, 'bug_text_id' );
756
757			db_param_push();
758			$t_query = 'UPDATE {bug_text}
759							SET description=' . db_param() . ',
760								steps_to_reproduce=' . db_param() . ',
761								additional_information=' . db_param() . '
762							WHERE id=' . db_param();
763			db_query( $t_query, array(
764				$this->description,
765				$this->steps_to_reproduce,
766				$this->additional_information,
767				$t_bug_text_id ) );
768
769			bug_text_clear_cache( $c_bug_id );
770
771			$t_current_user = auth_get_current_user_id();
772
773			if( $t_old_data->description != $this->description ) {
774				if( bug_revision_count( $c_bug_id, REV_DESCRIPTION ) < 1 ) {
775					bug_revision_add( $c_bug_id, $t_old_data->reporter_id, REV_DESCRIPTION, $t_old_data->description, 0, $t_old_data->date_submitted );
776				}
777				$t_revision_id = bug_revision_add( $c_bug_id, $t_current_user, REV_DESCRIPTION, $this->description );
778				history_log_event_special( $c_bug_id, DESCRIPTION_UPDATED, $t_revision_id );
779			}
780
781			if( $t_old_data->steps_to_reproduce != $this->steps_to_reproduce ) {
782				if( bug_revision_count( $c_bug_id, REV_STEPS_TO_REPRODUCE ) < 1 ) {
783					bug_revision_add( $c_bug_id, $t_old_data->reporter_id, REV_STEPS_TO_REPRODUCE, $t_old_data->steps_to_reproduce, 0, $t_old_data->date_submitted );
784				}
785				$t_revision_id = bug_revision_add( $c_bug_id, $t_current_user, REV_STEPS_TO_REPRODUCE, $this->steps_to_reproduce );
786				history_log_event_special( $c_bug_id, STEP_TO_REPRODUCE_UPDATED, $t_revision_id );
787			}
788
789			if( $t_old_data->additional_information != $this->additional_information ) {
790				if( bug_revision_count( $c_bug_id, REV_ADDITIONAL_INFO ) < 1 ) {
791					bug_revision_add( $c_bug_id, $t_old_data->reporter_id, REV_ADDITIONAL_INFO, $t_old_data->additional_information, 0, $t_old_data->date_submitted );
792				}
793				$t_revision_id = bug_revision_add( $c_bug_id, $t_current_user, REV_ADDITIONAL_INFO, $this->additional_information );
794				history_log_event_special( $c_bug_id, ADDITIONAL_INFO_UPDATED, $t_revision_id );
795			}
796		}
797
798		# Update the last update date
799		bug_update_date( $c_bug_id );
800
801		# allow bypass if user is sending mail separately
802		if( false == $p_bypass_mail ) {
803			# If handler changes, send out owner change email
804			if( $t_old_data->handler_id != $this->handler_id ) {
805				email_owner_changed( $c_bug_id, $t_old_data->handler_id, $this->handler_id );
806				return true;
807			}
808
809			# status changed
810			if( $t_old_data->status != $this->status ) {
811				$t_status = MantisEnum::getLabel( config_get( 'status_enum_string' ), $this->status );
812				$t_status = str_replace( ' ', '_', $t_status );
813				email_bug_status_changed( $c_bug_id, $t_status );
814				return true;
815			}
816
817			# @todo handle priority change if it requires special handling
818			email_bug_updated( $c_bug_id );
819		}
820
821		return true;
822	}
823}
824
825$g_cache_bug = array();
826$g_cache_bug_text = array();
827
828/**
829 * Cache a database result-set containing full contents of bug_table row.
830 * $p_stats parameter is an optional array representing bugnote statistics.
831 * This parameter can be "false" if the bug has no bugnotes, so the cache can differentiate
832 * from a still not cached stats registry.
833 * @param array $p_bug_database_result  Database row containing all columns from mantis_bug_table.
834 * @param array|boolean|null $p_stats   Optional: array representing bugnote statistics, or false to store empty cache value
835 * @return array returns an array representing the bug row if bug exists
836 * @access public
837 */
838function bug_cache_database_result( array $p_bug_database_result, $p_stats = null ) {
839	global $g_cache_bug;
840
841	if( !is_array( $p_bug_database_result ) || isset( $g_cache_bug[(int)$p_bug_database_result['id']] ) ) {
842		if( !is_null($p_stats) ) {
843			# force store the bugnote statistics
844			return bug_add_to_cache( $p_bug_database_result, $p_stats );
845		} else {
846			return $g_cache_bug[(int)$p_bug_database_result['id']];
847		}
848	}
849
850	return bug_add_to_cache( $p_bug_database_result, $p_stats );
851}
852
853/**
854 * Cache a bug row if necessary and return the cached copy
855 * @param integer $p_bug_id         Identifier of bug to cache from mantis_bug_table.
856 * @param boolean $p_trigger_errors Set to true to trigger an error if the bug does not exist.
857 * @return boolean|array returns an array representing the bug row if bug exists or false if bug does not exist
858 * @access public
859 * @uses database_api.php
860 */
861function bug_cache_row( $p_bug_id, $p_trigger_errors = true ) {
862	global $g_cache_bug;
863
864	if( isset( $g_cache_bug[$p_bug_id] ) ) {
865		return $g_cache_bug[$p_bug_id];
866	}
867
868	$c_bug_id = (int)$p_bug_id;
869
870	db_param_push();
871	$t_query = 'SELECT * FROM {bug} WHERE id=' . db_param();
872	$t_result = db_query( $t_query, array( $c_bug_id ) );
873
874	$t_row = db_fetch_array( $t_result );
875
876	if( !$t_row ) {
877		$g_cache_bug[$c_bug_id] = false;
878
879		if( $p_trigger_errors ) {
880			throw new ClientException( "Issue #$c_bug_id not found", ERROR_BUG_NOT_FOUND, array( $p_bug_id ) );
881		}
882
883		return false;
884	}
885
886	return bug_add_to_cache( $t_row );
887}
888
889/**
890 * Cache a set of bugs
891 * @param array $p_bug_id_array Integer array representing bug identifiers to cache.
892 * @return void
893 * @access public
894 * @uses database_api.php
895 */
896function bug_cache_array_rows( array $p_bug_id_array ) {
897	global $g_cache_bug;
898	$c_bug_id_array = array();
899
900	foreach( $p_bug_id_array as $t_bug_id ) {
901		if( !isset( $g_cache_bug[(int)$t_bug_id] ) ) {
902			$c_bug_id_array[] = (int)$t_bug_id;
903		}
904	}
905
906	if( empty( $c_bug_id_array ) ) {
907		return;
908	}
909
910	$t_query = 'SELECT * FROM {bug} WHERE id IN (' . implode( ',', $c_bug_id_array ) . ')';
911	$t_result = db_query( $t_query );
912
913	while( $t_row = db_fetch_array( $t_result ) ) {
914		bug_add_to_cache( $t_row );
915	}
916	return;
917}
918
919/**
920 * Inject a bug into the bug cache.
921 * $p_stats parameter is an optional array representing bugnote statistics.
922 * This parameter can be "false" if the bug has no bugnotes, so the cache can differentiate
923 * from a still not cached stats registry.
924 * @param array $p_bug_row A bug row to cache.
925 * @param array|boolean|null $p_stats   Array of Bugnote stats to cache, false to store empty value, null to skip
926 * @return array
927 * @access private
928 */
929function bug_add_to_cache( array $p_bug_row, $p_stats = null ) {
930	global $g_cache_bug;
931
932	$g_cache_bug[(int)$p_bug_row['id']] = $p_bug_row;
933
934	if( !is_null( $p_stats ) ) {
935		$g_cache_bug[(int)$p_bug_row['id']]['_stats'] = $p_stats;
936	}
937
938	return $g_cache_bug[(int)$p_bug_row['id']];
939}
940
941/**
942 * Clear a bug from the cache or all bugs if no bug id specified.
943 * @param integer $p_bug_id A bug identifier to clear (optional).
944 * @return boolean
945 * @access public
946 */
947function bug_clear_cache( $p_bug_id = null ) {
948	global $g_cache_bug;
949
950	if( null === $p_bug_id ) {
951		$g_cache_bug = array();
952	} else {
953		unset( $g_cache_bug[(int)$p_bug_id] );
954	}
955
956	return true;
957}
958
959/**
960 * Cache a bug text row if necessary and return the cached copy
961 * @param integer $p_bug_id         Integer bug id to retrieve text for.
962 * @param boolean $p_trigger_errors If the second parameter is true (default), trigger an error if bug text not found.
963 * @return boolean|array returns false if not bug text found or array of bug text
964 * @access public
965 * @uses database_api.php
966 */
967function bug_text_cache_row( $p_bug_id, $p_trigger_errors = true ) {
968	global $g_cache_bug_text;
969
970	$c_bug_id = (int)$p_bug_id;
971
972	if( isset( $g_cache_bug_text[$c_bug_id] ) ) {
973		return $g_cache_bug_text[$c_bug_id];
974	}
975
976	db_param_push();
977	$t_query = 'SELECT bt.* FROM {bug_text} bt, {bug} b
978				  WHERE b.id=' . db_param() . ' AND b.bug_text_id = bt.id';
979	$t_result = db_query( $t_query, array( $c_bug_id ) );
980
981	$t_row = db_fetch_array( $t_result );
982
983	if( !$t_row ) {
984		$g_cache_bug_text[$c_bug_id] = false;
985
986		if( $p_trigger_errors ) {
987			throw new ClientException(
988				"Issue '$p_bug_id' not found",
989				ERROR_BUG_NOT_FOUND,
990				array( $p_bug_id ) );
991		}
992
993		return false;
994	}
995
996	$g_cache_bug_text[$c_bug_id] = $t_row;
997
998	return $t_row;
999}
1000
1001/**
1002 * Clear a bug's bug text from the cache or all bug text if no bug id specified.
1003 * @param integer $p_bug_id A bug identifier to clear (optional).
1004 * @return boolean
1005 * @access public
1006 */
1007function bug_text_clear_cache( $p_bug_id = null ) {
1008	global $g_cache_bug_text;
1009
1010	if( null === $p_bug_id ) {
1011		$g_cache_bug_text = array();
1012	} else {
1013		unset( $g_cache_bug_text[(int)$p_bug_id] );
1014	}
1015
1016	return true;
1017}
1018
1019/**
1020 * Check if a bug exists
1021 * @param integer $p_bug_id Integer representing bug identifier.
1022 * @return boolean true if bug exists, false otherwise
1023 * @access public
1024 */
1025function bug_exists( $p_bug_id ) {
1026	$c_bug_id = (int)$p_bug_id;
1027
1028	# Check for invalid id values
1029	if( $c_bug_id <= 0 || $c_bug_id > DB_MAX_INT ) {
1030		return false;
1031	}
1032
1033	# bug exists if bug_cache_row returns any value
1034	if( bug_cache_row( $c_bug_id, false ) ) {
1035		return true;
1036	} else {
1037		return false;
1038	}
1039}
1040
1041/**
1042 * Check if a bug exists. If it doesn't then trigger an error
1043 * @param integer $p_bug_id Integer representing bug identifier.
1044 * @return void
1045 * @access public
1046 */
1047function bug_ensure_exists( $p_bug_id ) {
1048	if( !bug_exists( $p_bug_id ) ) {
1049		throw new ClientException(
1050			"Issue #$p_bug_id not found",
1051			ERROR_BUG_NOT_FOUND,
1052			array( $p_bug_id ) );
1053	}
1054}
1055
1056/**
1057 * check if the given user is the reporter of the bug
1058 * @param integer $p_bug_id  Integer representing bug identifier.
1059 * @param integer $p_user_id Integer representing a user identifier.
1060 * @return boolean return true if the user is the reporter, false otherwise
1061 * @access public
1062 */
1063function bug_is_user_reporter( $p_bug_id, $p_user_id ) {
1064	if( bug_get_field( $p_bug_id, 'reporter_id' ) == $p_user_id ) {
1065		return true;
1066	} else {
1067		return false;
1068	}
1069}
1070
1071/**
1072 * check if the given user is the handler of the bug
1073 * @param integer $p_bug_id  Integer representing bug identifier.
1074 * @param integer $p_user_id Integer representing a user identifier.
1075 * @return boolean return true if the user is the handler, false otherwise
1076 * @access public
1077 */
1078function bug_is_user_handler( $p_bug_id, $p_user_id ) {
1079	if( bug_get_field( $p_bug_id, 'handler_id' ) == $p_user_id ) {
1080		return true;
1081	} else {
1082		return false;
1083	}
1084}
1085
1086/**
1087 * Check if the bug is readonly and shouldn't be modified
1088 * For a bug to be readonly the status has to be >= bug_readonly_status_threshold and
1089 * current user access level < update_readonly_bug_threshold.
1090 * @param integer $p_bug_id Integer representing bug identifier.
1091 * @return boolean
1092 * @access public
1093 * @uses access_api.php
1094 * @uses config_api.php
1095 */
1096function bug_is_readonly( $p_bug_id ) {
1097	$t_status = bug_get_field( $p_bug_id, 'status' );
1098	if( $t_status < config_get( 'bug_readonly_status_threshold', null, null, bug_get_field( $p_bug_id, 'project_id' ) ) ) {
1099		return false;
1100	}
1101
1102	if( access_has_bug_level( config_get( 'update_readonly_bug_threshold' ), $p_bug_id ) ) {
1103		return false;
1104	}
1105
1106	return true;
1107}
1108
1109/**
1110 * Check if a given bug is resolved
1111 * @param integer $p_bug_id Integer representing bug identifier.
1112 * @return boolean true if bug is resolved, false otherwise
1113 * @access public
1114 * @uses config_api.php
1115 */
1116function bug_is_resolved( $p_bug_id ) {
1117	$t_bug = bug_get( $p_bug_id );
1118	return( $t_bug->status >= config_get( 'bug_resolved_status_threshold', null, null, $t_bug->project_id ) );
1119}
1120
1121/**
1122 * Check if a given bug is closed
1123 * @param integer $p_bug_id Integer representing bug identifier.
1124 * @return boolean true if bug is closed, false otherwise
1125 * @access public
1126 * @uses config_api.php
1127 */
1128function bug_is_closed( $p_bug_id ) {
1129	$t_bug = bug_get( $p_bug_id );
1130	return( $t_bug->status >= config_get( 'bug_closed_status_threshold', null, null, $t_bug->project_id ) );
1131}
1132
1133/**
1134 * Return a bug's overdue warning level.
1135 * Determines the level based on the difference between the bug's due date
1136 * and the current date/time, based on the defined delays
1137 * @see $g_due_date_warning_levels
1138 *
1139 * @param $p_bug_id
1140 *
1141 * @return int|false Warning level (0 = overdue), false if N/A.
1142 */
1143function bug_overdue_level( $p_bug_id ) {
1144	if( bug_is_resolved( $p_bug_id ) ) {
1145		return false;
1146	}
1147
1148	$t_bug = bug_get( $p_bug_id );
1149	$t_due_date = $t_bug->due_date;
1150
1151	if( date_is_null( $t_due_date ) ) {
1152		return false;
1153	}
1154
1155	$t_warning_levels = config_get( 'due_date_warning_levels', null, null, $t_bug->project_id );
1156	if( !empty( $t_warning_levels ) && !is_array( $t_warning_levels ) ) {
1157		trigger_error( ERROR_GENERIC );
1158	}
1159
1160	$t_now = db_now();
1161	foreach( $t_warning_levels as $t_level => $t_delay ) {
1162		if( $t_now > $t_due_date - $t_delay ) {
1163			return $t_level;
1164		}
1165	}
1166	return false;
1167}
1168
1169/**
1170 * Check if a given bug is overdue
1171 * @param integer $p_bug_id Integer representing bug identifier.
1172 * @return boolean true if bug is overdue, false otherwise
1173 * @access public
1174 * @uses database_api.php
1175 */
1176function bug_is_overdue( $p_bug_id ) {
1177	return bug_overdue_level( $p_bug_id ) === 0;
1178}
1179
1180/**
1181 * Validate workflow state to see if bug can be moved to requested state
1182 * @param integer $p_bug_status    Current bug status.
1183 * @param integer $p_wanted_status New bug status.
1184 * @return boolean
1185 * @access public
1186 * @uses config_api.php
1187 * @uses utility_api.php
1188 */
1189function bug_check_workflow( $p_bug_status, $p_wanted_status ) {
1190	$t_status_enum_workflow = config_get( 'status_enum_workflow' );
1191
1192	if( count( $t_status_enum_workflow ) < 1 ) {
1193		# workflow not defined, use default enum
1194		return true;
1195	}
1196
1197	if( $p_bug_status == $p_wanted_status ) {
1198		# no change in state, allow the transition
1199		return true;
1200	}
1201
1202	# There should always be a possible next status, if not defined, then allow all.
1203	if( !isset( $t_status_enum_workflow[$p_bug_status] ) ) {
1204		return true;
1205	}
1206
1207	# workflow defined - find allowed states
1208	$t_allowed_states = $t_status_enum_workflow[$p_bug_status];
1209
1210	return MantisEnum::hasValue( $t_allowed_states, $p_wanted_status );
1211}
1212
1213/**
1214 * Copy a bug from one project to another. Also make copies of issue notes, attachments, history,
1215 * email notifications etc.
1216 * @param integer $p_bug_id                A bug identifier.
1217 * @param integer $p_target_project_id     A target project identifier.
1218 * @param boolean $p_copy_custom_fields    Whether to copy custom fields.
1219 * @param boolean $p_copy_relationships    Whether to copy relationships.
1220 * @param boolean $p_copy_history          Whether to copy history.
1221 * @param boolean $p_copy_attachments      Whether to copy attachments.
1222 * @param boolean $p_copy_bugnotes         Whether to copy bugnotes.
1223 * @param boolean $p_copy_monitoring_users Whether to copy monitoring users.
1224 * @return integer representing the new bug identifier
1225 * @access public
1226 */
1227function bug_copy( $p_bug_id, $p_target_project_id = null, $p_copy_custom_fields = false, $p_copy_relationships = false, $p_copy_history = false, $p_copy_attachments = false, $p_copy_bugnotes = false, $p_copy_monitoring_users = false ) {
1228
1229	$t_bug_id = (int)$p_bug_id;
1230	$t_target_project_id = (int)$p_target_project_id;
1231
1232	$t_bug_data = bug_get( $t_bug_id, true );
1233
1234	# retrieve the project id associated with the bug
1235	if( ( $p_target_project_id == null ) || is_blank( $p_target_project_id ) ) {
1236		$t_target_project_id = $t_bug_data->project_id;
1237	}
1238
1239	$t_bug_data->project_id = $t_target_project_id;
1240	$t_bug_data->reporter_id = auth_get_current_user_id();
1241	$t_bug_data->date_submitted = db_now();
1242	$t_bug_data->last_updated = db_now();
1243
1244	$t_new_bug_id = $t_bug_data->create();
1245
1246	# MASC ATTENTION: IF THE SOURCE BUG HAS TO HANDLER THE bug_create FUNCTION CAN TRY TO AUTO-ASSIGN THE BUG
1247	# WE FORCE HERE TO DUPLICATE THE SAME HANDLER OF THE SOURCE BUG
1248	# @todo VB: Shouldn't we check if the handler in the source project is also a handler in the destination project?
1249	bug_set_field( $t_new_bug_id, 'handler_id', $t_bug_data->handler_id );
1250
1251	bug_set_field( $t_new_bug_id, 'duplicate_id', $t_bug_data->duplicate_id );
1252	bug_set_field( $t_new_bug_id, 'status', $t_bug_data->status );
1253	bug_set_field( $t_new_bug_id, 'resolution', $t_bug_data->resolution );
1254	bug_set_field( $t_new_bug_id, 'projection', $t_bug_data->projection );
1255	bug_set_field( $t_new_bug_id, 'eta', $t_bug_data->eta );
1256	bug_set_field( $t_new_bug_id, 'fixed_in_version', $t_bug_data->fixed_in_version );
1257	bug_set_field( $t_new_bug_id, 'target_version', $t_bug_data->target_version );
1258	bug_set_field( $t_new_bug_id, 'sponsorship_total', 0 );
1259	bug_set_field( $t_new_bug_id, 'sticky', 0 );
1260	bug_set_field( $t_new_bug_id, 'due_date', $t_bug_data->due_date );
1261
1262	# COPY CUSTOM FIELDS
1263	if( $p_copy_custom_fields ) {
1264		db_param_push();
1265		$t_query = 'SELECT field_id, bug_id, value, text FROM {custom_field_string} WHERE bug_id=' . db_param();
1266		$t_result = db_query( $t_query, array( $t_bug_id ) );
1267
1268		while( $t_bug_custom = db_fetch_array( $t_result ) ) {
1269			$c_field_id = (int)$t_bug_custom['field_id'];
1270			$c_new_bug_id = (int)$t_new_bug_id;
1271			$c_value = $t_bug_custom['value'];
1272			$c_text = $t_bug_custom['text'];
1273
1274			db_param_push();
1275			$t_query = 'INSERT INTO {custom_field_string}
1276						   ( field_id, bug_id, value, text )
1277						   VALUES (' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ')';
1278			db_query( $t_query, array( $c_field_id, $c_new_bug_id, $c_value, $c_text ) );
1279		}
1280	}
1281
1282	# Copy Relationships
1283	if( $p_copy_relationships ) {
1284		relationship_copy_all( $t_bug_id, $t_new_bug_id );
1285	}
1286
1287	# Copy bugnotes
1288	if( $p_copy_bugnotes ) {
1289		db_param_push();
1290		$t_query = 'SELECT * FROM {bugnote} WHERE bug_id=' . db_param();
1291		$t_result = db_query( $t_query, array( $t_bug_id ) );
1292
1293		while( $t_bug_note = db_fetch_array( $t_result ) ) {
1294			$t_bugnote_text_id = $t_bug_note['bugnote_text_id'];
1295
1296			db_param_push();
1297			$t_query2 = 'SELECT * FROM {bugnote_text} WHERE id=' . db_param();
1298			$t_result2 = db_query( $t_query2, array( $t_bugnote_text_id ) );
1299
1300			$t_bugnote_text_insert_id = -1;
1301			if( $t_bugnote_text = db_fetch_array( $t_result2 ) ) {
1302				db_param_push();
1303				$t_query2 = 'INSERT INTO {bugnote_text}
1304							   ( note )
1305							   VALUES ( ' . db_param() . ' )';
1306				db_query( $t_query2, array( $t_bugnote_text['note'] ) );
1307				$t_bugnote_text_insert_id = db_insert_id( db_get_table( 'bugnote_text' ) );
1308			}
1309
1310			db_param_push();
1311			$t_query2 = 'INSERT INTO {bugnote}
1312						   ( bug_id, reporter_id, bugnote_text_id, view_state, date_submitted, last_modified )
1313						   VALUES ( ' . db_param() . ',
1314						   			' . db_param() . ',
1315						   			' . db_param() . ',
1316						   			' . db_param() . ',
1317						   			' . db_param() . ',
1318						   			' . db_param() . ')';
1319			db_query( $t_query2, array( $t_new_bug_id, $t_bug_note['reporter_id'], $t_bugnote_text_insert_id, $t_bug_note['view_state'], $t_bug_note['date_submitted'], $t_bug_note['last_modified'] ) );
1320		}
1321	}
1322
1323	# Copy attachments
1324	if( $p_copy_attachments ) {
1325	    file_copy_attachments( $t_bug_id, $t_new_bug_id );
1326	}
1327
1328	# Copy users monitoring bug
1329	if( $p_copy_monitoring_users ) {
1330		bug_monitor_copy( $t_bug_id, $t_new_bug_id );
1331	}
1332
1333	# COPY HISTORY
1334	history_delete( $t_new_bug_id );	# should history only be deleted inside the if statement below?
1335	if( $p_copy_history ) {
1336		# @todo problem with this code: the generated history trail is incorrect because the note IDs are those of the original bug, not the copied ones
1337		# @todo actually, does it even make sense to copy the history ?
1338		db_param_push();
1339		$t_query = 'SELECT * FROM {bug_history} WHERE bug_id = ' . db_param();
1340		$t_result = db_query( $t_query, array( $t_bug_id ) );
1341
1342		while( $t_bug_history = db_fetch_array( $t_result ) ) {
1343			db_param_push();
1344			$t_query = 'INSERT INTO {bug_history}
1345						  ( user_id, bug_id, date_modified, field_name, old_value, new_value, type )
1346						  VALUES ( ' . db_param() . ',' . db_param() . ',' . db_param() . ',
1347						  		   ' . db_param() . ',' . db_param() . ',' . db_param() . ',
1348						  		   ' . db_param() . ' );';
1349			db_query( $t_query, array( $t_bug_history['user_id'], $t_new_bug_id, $t_bug_history['date_modified'], $t_bug_history['field_name'], $t_bug_history['old_value'], $t_bug_history['new_value'], $t_bug_history['type'] ) );
1350		}
1351	} else {
1352		# Create a "New Issue" history entry
1353		history_log_event_special( $t_new_bug_id, NEW_BUG );
1354	}
1355
1356	# Create history entries to reflect the copy operation
1357	history_log_event_special( $t_new_bug_id, BUG_CREATED_FROM, '', $t_bug_id );
1358	history_log_event_special( $t_bug_id, BUG_CLONED_TO, '', $t_new_bug_id );
1359
1360	return $t_new_bug_id;
1361}
1362
1363/**
1364 * Moves an issue from a project to another.
1365 *
1366 * @todo Validate with sub-project / category inheritance scenarios.
1367 * @param integer $p_bug_id            The bug to be moved.
1368 * @param integer $p_target_project_id The target project to move the bug to.
1369 * @return void
1370 * @access public
1371 */
1372function bug_move( $p_bug_id, $p_target_project_id ) {
1373	# Attempt to move disk based attachments to new project file directory.
1374	file_move_bug_attachments( $p_bug_id, $p_target_project_id );
1375
1376	# Move the issue to the new project.
1377	bug_set_field( $p_bug_id, 'project_id', $p_target_project_id );
1378
1379	# Update the category if needed
1380	$t_category_id = bug_get_field( $p_bug_id, 'category_id' );
1381
1382	# Bug has no category
1383	if( $t_category_id == 0 ) {
1384		# Category is required in target project, set it to default
1385		if( ON != config_get( 'allow_no_category', null, null, $p_target_project_id ) ) {
1386			bug_set_field( $p_bug_id, 'category_id', config_get( 'default_category_for_moves', null, null, $p_target_project_id ) );
1387		}
1388	} else {
1389		# Check if the category is global, and if not attempt mapping it to the new project
1390		$t_category_project_id = category_get_field( $t_category_id, 'project_id' );
1391
1392		if( $t_category_project_id != ALL_PROJECTS
1393		  && !in_array( $t_category_project_id, project_hierarchy_inheritance( $p_target_project_id ) )
1394		) {
1395			# Map by name
1396			$t_category_name = category_get_field( $t_category_id, 'name' );
1397			$t_target_project_category_id = category_get_id_by_name( $t_category_name, $p_target_project_id, false );
1398			if( $t_target_project_category_id === false ) {
1399				# Use target project's default category for moves, since there is no match by name.
1400				$t_target_project_category_id = config_get( 'default_category_for_moves', null, null, $p_target_project_id );
1401			}
1402			bug_set_field( $p_bug_id, 'category_id', $t_target_project_category_id );
1403		}
1404	}
1405}
1406
1407/**
1408 * allows bug deletion :
1409 * delete the bug, bugtext, bugnote, and bugtexts selected
1410 * @param integer $p_bug_id Integer representing bug identifier.
1411 * @return void
1412 * @access public
1413 */
1414function bug_delete( $p_bug_id ) {
1415	$c_bug_id = (int)$p_bug_id;
1416
1417	# call pre-deletion custom function
1418	helper_call_custom_function( 'issue_delete_validate', array( $p_bug_id ) );
1419
1420	event_signal( 'EVENT_BUG_DELETED', array( $c_bug_id ) );
1421
1422	# log deletion of bug
1423	history_log_event_special( $p_bug_id, BUG_DELETED, bug_format_id( $p_bug_id ) );
1424
1425	email_bug_deleted( $p_bug_id );
1426	email_relationship_bug_deleted( $p_bug_id );
1427
1428	# call post-deletion custom function.  We call this here to allow the custom function to access the details of the bug before
1429	# they are deleted from the database given it's id.  The other option would be to move this to the end of the function and
1430	# provide it with bug data rather than an id, but this will break backward compatibility.
1431	helper_call_custom_function( 'issue_delete_notify', array( $p_bug_id ) );
1432
1433	# Unmonitor bug for all users
1434	bug_unmonitor( $p_bug_id, null );
1435
1436	# Delete custom fields
1437	custom_field_delete_all_values( $p_bug_id );
1438
1439	# Delete bugnotes
1440	bugnote_delete_all( $p_bug_id );
1441
1442	# Delete all sponsorships
1443	sponsorship_delete_all( $p_bug_id );
1444
1445	# Delete all relationships
1446	relationship_delete_all( $p_bug_id );
1447
1448	# Delete files
1449	file_delete_attachments( $p_bug_id );
1450
1451	# Detach tags
1452	tag_bug_detach_all( $p_bug_id, false );
1453
1454	# Delete the bug history
1455	history_delete( $p_bug_id );
1456
1457	# Delete bug info revisions
1458	bug_revision_delete( $p_bug_id );
1459
1460	# Delete the bugnote text
1461	$t_bug_text_id = bug_get_field( $p_bug_id, 'bug_text_id' );
1462
1463	db_param_push();
1464	$t_query = 'DELETE FROM {bug_text} WHERE id=' . db_param();
1465	db_query( $t_query, array( $t_bug_text_id ) );
1466
1467	# Delete the bug entry
1468	db_param_push();
1469	$t_query = 'DELETE FROM {bug} WHERE id=' . db_param();
1470	db_query( $t_query, array( $c_bug_id ) );
1471
1472	bug_clear_cache_all( $p_bug_id );
1473}
1474
1475/**
1476 * Delete all bugs associated with a project
1477 * @param integer $p_project_id Integer representing a project identifier.
1478 * @access public
1479 * @uses database_api.php
1480 * @return void
1481 */
1482function bug_delete_all( $p_project_id ) {
1483	$c_project_id = (int)$p_project_id;
1484
1485	db_param_push();
1486	$t_query = 'SELECT id FROM {bug} WHERE project_id=' . db_param();
1487	$t_result = db_query( $t_query, array( $c_project_id ) );
1488
1489	while( $t_row = db_fetch_array( $t_result ) ) {
1490		bug_delete( $t_row['id'] );
1491	}
1492
1493	# @todo should we check the return value of each bug_delete() and
1494	#  return false if any of them return false? Presumable bug_delete()
1495	#  will eventually trigger an error on failure so it won't matter...
1496}
1497
1498/**
1499 * Returns the extended record of the specified bug, this includes
1500 * the bug text fields
1501 * @todo include reporter name and handler name, the problem is that
1502 *      handler can be 0, in this case no corresponding name will be
1503 *      found.  Use equivalent of (+) in Oracle.
1504 * @param integer $p_bug_id Integer representing bug identifier.
1505 * @return array
1506 * @access public
1507 */
1508function bug_get_extended_row( $p_bug_id ) {
1509	$t_base = bug_cache_row( $p_bug_id );
1510	$t_text = bug_text_cache_row( $p_bug_id );
1511
1512	# merge $t_text first so that the 'id' key has the bug id not the bug text id
1513	return array_merge( $t_text, $t_base );
1514}
1515
1516/**
1517 * Returns the record of the specified bug
1518 * @param integer $p_bug_id Integer representing bug identifier.
1519 * @return array
1520 * @access public
1521 */
1522function bug_get_row( $p_bug_id ) {
1523	return bug_cache_row( $p_bug_id );
1524}
1525
1526/**
1527 * Returns an object representing the specified bug
1528 * @param integer $p_bug_id       Integer representing bug identifier.
1529 * @param boolean $p_get_extended Whether to include extended information (including bug_text).
1530 * @return BugData BugData Object
1531 * @access public
1532 */
1533function bug_get( $p_bug_id, $p_get_extended = false ) {
1534	if( $p_get_extended ) {
1535		$t_row = bug_get_extended_row( $p_bug_id );
1536	} else {
1537		$t_row = bug_get_row( $p_bug_id );
1538	}
1539
1540	$t_bug_data = new BugData;
1541	$t_bug_data->loadrow( $t_row );
1542	return $t_bug_data;
1543}
1544
1545/**
1546 * Convert row [from database] to bug object
1547 * @param array $p_row Bug database row.
1548 * @return BugData
1549 */
1550function bug_row_to_object( array $p_row ) {
1551	$t_bug_data = new BugData;
1552	$t_bug_data->loadrow( $p_row );
1553	return $t_bug_data;
1554}
1555
1556/**
1557 * return the specified field of the given bug
1558 *  if the field does not exist, display a warning and return ''
1559 * @param integer $p_bug_id     Integer representing bug identifier.
1560 * @param string  $p_field_name Field name to retrieve.
1561 * @return string
1562 * @access public
1563 */
1564function bug_get_field( $p_bug_id, $p_field_name ) {
1565	$t_row = bug_get_row( $p_bug_id );
1566
1567	if( isset( $t_row[$p_field_name] ) ) {
1568		return $t_row[$p_field_name];
1569	} else {
1570		error_parameters( $p_field_name );
1571		trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
1572		return '';
1573	}
1574}
1575
1576/**
1577 * return the specified text field of the given bug
1578 *  if the field does not exist, display a warning and return ''
1579 * @param integer $p_bug_id     Integer representing bug identifier.
1580 * @param string  $p_field_name Field name to retrieve.
1581 * @return string
1582 * @access public
1583 */
1584function bug_get_text_field( $p_bug_id, $p_field_name ) {
1585	$t_row = bug_text_cache_row( $p_bug_id );
1586
1587	if( isset( $t_row[$p_field_name] ) ) {
1588		return $t_row[$p_field_name];
1589	} else {
1590		error_parameters( $p_field_name );
1591		trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
1592		return '';
1593	}
1594}
1595
1596/**
1597 * return the bug summary
1598 *  this is a wrapper for the custom function
1599 * @param integer $p_bug_id  Integer representing bug identifier.
1600 * @param integer $p_context Representing SUMMARY_CAPTION, SUMMARY_FIELD.
1601 * @return string
1602 * @access public
1603 * @uses helper_api.php
1604 */
1605function bug_format_summary( $p_bug_id, $p_context ) {
1606	return helper_call_custom_function( 'format_issue_summary', array( $p_bug_id, $p_context ) );
1607}
1608
1609/**
1610 * return the timestamp for the most recent time at which a bugnote
1611 *  associated with the bug was modified
1612 * @param integer $p_bug_id Integer representing bug identifier.
1613 * @return boolean|integer false or timestamp in integer format representing newest bugnote timestamp
1614 * @access public
1615 * @uses database_api.php
1616 */
1617function bug_get_newest_bugnote_timestamp( $p_bug_id ) {
1618	$c_bug_id = (int)$p_bug_id;
1619
1620	db_param_push();
1621	$t_query = 'SELECT last_modified FROM {bugnote} WHERE bug_id=' . db_param() . ' ORDER BY last_modified DESC';
1622	$t_result = db_query( $t_query, array( $c_bug_id ), 1 );
1623	$t_row = db_result( $t_result );
1624
1625	if( false === $t_row ) {
1626		return false;
1627	} else {
1628		return $t_row;
1629	}
1630}
1631
1632/**
1633 * For a list of bug ids, returns an array of bugnote stats.
1634 * If a bug has no visible bugnotes, returns "false" as the stats item for that bug id.
1635 * @param array $p_bugs_id         Array of Integer representing bug identifiers.
1636 * @param integer|null $p_user_id  User for checking access levels. null defaults to current user
1637 * @return array                   Array of bugnote stats
1638 * @access public
1639 * @uses database_api.php
1640 */
1641function bug_get_bugnote_stats_array( array $p_bugs_id, $p_user_id = null ) {
1642	$t_id_array = array();
1643	foreach( $p_bugs_id as $t_id ) {
1644		$t_id_array[$t_id] = (int)$t_id;
1645	}
1646	if( empty( $t_id_array ) ) {
1647		return array();
1648	}
1649
1650	if ( null === $p_user_id ) {
1651		$t_user_id = auth_get_current_user_id();
1652	}
1653	else {
1654		$t_user_id = $p_user_id;
1655	}
1656
1657	db_param_push();
1658	$t_params = array();
1659	$t_in_clause_elems = array();
1660	foreach( $t_id_array as $t_id ) {
1661		$t_in_clause_elems[] = db_param();
1662		$t_params[] = $t_id;
1663	}
1664	$t_query = 'SELECT n.id, n.bug_id, n.reporter_id, n.view_state, n.last_modified, n.date_submitted, b.project_id'
1665		. ' FROM {bugnote} n JOIN {bug} b ON (n.bug_id = b.id)'
1666		. ' WHERE n.bug_id IN (' . implode( ', ', $t_in_clause_elems ) . ')'
1667		. ' ORDER BY b.project_id, n.bug_id, n.last_modified';
1668	# perform query
1669	$t_result = db_query( $t_query, $t_params );
1670	$t_counter = 0;
1671	$t_stats = array();
1672	# We need to check for each bugnote if it has permissions to view in respective project.
1673	# bugnotes are grouped by project_id and bug_id to save calls to config_get
1674	$t_current_project_id = null;
1675	$t_current_bug_id = null;
1676	while( $t_query_row = db_fetch_array( $t_result ) ) {
1677		$c_bug_id = (int)$t_query_row['bug_id'];
1678		if( 0 == $t_counter || $t_current_project_id !== $t_query_row['project_id'] ) {
1679			# evaluating a new project from the rowset
1680			$t_current_project_id = $t_query_row['project_id'];
1681			$t_user_access_level = access_get_project_level( $t_query_row['project_id'], $t_user_id );
1682			$t_private_bugnote_visible = access_compare_level(
1683					$t_user_access_level,
1684					config_get( 'private_bugnote_threshold', null, $t_user_id, $t_query_row['project_id'] )
1685					);
1686		}
1687		if( 0 == $t_counter || $t_current_bug_id !== $c_bug_id ) {
1688			# evaluating a new bug from the rowset
1689			$t_current_bug_id = $c_bug_id;
1690			$t_note_count = 0;
1691			$t_last_submit_date= 0;
1692		}
1693		$t_note_visible = $t_private_bugnote_visible
1694				|| $t_query_row['reporter_id'] == $t_user_id
1695				|| ( VS_PUBLIC == $t_query_row['view_state'] );
1696		if( $t_note_visible ) {
1697			# only count the bugnote if user has access
1698			$t_stats[$c_bug_id]['bug_id'] = $c_bug_id;
1699			$t_stats[$c_bug_id]['last_modified'] = $t_query_row['last_modified'];
1700			$t_stats[$c_bug_id]['count'] = ++$t_note_count;
1701			$t_stats[$c_bug_id]['last_modified_bugnote'] = $t_query_row['id'];
1702			if( $t_query_row['date_submitted'] > $t_last_submit_date ) {
1703				$t_last_submit_date = $t_query_row['date_submitted'];
1704				$t_stats[$c_bug_id]['last_submitted_bugnote'] = $t_query_row['id'];
1705			}
1706			if( isset( $t_id_array[$c_bug_id] ) ) {
1707				unset( $t_id_array[$c_bug_id] );
1708			}
1709		}
1710		$t_counter++;
1711	}
1712
1713	# The remaining bug ids, are those without visible notes. Save false as cached value
1714	foreach( $t_id_array as $t_id ) {
1715		$t_stats[$t_id] = false;
1716	}
1717	return $t_stats;
1718}
1719
1720/**
1721 * return the timestamp for the most recent time at which a bugnote
1722 * associated with the bug was modified and the total bugnote
1723 * count in one db query
1724 * @param integer $p_bug_id Integer representing bug identifier.
1725 * @return array|false Bugnote stats, false if no bugnotes
1726 * @access public
1727 * @uses database_api.php
1728 */
1729function bug_get_bugnote_stats( $p_bug_id ) {
1730	global $g_cache_bug;
1731	$c_bug_id = (int)$p_bug_id;
1732
1733	if( array_key_exists( '_stats', $g_cache_bug[$c_bug_id] ) ) {
1734		return $g_cache_bug[$c_bug_id]['_stats'];
1735	}
1736	else {
1737		$t_stats = bug_get_bugnote_stats_array( array( $p_bug_id ) );
1738		return $t_stats[$p_bug_id];
1739	}
1740}
1741
1742/**
1743 * Get array of attachments associated with the specified bug id.  The array will be
1744 * sorted in terms of date added (ASC).  The array will include the following fields:
1745 * id, title, diskfile, filename, filesize, file_type, date_added, user_id.
1746 * @param integer $p_bug_id Integer representing bug identifier.
1747 * @return array array of results or empty array
1748 * @access public
1749 * @uses database_api.php
1750 * @uses file_api.php
1751 */
1752function bug_get_attachments( $p_bug_id ) {
1753	db_param_push();
1754	$t_query = 'SELECT id, title, diskfile, filename, filesize, file_type, date_added, user_id, bugnote_id
1755		                FROM {bug_file}
1756		                WHERE bug_id=' . db_param() . '
1757		                ORDER BY date_added';
1758	$t_db_result = db_query( $t_query, array( $p_bug_id ) );
1759
1760	$t_result = array();
1761
1762	while( $t_row = db_fetch_array( $t_db_result ) ) {
1763		$t_result[] = $t_row;
1764	}
1765
1766	return $t_result;
1767}
1768
1769/**
1770 * Set the value of a bug field
1771 * @param integer                $p_bug_id     Integer representing bug identifier.
1772 * @param string                 $p_field_name Pre-defined field name.
1773 * @param boolean|integer|string $p_value      Value to set.
1774 * @return boolean (always true)
1775 * @access public
1776 * @uses database_api.php
1777 * @uses history_api.php
1778 */
1779function bug_set_field( $p_bug_id, $p_field_name, $p_value ) {
1780	$c_bug_id = (int)$p_bug_id;
1781	$c_value = null;
1782
1783	switch( $p_field_name ) {
1784		# boolean
1785		case 'sticky':
1786			$c_value = $p_value;
1787			break;
1788
1789		# integer
1790		case 'project_id':
1791		case 'reporter_id':
1792		case 'handler_id':
1793		case 'duplicate_id':
1794		case 'priority':
1795		case 'severity':
1796		case 'reproducibility':
1797		case 'status':
1798		case 'resolution':
1799		case 'projection':
1800		case 'category_id':
1801		case 'eta':
1802		case 'view_state':
1803		case 'profile_id':
1804		case 'sponsorship_total':
1805			$c_value = (int)$p_value;
1806			break;
1807
1808		# string
1809		case 'os':
1810		case 'os_build':
1811		case 'platform':
1812		case 'version':
1813		case 'fixed_in_version':
1814		case 'target_version':
1815		case 'build':
1816		case 'summary':
1817			$c_value = $p_value;
1818			break;
1819
1820		# dates
1821		case 'last_updated':
1822		case 'date_submitted':
1823		case 'due_date':
1824			if( !is_numeric( $p_value ) ) {
1825				trigger_error( ERROR_GENERIC, ERROR );
1826			}
1827			$c_value = $p_value;
1828			break;
1829
1830		default:
1831			trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
1832			break;
1833	}
1834
1835	$t_current_value = bug_get_field( $p_bug_id, $p_field_name );
1836
1837	# return if status is already set
1838	if( $c_value == $t_current_value ) {
1839		return true;
1840	}
1841
1842	# Update fields
1843	db_param_push();
1844	$t_query = 'UPDATE {bug} SET ' . $p_field_name . '=' . db_param() . ' WHERE id=' . db_param();
1845	db_query( $t_query, array( $c_value, $c_bug_id ) );
1846
1847	# updated the last_updated date
1848	if( $p_field_name != 'last_updated' ) {
1849		bug_update_date( $p_bug_id );
1850	}
1851
1852	# log changes except for duplicate_id which is obsolete and should be removed in
1853	# MantisBT 1.3.
1854	switch( $p_field_name ) {
1855		case 'duplicate_id':
1856			break;
1857
1858		case 'category_id':
1859			history_log_event_direct( $p_bug_id, 'category', category_full_name( $t_current_value, false ), category_full_name( $c_value, false ) );
1860			break;
1861
1862		default:
1863			history_log_event_direct( $p_bug_id, $p_field_name, $t_current_value, $c_value );
1864	}
1865
1866	bug_clear_cache( $p_bug_id );
1867
1868	return true;
1869}
1870
1871/**
1872 * assign the bug to the given user
1873 * @param integer $p_bug_id          A bug identifier.
1874 * @param integer $p_user_id         A user identifier.
1875 * @param string  $p_bugnote_text    The bugnote text.
1876 * @param boolean $p_bugnote_private Indicate whether bugnote is private.
1877 * @return boolean
1878 * @access public
1879 * @uses database_api.php
1880 */
1881function bug_assign( $p_bug_id, $p_user_id, $p_bugnote_text = '', $p_bugnote_private = false ) {
1882	if( $p_user_id != NO_USER ) {
1883		$t_bug_sponsored = config_get( 'enable_sponsorship' )
1884			&& sponsorship_get_amount( sponsorship_get_all_ids( $p_bug_id ) ) > 0;
1885		# The new handler is checked at project level
1886		$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
1887		if( !access_has_project_level( config_get( 'handle_bug_threshold' ), $t_project_id, $p_user_id ) ) {
1888			trigger_error( ERROR_HANDLER_ACCESS_TOO_LOW, ERROR );
1889		}
1890		if( $t_bug_sponsored && !access_has_project_level( config_get( 'handle_sponsored_bugs_threshold' ), $t_project_id, $p_user_id ) ) {
1891			trigger_error( ERROR_SPONSORSHIP_HANDLER_ACCESS_LEVEL_TOO_LOW, ERROR );
1892		}
1893	}
1894
1895	# extract current information into history variables
1896	$h_status = bug_get_field( $p_bug_id, 'status' );
1897	$h_handler_id = bug_get_field( $p_bug_id, 'handler_id' );
1898
1899	$t_ass_val = bug_get_status_for_assign( $h_handler_id, $p_user_id, $h_status );
1900
1901	if( ( $t_ass_val != $h_status ) || ( $p_user_id != $h_handler_id ) ) {
1902
1903		# get user id
1904		db_param_push();
1905		$t_query = 'UPDATE {bug}
1906					  SET handler_id=' . db_param() . ', status=' . db_param() . '
1907					  WHERE id=' . db_param();
1908		db_query( $t_query, array( $p_user_id, $t_ass_val, $p_bug_id ) );
1909
1910		# log changes
1911		history_log_event_direct( $p_bug_id, 'status', $h_status, $t_ass_val );
1912		history_log_event_direct( $p_bug_id, 'handler_id', $h_handler_id, $p_user_id );
1913
1914		# Add bugnote if supplied ignore false return
1915		if( !is_blank( $p_bugnote_text ) ) {
1916			$t_bugnote_id = bugnote_add( $p_bug_id, $p_bugnote_text, 0, $p_bugnote_private, 0, '', null, false );
1917			bugnote_process_mentions( $p_bug_id, $t_bugnote_id, $p_bugnote_text );
1918		}
1919
1920		# updated the last_updated date
1921		bug_update_date( $p_bug_id );
1922
1923		bug_clear_cache( $p_bug_id );
1924
1925		# Send email for change of handler
1926		email_owner_changed( $p_bug_id, $h_handler_id, $p_user_id );
1927	}
1928
1929	return true;
1930}
1931
1932/**
1933 * close the given bug
1934 * @param integer $p_bug_id          A bug identifier.
1935 * @param string  $p_bugnote_text    The bugnote text.
1936 * @param boolean $p_bugnote_private Whether the bugnote is private.
1937 * @param string  $p_time_tracking   Time tracking value.
1938 * @return boolean (always true)
1939 * @access public
1940 */
1941function bug_close( $p_bug_id, $p_bugnote_text = '', $p_bugnote_private = false, $p_time_tracking = '0:00' ) {
1942	$p_bugnote_text = trim( $p_bugnote_text );
1943
1944	# Add bugnote if supplied ignore a false return
1945	# Moved bugnote_add before bug_set_field calls in case time_tracking_no_note is off.
1946	# Error condition stopped execution but status had already been changed
1947	if( !is_blank( $p_bugnote_text ) || $p_time_tracking != '0:00' ) {
1948		$t_bugnote_id = bugnote_add( $p_bug_id, $p_bugnote_text, $p_time_tracking, $p_bugnote_private, 0, '', null, false );
1949		bugnote_process_mentions( $p_bug_id, $t_bugnote_id, $p_bugnote_text );
1950	}
1951
1952	bug_set_field( $p_bug_id, 'status', config_get( 'bug_closed_status_threshold' ) );
1953
1954	email_close( $p_bug_id );
1955	email_relationship_child_closed( $p_bug_id );
1956
1957	return true;
1958}
1959
1960/**
1961 * resolve the given bug
1962 * @param integer $p_bug_id           A bug identifier.
1963 * @param integer $p_resolution       Resolution status.
1964 * @param string  $p_fixed_in_version Fixed in version.
1965 * @param string  $p_bugnote_text     The bugnote text.
1966 * @param integer $p_duplicate_id     A duplicate identifier.
1967 * @param integer $p_handler_id       A handler identifier.
1968 * @param boolean $p_bugnote_private  Whether this is a private bugnote.
1969 * @param string  $p_time_tracking    Time tracking value.
1970 * @access public
1971 * @return boolean
1972 */
1973function bug_resolve( $p_bug_id, $p_resolution, $p_fixed_in_version = '', $p_bugnote_text = '', $p_duplicate_id = null, $p_handler_id = null, $p_bugnote_private = false, $p_time_tracking = '0:00' ) {
1974	$c_resolution = (int)$p_resolution;
1975	$p_bugnote_text = trim( $p_bugnote_text );
1976
1977	# Add bugnote if supplied
1978	# Moved bugnote_add before bug_set_field calls in case time_tracking_no_note is off.
1979	# Error condition stopped execution but status had already been changed
1980	if( !is_blank( $p_bugnote_text ) || $p_time_tracking != '0:00' ) {
1981		$t_bugnote_id = bugnote_add( $p_bug_id, $p_bugnote_text, $p_time_tracking, $p_bugnote_private, 0, '', null, false );
1982		bugnote_process_mentions( $p_bug_id, $t_bugnote_id, $p_bugnote_text );
1983	}
1984
1985	$t_duplicate = !is_blank( $p_duplicate_id ) && ( $p_duplicate_id != 0 );
1986	if( $t_duplicate ) {
1987		if( $p_bug_id == $p_duplicate_id ) {
1988			trigger_error( ERROR_BUG_DUPLICATE_SELF, ERROR );
1989
1990			# never returns
1991		}
1992
1993		# the related bug exists...
1994		bug_ensure_exists( $p_duplicate_id );
1995
1996		relationship_upsert( $p_bug_id, $p_duplicate_id, BUG_DUPLICATE, /* email_for_source */ false );
1997
1998		# Copy list of users monitoring the duplicate bug to the original bug
1999		$t_old_reporter_id = bug_get_field( $p_bug_id, 'reporter_id' );
2000		$t_old_handler_id = bug_get_field( $p_bug_id, 'handler_id' );
2001		if( user_exists( $t_old_reporter_id ) ) {
2002			bug_monitor( $p_duplicate_id, $t_old_reporter_id );
2003		}
2004		if( user_exists( $t_old_handler_id ) ) {
2005			bug_monitor( $p_duplicate_id, $t_old_handler_id );
2006		}
2007		bug_monitor_copy( $p_bug_id, $p_duplicate_id );
2008
2009		bug_set_field( $p_bug_id, 'duplicate_id', (int)$p_duplicate_id );
2010	}
2011
2012	bug_set_field( $p_bug_id, 'status', config_get( 'bug_resolved_status_threshold' ) );
2013	bug_set_field( $p_bug_id, 'fixed_in_version', $p_fixed_in_version );
2014	bug_set_field( $p_bug_id, 'resolution', $c_resolution );
2015
2016	# only set handler if specified explicitly or if bug was not assigned to a handler
2017	if( null == $p_handler_id ) {
2018		if( bug_get_field( $p_bug_id, 'handler_id' ) == 0 ) {
2019			$p_handler_id = auth_get_current_user_id();
2020			bug_set_field( $p_bug_id, 'handler_id', $p_handler_id );
2021		}
2022	} else {
2023		bug_set_field( $p_bug_id, 'handler_id', $p_handler_id );
2024	}
2025
2026	email_resolved( $p_bug_id );
2027	email_relationship_child_resolved( $p_bug_id );
2028
2029	return true;
2030}
2031
2032/**
2033 * reopen the given bug
2034 * @param integer $p_bug_id          A bug identifier.
2035 * @param string  $p_bugnote_text    The bugnote text.
2036 * @param string  $p_time_tracking   Time tracking value.
2037 * @param boolean $p_bugnote_private Whether this is a private bugnote.
2038 * @return boolean (always true)
2039 * @access public
2040 * @uses database_api.php
2041 * @uses email_api.php
2042 * @uses bugnote_api.php
2043 * @uses config_api.php
2044 */
2045function bug_reopen( $p_bug_id, $p_bugnote_text = '', $p_time_tracking = '0:00', $p_bugnote_private = false ) {
2046	$p_bugnote_text = trim( $p_bugnote_text );
2047
2048	# Add bugnote if supplied
2049	# Moved bugnote_add before bug_set_field calls in case time_tracking_no_note is off.
2050	# Error condition stopped execution but status had already been changed
2051	if( !is_blank( $p_bugnote_text ) || $p_time_tracking != '0:00' ) {
2052		$t_bugnote_id = bugnote_add( $p_bug_id, $p_bugnote_text, $p_time_tracking, $p_bugnote_private, 0, '', null, false );
2053		bugnote_process_mentions( $p_bug_id, $t_bugnote_id, $p_bugnote_text );
2054	}
2055
2056	bug_set_field( $p_bug_id, 'status', config_get( 'bug_reopen_status' ) );
2057	bug_set_field( $p_bug_id, 'resolution', config_get( 'bug_reopen_resolution' ) );
2058
2059	email_bug_reopened( $p_bug_id );
2060
2061	return true;
2062}
2063
2064/**
2065 * updates the last_updated field
2066 * @param integer $p_bug_id Integer representing bug identifier.
2067 * @return boolean (always true)
2068 * @access public
2069 * @uses database_api.php
2070 */
2071function bug_update_date( $p_bug_id ) {
2072	db_param_push();
2073	$t_query = 'UPDATE {bug} SET last_updated=' . db_param() . ' WHERE id=' . db_param();
2074	db_query( $t_query, array( db_now(), $p_bug_id ) );
2075
2076	bug_clear_cache( $p_bug_id );
2077
2078	return true;
2079}
2080
2081/**
2082 * enable monitoring of this bug for the user
2083 * @param integer $p_bug_id  Integer representing bug identifier.
2084 * @param integer $p_user_id Integer representing user identifier.
2085 * @return boolean true if successful, false if unsuccessful
2086 * @access public
2087 * @uses database_api.php
2088 * @uses history_api.php
2089 * @uses user_api.php
2090 */
2091function bug_monitor( $p_bug_id, $p_user_id ) {
2092	$c_bug_id = (int)$p_bug_id;
2093	$c_user_id = (int)$p_user_id;
2094
2095	# Make sure we aren't already monitoring this bug
2096	if( user_is_monitoring_bug( $c_user_id, $c_bug_id ) ) {
2097		return true;
2098	}
2099
2100	# Don't let the anonymous user monitor bugs
2101	if( user_is_anonymous( $c_user_id ) ) {
2102		return false;
2103	}
2104
2105	# Insert monitoring record
2106	db_param_push();
2107	$t_query = 'INSERT INTO {bug_monitor} ( user_id, bug_id ) VALUES (' . db_param() . ',' . db_param() . ')';
2108	db_query( $t_query, array( $c_user_id, $c_bug_id ) );
2109
2110	# log new monitoring action
2111	history_log_event_special( $c_bug_id, BUG_MONITOR, $c_user_id );
2112
2113	# updated the last_updated date
2114	bug_update_date( $p_bug_id );
2115
2116	email_monitor_added( $p_bug_id, $p_user_id );
2117
2118	return true;
2119}
2120
2121/**
2122 * Returns the list of users monitoring the specified bug
2123 *
2124 * @param integer $p_bug_id Integer representing bug identifier.
2125 * @return array
2126 */
2127function bug_get_monitors( $p_bug_id ) {
2128	if( ! access_has_bug_level( config_get( 'show_monitor_list_threshold' ), $p_bug_id ) ) {
2129		return array();
2130	}
2131
2132	# get the bugnote data
2133	db_param_push();
2134	$t_query = 'SELECT user_id, enabled
2135			FROM {bug_monitor} m, {user} u
2136			WHERE m.bug_id=' . db_param() . ' AND m.user_id = u.id
2137			ORDER BY u.realname, u.username';
2138	$t_result = db_query( $t_query, array( $p_bug_id ) );
2139
2140	$t_users = array();
2141	while( $t_row = db_fetch_array( $t_result ) ) {
2142		$t_users[] = $t_row['user_id'];
2143	}
2144
2145	user_cache_array_rows( $t_users );
2146
2147	return $t_users;
2148}
2149
2150/**
2151 * Copy list of users monitoring a bug to the monitor list of a second bug
2152 * @param integer $p_source_bug_id Integer representing the bug identifier of the source bug.
2153 * @param integer $p_dest_bug_id   Integer representing the bug identifier of the destination bug.
2154 * @return void
2155 * @access public
2156 * @uses database_api.php
2157 * @uses history_api.php
2158 * @uses user_api.php
2159 */
2160function bug_monitor_copy( $p_source_bug_id, $p_dest_bug_id ) {
2161	$c_source_bug_id = (int)$p_source_bug_id;
2162	$c_dest_bug_id = (int)$p_dest_bug_id;
2163
2164	db_param_push();
2165	$t_query = 'SELECT user_id FROM {bug_monitor} WHERE bug_id = ' . db_param();
2166	$t_result = db_query( $t_query, array( $c_source_bug_id ) );
2167
2168	while( $t_bug_monitor = db_fetch_array( $t_result ) ) {
2169		if( user_exists( $t_bug_monitor['user_id'] ) &&
2170			!user_is_monitoring_bug( $t_bug_monitor['user_id'], $c_dest_bug_id ) ) {
2171			db_param_push();
2172			$t_query = 'INSERT INTO {bug_monitor} ( user_id, bug_id )
2173				VALUES ( ' . db_param() . ', ' . db_param() . ' )';
2174			db_query( $t_query, array( $t_bug_monitor['user_id'], $c_dest_bug_id ) );
2175			history_log_event_special( $c_dest_bug_id, BUG_MONITOR, $t_bug_monitor['user_id'] );
2176		}
2177	}
2178}
2179
2180/**
2181 * disable monitoring of this bug for the user
2182 * if $p_user_id = null, then bug is unmonitored for all users.
2183 * @param integer $p_bug_id  Integer representing bug identifier.
2184 * @param integer $p_user_id Integer representing user identifier.
2185 * @return boolean (always true)
2186 * @access public
2187 * @uses database_api.php
2188 * @uses history_api.php
2189 */
2190function bug_unmonitor( $p_bug_id, $p_user_id ) {
2191	# Delete monitoring record
2192	db_param_push();
2193	$t_query = 'DELETE FROM {bug_monitor} WHERE bug_id = ' . db_param();
2194	$t_db_query_params[] = $p_bug_id;
2195
2196	if( $p_user_id !== null ) {
2197		$t_query .= ' AND user_id = ' . db_param();
2198		$t_db_query_params[] = $p_user_id;
2199	}
2200
2201	db_query( $t_query, $t_db_query_params );
2202
2203	# log new un-monitor action
2204	history_log_event_special( $p_bug_id, BUG_UNMONITOR, (int)$p_user_id );
2205
2206	# updated the last_updated date
2207	bug_update_date( $p_bug_id );
2208
2209	return true;
2210}
2211
2212/**
2213 * Pads the bug id with the appropriate number of zeros.
2214 * @param integer $p_bug_id A bug identifier.
2215 * @return string
2216 * @access public
2217 * @uses config_api.php
2218 */
2219function bug_format_id( $p_bug_id ) {
2220	$t_padding = config_get( 'display_bug_padding' );
2221	$t_string = sprintf( '%0' . (int)$t_padding . 'd', $p_bug_id );
2222
2223	return event_signal( 'EVENT_DISPLAY_BUG_ID', $t_string, array( $p_bug_id ) );
2224}
2225
2226/**
2227 * Returns the resulting status for a bug after an assignment action is performed.
2228 * If the option "auto_set_status_to_assigned" is enabled, the resulting status
2229 * is calculated based on current handler and status , and requested modifications.
2230 * @param integer $p_current_handler	Current handler user id
2231 * @param integer $p_new_handler		New handler user id
2232 * @param integer $p_current_status		Current bug status
2233 * @param integer $p_new_status			New bug status (as being part of a status change combined action)
2234 * @return integer		Calculated status after assignment
2235 */
2236function bug_get_status_for_assign( $p_current_handler, $p_new_handler, $p_current_status, $p_new_status = null ) {
2237	if( null === $p_new_status ) {
2238		$p_new_status = $p_current_status;
2239	}
2240	if( config_get( 'auto_set_status_to_assigned' ) ) {
2241		$t_assigned_status = config_get( 'bug_assigned_status' );
2242
2243		if(		$p_current_handler == NO_USER &&
2244				$p_new_handler != NO_USER &&
2245				$p_new_status == $p_current_status &&
2246				$p_new_status < $t_assigned_status &&
2247				bug_check_workflow( $p_current_status, $t_assigned_status ) ) {
2248
2249			return $t_assigned_status;
2250		}
2251	}
2252	return $p_new_status;
2253}
2254
2255/**
2256 * Clear a bug from all the related caches or all bugs if no bug id specified.
2257 * @param integer $p_bug_id A bug identifier to clear (optional).
2258 * @return boolean
2259 * @access public
2260 */
2261function bug_clear_cache_all( $p_bug_id = null ) {
2262	bug_clear_cache( $p_bug_id );
2263	bug_text_clear_cache( $p_bug_id );
2264	file_bug_attachment_count_clear_cache( $p_bug_id );
2265	bugnote_clear_bug_cache( $p_bug_id );
2266	tag_clear_cache_bug_tags( $p_bug_id );
2267	custom_field_clear_cache_values( $p_bug_id );
2268
2269	$t_plugin_objects = columns_get_plugin_columns();
2270	foreach( $t_plugin_objects as $t_plugin_column ) {
2271		$t_plugin_column->clear_cache();
2272	}
2273	return true;
2274}
2275
2276/**
2277 * Populate the caches related to the selected columns
2278 * @param array $p_bugs	Array of BugData objects
2279 * @param array $p_selected_columns	Array of columns to show
2280 */
2281function bug_cache_columns_data( array $p_bugs, array $p_selected_columns ) {
2282	$t_bug_ids = array();
2283	$t_user_ids = array();
2284	$t_project_ids = array();
2285	$t_category_ids = array();
2286	foreach( $p_bugs as $t_bug ) {
2287		$t_bug_ids[] = (int)$t_bug->id;
2288		$t_user_ids[] = (int)$t_bug->handler_id;
2289		$t_user_ids[] = (int)$t_bug->reporter_id;
2290		$t_project_ids[] = (int)$t_bug->project_id;
2291		$t_category_ids[] = (int)$t_bug->category_id;
2292	}
2293	$t_user_ids = array_unique( $t_user_ids );
2294	$t_project_ids = array_unique( $t_project_ids );
2295	$t_category_ids = array_unique( $t_category_ids );
2296
2297	$t_custom_field_ids = array();
2298	$t_users_cached = false;
2299	foreach( $p_selected_columns as $t_column ) {
2300
2301		if( column_is_plugin_column( $t_column ) ) {
2302			$plugin_objects = columns_get_plugin_columns();
2303			$plugin_objects[$t_column]->cache( $p_bugs );
2304			continue;
2305		}
2306
2307		if( column_is_custom_field( $t_column ) ) {
2308			$t_cf_name = column_get_custom_field_name( $t_column );
2309			$t_cf_id = custom_field_get_id_from_name( $t_cf_name );
2310			if( $t_cf_id ) {
2311				$t_custom_field_ids[] = $t_cf_id;
2312				continue;
2313			}
2314		}
2315
2316		switch( $t_column ) {
2317			case 'attachment_count':
2318				file_bug_attachment_count_cache( $t_bug_ids );
2319				break;
2320			case 'handler_id':
2321			case 'reporter_id':
2322			case 'status':
2323				if( !$t_users_cached ) {
2324					user_cache_array_rows( $t_user_ids );
2325					$t_users_cached = true;
2326				}
2327				break;
2328			case 'project_id':
2329				project_cache_array_rows( $t_project_ids );
2330				break;
2331			case 'category_id':
2332				category_cache_array_rows( $t_category_ids );
2333				break;
2334			case 'tags':
2335				tag_cache_bug_tag_rows( $t_bug_ids );
2336				break;
2337		}
2338	}
2339
2340	if( !empty( $t_custom_field_ids ) ) {
2341		custom_field_cache_values( $t_bug_ids, $t_custom_field_ids );
2342	}
2343}
2344