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 * Bugnote API
19 *
20 * @package CoreAPI
21 * @subpackage BugnoteAPI
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 bug_api.php
30 * @uses bug_revision_api.php
31 * @uses config_api.php
32 * @uses constant_inc.php
33 * @uses database_api.php
34 * @uses email_api.php
35 * @uses error_api.php
36 * @uses event_api.php
37 * @uses file_api.php
38 * @uses helper_api.php
39 * @uses history_api.php
40 * @uses lang_api.php
41 * @uses mention_api.php
42 * @uses user_api.php
43 * @uses utility_api.php
44 */
45
46require_api( 'access_api.php' );
47require_api( 'antispam_api.php' );
48require_api( 'authentication_api.php' );
49require_api( 'bug_api.php' );
50require_api( 'bug_revision_api.php' );
51require_api( 'config_api.php' );
52require_api( 'constant_inc.php' );
53require_api( 'database_api.php' );
54require_api( 'email_api.php' );
55require_api( 'error_api.php' );
56require_api( 'event_api.php' );
57require_api( 'file_api.php' );
58require_api( 'helper_api.php' );
59require_api( 'history_api.php' );
60require_api( 'lang_api.php' );
61require_api( 'mention_api.php' );
62require_api( 'user_api.php' );
63require_api( 'utility_api.php' );
64
65use Mantis\Exceptions\ClientException;
66
67# Cache of bugnotes arrays related to a bug, indexed by bug_id.
68# Each item is an array of BugnoteData objects
69$g_cache_bugnotes_by_bug_id = array();
70
71# Cache of BugnoteData objects, indexed by bugnote id
72$g_cache_bugnotes_by_id = array();
73
74/**
75 * Bugnote Data Structure Definition
76 */
77class BugnoteData {
78	/**
79	 * Bugnote ID
80	 */
81	public $id;
82
83	/**
84	 * Bug ID
85	 */
86	public $bug_id;
87
88	/**
89	 * Reporter ID
90	 */
91	public $reporter_id;
92
93	/**
94	 * Note text
95	 */
96	public $note;
97
98	/**
99	 * View State
100	 */
101	public $view_state;
102
103	/**
104	 * Date submitted
105	 */
106	public $date_submitted;
107
108	/**
109	 * Last Modified
110	 */
111	public $last_modified;
112
113	/**
114	 * Bugnote type
115	 */
116	public $note_type;
117
118	/**
119	 * Used for storing list of recipients for a reminder truncated to
120	 * field length.
121	 */
122	public $note_attr;
123
124	/**
125	 * Time tracking information
126	 */
127	public $time_tracking;
128
129	/**
130	 * Bugnote Text id
131	 */
132	public $bugnote_text_id;
133}
134
135/**
136 * Check if a bugnote with the given ID exists
137 * return true if the bugnote exists, false otherwise
138 * @param integer $p_bugnote_id A bugnote identifier.
139 * @return boolean
140 * @access public
141 */
142function bugnote_exists( $p_bugnote_id ) {
143	$c_bugnote_id = (int)$p_bugnote_id;
144
145	global $g_cache_bugnotes_by_id;
146	if( isset( $g_cache_bugnotes_by_id[$c_bugnote_id] ) ) {
147		return true;
148	}
149
150	# Check for invalid id values
151	if( $c_bugnote_id <= 0 || $c_bugnote_id > DB_MAX_INT ) {
152		return false;
153	}
154
155	db_param_push();
156	$t_query = 'SELECT b.*, t.note
157			          	FROM      {bugnote} b
158			          	LEFT JOIN {bugnote_text} t ON b.bugnote_text_id = t.id
159						WHERE b.id = ' . db_param();
160	$t_result = db_query( $t_query, array( $c_bugnote_id ) );
161	$t_row = db_fetch_array( $t_result );
162
163	if( $t_row === false ) {
164		return false;
165	}
166
167	$t_bugnote = bugnote_row_to_object( $t_row );
168	bugnote_cache( $t_bugnote );
169	return true;
170}
171
172/**
173 * Caches the provided bugnote object.
174 *
175 * @param BugnoteData $p_bugnote The bugnote object.
176 * @return void
177 */
178function bugnote_cache( BugnoteData $p_bugnote ) {
179	global $g_cache_bugnotes_by_id;
180
181	$g_cache_bugnotes_by_id[(int)$p_bugnote->id] = $p_bugnote;
182}
183
184/**
185 * Check if a bugnote with the given ID exists
186 * return true if the bugnote exists, raise an error if not
187 * @param integer $p_bugnote_id A bugnote identifier.
188 * @access public
189 * @return void
190 */
191function bugnote_ensure_exists( $p_bugnote_id ) {
192	if( !bugnote_exists( $p_bugnote_id ) ) {
193		throw new ClientException(
194			"Issue note #$p_bugnote_id not found",
195			ERROR_BUGNOTE_NOT_FOUND,
196			array( $p_bugnote_id ) );
197	}
198}
199
200/**
201 * Check if the given user is the reporter of the bugnote
202 * return true if the user is the reporter, false otherwise
203 * @param integer $p_bugnote_id A bugnote identifier.
204 * @param integer $p_user_id    A user identifier.
205 * @return boolean
206 * @access public
207 */
208function bugnote_is_user_reporter( $p_bugnote_id, $p_user_id ) {
209	if( bugnote_get_field( $p_bugnote_id, 'reporter_id' ) == $p_user_id ) {
210		return true;
211	} else {
212		return false;
213	}
214}
215
216/**
217 * Add a bugnote to a bug
218 * return the ID of the new bugnote
219 * @param integer $p_bug_id          A bug identifier.
220 * @param string  $p_bugnote_text    The bugnote text to add.
221 * @param string  $p_time_tracking   Time tracking value - hh:mm string.
222 * @param boolean $p_private         Whether bugnote is private.
223 * @param integer $p_type            The bugnote type.
224 * @param string  $p_attr            Bugnote Attribute.
225 * @param integer $p_user_id         A user identifier.
226 * @param boolean $p_send_email      Whether to generate email.
227 * @param integer $p_date_submitted  Date submitted (defaults to now()).
228 * @param integer $p_last_modified   Last modification date (defaults to now()).
229 * @param boolean $p_skip_bug_update Skip bug last modification update (useful when importing bugs/bugnotes).
230 * @param boolean $p_log_history     Log changes to bugnote history (defaults to true).
231 * @param boolean $p_trigger_event   Trigger extensibility event.
232 * @return boolean|integer false or indicating bugnote id added
233 * @access public
234 */
235function bugnote_add( $p_bug_id, $p_bugnote_text, $p_time_tracking = '0:00', $p_private = false, $p_type = BUGNOTE, $p_attr = '', $p_user_id = null, $p_send_email = true, $p_date_submitted = 0, $p_last_modified = 0, $p_skip_bug_update = false, $p_log_history = true, $p_trigger_event = true ) {
236	$c_bug_id = (int)$p_bug_id;
237	$c_time_tracking = helper_duration_to_minutes( $p_time_tracking );
238	$c_type = (int)$p_type;
239	$c_date_submitted = $p_date_submitted <= 0 ? db_now() : (int)$p_date_submitted;
240	$c_last_modified = $p_last_modified <= 0 ? db_now() : (int)$p_last_modified;
241
242	antispam_check();
243
244	if( REMINDER !== $p_type ) {
245		# Check if this is a time-tracking note
246		$t_time_tracking_enabled = config_get( 'time_tracking_enabled' );
247		if( ON == $t_time_tracking_enabled && $c_time_tracking > 0 ) {
248			$t_time_tracking_without_note = config_get( 'time_tracking_without_note' );
249			if( is_blank( $p_bugnote_text ) && OFF == $t_time_tracking_without_note ) {
250				throw new ClientException(
251					'Time tracking not allowed with empty note',
252					ERROR_EMPTY_FIELD,
253					array( lang_get( 'bugnote' ) ) );
254			}
255
256			$c_type = TIME_TRACKING;
257		}
258	}
259
260	# Event integration
261	$t_bugnote_text = event_signal( 'EVENT_BUGNOTE_DATA', $p_bugnote_text, $c_bug_id );
262
263	# MySQL 4-bytes UTF-8 chars workaround #21101
264	$t_bugnote_text = db_mysql_fix_utf8( $t_bugnote_text );
265
266	# insert bugnote text
267	db_param_push();
268	$t_query = 'INSERT INTO {bugnote_text} ( note ) VALUES ( ' . db_param() . ' )';
269	db_query( $t_query, array( $t_bugnote_text ) );
270
271	# retrieve bugnote text id number
272	$t_bugnote_text_id = db_insert_id( db_get_table( 'bugnote_text' ) );
273
274	# get user information
275	if( $p_user_id === null ) {
276		$p_user_id = auth_get_current_user_id();
277	}
278
279	# Check for private bugnotes.
280	if( $p_private && access_has_bug_level( config_get( 'set_view_status_threshold' ), $p_bug_id, $p_user_id ) ) {
281		$t_view_state = VS_PRIVATE;
282	} else {
283		$t_view_state = VS_PUBLIC;
284	}
285
286	# insert bugnote info
287	db_param_push();
288	$t_query = 'INSERT INTO {bugnote}
289			(bug_id, reporter_id, bugnote_text_id, view_state, date_submitted, last_modified, note_type, note_attr, time_tracking)
290		VALUES ('
291		. db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', '
292		. db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', '
293		. db_param() . ' )';
294	$t_params = array(
295		$c_bug_id, $p_user_id, $t_bugnote_text_id, $t_view_state,
296		$c_date_submitted, $c_last_modified, $c_type, $p_attr,
297		$c_time_tracking );
298	db_query( $t_query, $t_params );
299
300	# get bugnote id
301	$t_bugnote_id = db_insert_id( db_get_table( 'bugnote' ) );
302
303	# update bug last updated
304	if( !$p_skip_bug_update ) {
305		bug_update_date( $p_bug_id );
306	}
307
308	# log new bug
309	if( $p_log_history ) {
310		history_log_event_special( $p_bug_id, BUGNOTE_ADDED, bugnote_format_id( $t_bugnote_id ) );
311	}
312
313	# Event integration
314	if( $p_trigger_event ) {
315		event_signal( 'EVENT_BUGNOTE_ADD', array( $p_bug_id, $t_bugnote_id, 'files' => array() ) );
316	}
317
318	# only send email if the text is not blank, otherwise, it is just recording of time without a comment.
319	if( true == $p_send_email && !is_blank( $t_bugnote_text ) ) {
320		email_bugnote_add( $t_bugnote_id );
321	}
322
323	return $t_bugnote_id;
324}
325
326/**
327 * Process mentions in bugnote, typically after its added.
328 *
329 * @param  int $p_bug_id          The bug id
330 * @param  int $p_bugnote_id      The bugnote id
331 * @param  string $p_bugnote_text The bugnote text
332 * @return array User ids that received mentioned emails.
333 * @access public
334 */
335function bugnote_process_mentions( $p_bug_id, $p_bugnote_id, $p_bugnote_text ) {
336	# Process the mentions that have access to the issue note
337	$t_mentioned_user_ids = mention_get_users( $p_bugnote_text );
338	$t_filtered_mentioned_user_ids = access_has_bugnote_level_filter(
339		config_get( 'view_bug_threshold' ),
340		$p_bugnote_id,
341		$t_mentioned_user_ids );
342
343	$t_removed_mentions_user_ids = array_diff( $t_mentioned_user_ids, $t_filtered_mentioned_user_ids );
344
345	return mention_process_user_mentions(
346		$p_bug_id,
347		$t_filtered_mentioned_user_ids,
348		$p_bugnote_text,
349		$t_removed_mentions_user_ids );
350}
351
352/**
353 * Delete a bugnote
354 * @param integer $p_bugnote_id A bug note identifier.
355 * @return boolean
356 * @access public
357 */
358function bugnote_delete( $p_bugnote_id ) {
359	$t_bug_id = bugnote_get_field( $p_bugnote_id, 'bug_id' );
360	$t_bugnote_text_id = bugnote_get_field( $p_bugnote_id, 'bugnote_text_id' );
361
362	# Remove the bugnote
363	db_param_push();
364	$t_query = 'DELETE FROM {bugnote} WHERE id=' . db_param();
365	db_query( $t_query, array( $p_bugnote_id ) );
366
367	# Remove the bugnote text
368	db_param_push();
369	$t_query = 'DELETE FROM {bugnote_text} WHERE id=' . db_param();
370	db_query( $t_query, array( $t_bugnote_text_id ) );
371
372	# log deletion of bug
373	history_log_event_special( $t_bug_id, BUGNOTE_DELETED, bugnote_format_id( $p_bugnote_id ) );
374
375	# Delete attachments linked to bugnote in the db (i.e. bugnote_id is set)
376	file_delete_bugnote_attachments( $t_bug_id, $p_bugnote_id );
377
378	# Event integration
379	event_signal( 'EVENT_BUGNOTE_DELETED', array( $t_bug_id, $p_bugnote_id ) );
380
381	return true;
382}
383
384/**
385 * delete all bugnotes associated with the given bug
386 * @param integer $p_bug_id A bug identifier.
387 * @return void
388 * @access public
389 */
390function bugnote_delete_all( $p_bug_id ) {
391	# Delete the bugnote text items
392	db_param_push();
393	$t_query = 'SELECT bugnote_text_id FROM {bugnote} WHERE bug_id=' . db_param();
394	$t_result = db_query( $t_query, array( (int)$p_bug_id ) );
395	while( $t_row = db_fetch_array( $t_result ) ) {
396		$t_bugnote_text_id = $t_row['bugnote_text_id'];
397
398		# Delete the corresponding bugnote texts
399		db_param_push();
400		$t_query = 'DELETE FROM {bugnote_text} WHERE id=' . db_param();
401		db_query( $t_query, array( $t_bugnote_text_id ) );
402	}
403
404	# Delete the corresponding bugnotes
405	db_param_push();
406	$t_query = 'DELETE FROM {bugnote} WHERE bug_id=' . db_param();
407	db_query( $t_query, array( (int)$p_bug_id ) );
408}
409
410/**
411 * Get the text associated with the bugnote
412 * @param integer $p_bugnote_id A bugnote identifier.
413 * @return string bugnote text
414 * @access public
415 */
416function bugnote_get_text( $p_bugnote_id ) {
417	$t_bugnote_text_id = bugnote_get_field( $p_bugnote_id, 'bugnote_text_id' );
418
419	# grab the bugnote text
420	db_param_push();
421	$t_query = 'SELECT note FROM {bugnote_text} WHERE id=' . db_param();
422	$t_result = db_query( $t_query, array( $t_bugnote_text_id ) );
423
424	return db_result( $t_result );
425}
426
427/**
428 * Get a field for the given bugnote
429 * @param integer $p_bugnote_id A bugnote identifier.
430 * @param string  $p_field_name Field name to retrieve.
431 * @return string field value
432 * @access public
433 */
434function bugnote_get_field( $p_bugnote_id, $p_field_name ) {
435	$t_bugnote = bugnote_get( $p_bugnote_id );
436	return $t_bugnote->$p_field_name;
437}
438
439/**
440 * Get latest bugnote id
441 * @param integer $p_bug_id A bug identifier.
442 * @return int latest bugnote id
443 * @access public
444 */
445function bugnote_get_latest_id( $p_bug_id ) {
446	db_param_push();
447	$t_query = 'SELECT id FROM {bugnote} WHERE bug_id=' . db_param() . ' ORDER by last_modified DESC';
448	$t_result = db_query( $t_query, array( (int)$p_bug_id ), 1 );
449
450	return (int)db_result( $t_result );
451}
452
453/**
454 * Build the bugnotes array for the given bug_id filtered by specified $p_user_access_level.
455 * Bugnotes are sorted by date_submitted according to 'bugnote_order' configuration setting.
456 * Return BugnoteData class object with raw values from the tables except the field
457 * last_modified - it is UNIX_TIMESTAMP.
458 * @param integer $p_bug_id             A bug identifier.
459 * @param integer $p_user_bugnote_order Sort order.
460 * @param integer $p_user_bugnote_limit Number of bugnotes to display to user.
461 * @param integer $p_user_id            A user identifier.
462 * @return array array of bugnotes
463 * @access public
464 */
465function bugnote_get_all_visible_bugnotes( $p_bug_id, $p_user_bugnote_order, $p_user_bugnote_limit, $p_user_id = null ) {
466	if( $p_user_id === null ) {
467		$t_user_id = auth_get_current_user_id();
468	} else {
469		$t_user_id = $p_user_id;
470	}
471
472	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
473	$t_user_access_level = user_get_access_level( $t_user_id, $t_project_id );
474
475	$t_all_bugnotes = bugnote_get_all_bugnotes( $p_bug_id );
476
477	$t_private_bugnote_visible = access_compare_level( $t_user_access_level, config_get( 'private_bugnote_threshold' ) );
478	$t_time_tracking_visible = access_compare_level( $t_user_access_level, config_get( 'time_tracking_view_threshold' ) );
479
480	$t_bugnotes = array();
481	$t_bugnote_count = count( $t_all_bugnotes );
482	$t_bugnote_limit = $p_user_bugnote_limit > 0 ? $p_user_bugnote_limit : $t_bugnote_count;
483	$t_bugnotes_found = 0;
484
485	# build a list of the latest bugnotes that the user can see
486	for( $i = 0; ( $i < $t_bugnote_count ) && ( $t_bugnotes_found < $t_bugnote_limit ); $i++ ) {
487		$t_bugnote = array_pop( $t_all_bugnotes );
488
489		if( $t_private_bugnote_visible || $t_bugnote->reporter_id == $t_user_id || ( VS_PUBLIC == $t_bugnote->view_state ) ) {
490			# If the access level specified is not enough to see time tracking information
491			# then reset it to 0.
492			if( !$t_time_tracking_visible ) {
493				$t_bugnote->time_tracking = 0;
494			}
495
496			$t_bugnotes[$t_bugnotes_found++] = $t_bugnote;
497		}
498	}
499
500	# reverse the list for users with ascending view preferences
501	if( 'ASC' == $p_user_bugnote_order ) {
502		$t_bugnotes = array_reverse( $t_bugnotes );
503	}
504
505	return $t_bugnotes;
506}
507
508/**
509 * Build a string that captures all the notes visible to the logged in user along with their
510 * metadata.  The string will contain information about each note including reporter, timestamp,
511 * time tracking, view state.  This will result in multi-line string with "\n" as the line
512 * separator.
513 *
514 * @param integer $p_bug_id             A bug identifier.
515 * @param integer $p_user_bugnote_order Sort order.
516 * @param integer $p_user_bugnote_limit Number of bugnotes to display to user.
517 * @param integer $p_user_id            A user identifier.
518 * @return string The string containing all visible notes.
519 * @access public
520 */
521function bugnote_get_all_visible_as_string( $p_bug_id, $p_user_bugnote_order, $p_user_bugnote_limit, $p_user_id = null ) {
522	$t_notes = bugnote_get_all_visible_bugnotes( $p_bug_id, $p_user_bugnote_order, $p_user_bugnote_limit, $p_user_id );
523	$t_date_format = config_get( 'normal_date_format' );
524	$t_show_time_tracking = access_has_bug_level( config_get( 'time_tracking_view_threshold' ), $p_bug_id );
525
526	$t_output = '';
527
528	foreach( $t_notes as $t_note ) {
529		$t_note_string = '@' . user_get_name( $t_note->reporter_id );
530		if ( $t_note->view_state != VS_PUBLIC ) {
531			$t_note_string .= ' (' . lang_get( 'private' ) . ')';
532		}
533
534		$t_note_string .= ' ' . date( $t_date_format, $t_note->date_submitted );
535
536		if ( $t_show_time_tracking && $t_note->note_type == TIME_TRACKING ) {
537			$t_time_tracking_hhmm = db_minutes_to_hhmm( $t_note->time_tracking );
538			$t_note_string .= ' ' . lang_get( 'time_tracking_time_spent' ) . ' ' . $t_time_tracking_hhmm;
539		}
540
541		$t_note_string .= "\n" . $t_note->note . "\n";
542
543		if ( !empty( $t_output ) ) {
544			# Use a marker that doesn't confuse markdown parser.
545			# `---` or `===` would mark previous line as a header.
546			$t_output .= "=-=\n";
547		}
548
549		$t_output .= $t_note_string;
550	}
551
552	return $t_output;
553}
554
555/**
556 * Converts a bugnote database row to a bugnote object.
557 *
558 * @param array $p_row The bugnote row (including bugnote_text note)
559 * @return BugnoteData The bugnote object.
560 * @access private
561 */
562function bugnote_row_to_object( array $p_row ) {
563	$t_bugnote = new BugnoteData;
564
565	$t_bugnote->id = $p_row['id'];
566	$t_bugnote->bug_id = (int)$p_row['bug_id'];
567	$t_bugnote->bugnote_text_id = (int)$p_row['bugnote_text_id'];
568	$t_bugnote->note = $p_row['note'];
569	$t_bugnote->view_state = (int)$p_row['view_state'];
570	$t_bugnote->reporter_id = (int)$p_row['reporter_id'];
571	$t_bugnote->date_submitted = (int)$p_row['date_submitted'];
572	$t_bugnote->last_modified = (int)$p_row['last_modified'];
573	$t_bugnote->note_type = (int)$p_row['note_type'];
574	$t_bugnote->note_attr = $p_row['note_attr'];
575	$t_bugnote->time_tracking = (int)$p_row['time_tracking'];
576
577	# Handle old bugnotes before setting type to time tracking
578	if ( $t_bugnote->time_tracking != 0 ) {
579		$t_bugnote->note_type = TIME_TRACKING;
580	}
581
582	return $t_bugnote;
583}
584
585/**
586 * Build the bugnotes array for the given bug_id.
587 * Return BugnoteData class object with raw values from the tables except the field
588 * last_modified - it is UNIX_TIMESTAMP.
589 * The data is not filtered by VIEW_STATE !!
590 * @param integer $p_bug_id A bug identifier.
591 * @return array array of bugnotes
592 * @access public
593 */
594function bugnote_get_all_bugnotes( $p_bug_id ) {
595	global $g_cache_bugnotes_by_bug_id;
596
597	# the cache should be aware of the sorting order
598	if( !isset( $g_cache_bugnotes_by_bug_id[(int)$p_bug_id] ) ) {
599		# Now sorting by submit date and id (#11742). The date_submitted
600		# column is currently not indexed, but that does not seem to affect
601		# performance in a measurable way
602		db_param_push();
603		$t_query = 'SELECT b.*, t.note
604			          	FROM      {bugnote} b
605			          	LEFT JOIN {bugnote_text} t ON b.bugnote_text_id = t.id
606						WHERE b.bug_id=' . db_param() . '
607						ORDER BY b.date_submitted ASC, b.id ASC';
608		$t_bugnotes = array();
609
610		# BUILD bugnotes array
611		$t_result = db_query( $t_query, array( $p_bug_id ) );
612
613		while( $t_row = db_fetch_array( $t_result ) ) {
614			$t_bugnote = bugnote_row_to_object( $t_row );
615			$t_bugnotes[] = $t_bugnote;
616			bugnote_cache( $t_bugnote );
617		}
618
619		$g_cache_bugnotes_by_bug_id[(int)$p_bug_id] = $t_bugnotes;
620	}
621
622	return $g_cache_bugnotes_by_bug_id[(int)$p_bug_id];
623}
624
625/**
626 * Gets the bugnote object given its id.
627 *
628 * @param int $p_bugnote_id The bugnote id.
629 * @return BugnoteData The bugnote object.
630 */
631function bugnote_get( $p_bugnote_id ) {
632	# If bugnote exists but not in cache, it will be added to cache.
633	# If bugnote doesn't exist, this will trigger an error.
634	bugnote_ensure_exists( $p_bugnote_id );
635
636	global $g_cache_bugnotes_by_id;
637
638	# Return the object from the cache, fetched above.
639	if( isset( $g_cache_bugnotes_by_id[(int)$p_bugnote_id] ) ) {
640		return $g_cache_bugnotes_by_id[(int)$p_bugnote_id];
641	}
642
643	# if we reached here something is wrong, trigger an error.
644	trigger_error( ERROR_BUGNOTE_NOT_FOUND, ERROR );
645}
646
647/**
648 * Update the time_tracking field of the bugnote
649 * @param integer $p_bugnote_id    A bugnote identifier.
650 * @param string  $p_time_tracking Timetracking string (hh:mm format).
651 * @return void
652 * @access public
653 */
654function bugnote_set_time_tracking( $p_bugnote_id, $p_time_tracking ) {
655	$c_bugnote_time_tracking = helper_duration_to_minutes( $p_time_tracking );
656
657	db_param_push();
658	$t_query = 'UPDATE {bugnote} SET time_tracking = ' . db_param() . ' WHERE id=' . db_param();
659	db_query( $t_query, array( $c_bugnote_time_tracking, $p_bugnote_id ) );
660}
661
662/**
663 * Update the last_modified field of the bugnote
664 * @param integer $p_bugnote_id A bugnote identifier.
665 * @return void
666 * @access public
667 */
668function bugnote_date_update( $p_bugnote_id ) {
669	db_param_push();
670	$t_query = 'UPDATE {bugnote} SET last_modified=' . db_param() . ' WHERE id=' . db_param();
671	db_query( $t_query, array( db_now(), $p_bugnote_id ) );
672}
673
674/**
675 * Set the bugnote text
676 * @param integer $p_bugnote_id   A bugnote identifier.
677 * @param string  $p_bugnote_text The bugnote text to set.
678 * @return boolean
679 * @access public
680 */
681function bugnote_set_text( $p_bugnote_id, $p_bugnote_text ) {
682	$t_old_text = bugnote_get_text( $p_bugnote_id );
683
684	if( $t_old_text == $p_bugnote_text ) {
685		return true;
686	}
687	# MySQL 4-bytes UTF-8 chars workaround #21101
688	$p_bugnote_text = db_mysql_fix_utf8( $p_bugnote_text );
689
690
691	$t_bug_id = bugnote_get_field( $p_bugnote_id, 'bug_id' );
692	$t_bugnote_text_id = bugnote_get_field( $p_bugnote_id, 'bugnote_text_id' );
693
694	# insert an 'original' revision if needed
695	if( bug_revision_count( $t_bug_id, REV_BUGNOTE, $p_bugnote_id ) < 1 ) {
696		$t_user_id = bugnote_get_field( $p_bugnote_id, 'reporter_id' );
697		$t_timestamp = bugnote_get_field( $p_bugnote_id, 'last_modified' );
698		bug_revision_add( $t_bug_id, $t_user_id, REV_BUGNOTE, $t_old_text, $p_bugnote_id, $t_timestamp );
699	}
700
701	db_param_push();
702	$t_query = 'UPDATE {bugnote_text} SET note=' . db_param() . ' WHERE id=' . db_param();
703	db_query( $t_query, array( $p_bugnote_text, $t_bugnote_text_id ) );
704
705	# updated the last_updated date
706	bugnote_date_update( $p_bugnote_id );
707	bug_update_date( $t_bug_id );
708
709	# insert a new revision
710	$t_user_id = auth_get_current_user_id();
711	$t_revision_id = bug_revision_add( $t_bug_id, $t_user_id, REV_BUGNOTE, $p_bugnote_text, $p_bugnote_id );
712
713	# log new bugnote
714	history_log_event_special( $t_bug_id, BUGNOTE_UPDATED, bugnote_format_id( $p_bugnote_id ), $t_revision_id );
715
716	return true;
717}
718
719/**
720 * Set the view state of the bugnote
721 * @param integer $p_bugnote_id A bugnote identifier.
722 * @param boolean $p_private    Whether bugnote should be set to private status.
723 * @return boolean
724 * @access public
725 */
726function bugnote_set_view_state( $p_bugnote_id, $p_private ) {
727	$t_bug_id = bugnote_get_field( $p_bugnote_id, 'bug_id' );
728
729	if( $p_private ) {
730		$t_view_state = VS_PRIVATE;
731	} else {
732		$t_view_state = VS_PUBLIC;
733	}
734
735	db_param_push();
736	$t_query = 'UPDATE {bugnote} SET view_state=' . db_param() . ' WHERE id=' . db_param();
737	db_query( $t_query, array( $t_view_state, $p_bugnote_id ) );
738
739	history_log_event_special( $t_bug_id, BUGNOTE_STATE_CHANGED, $t_view_state, bugnote_format_id( $p_bugnote_id ) );
740
741	return true;
742}
743
744/**
745 * Pad the bugnote id with the appropriate number of zeros for printing
746 * @param integer $p_bugnote_id A bugnote identifier.
747 * @return string
748 * @access public
749 */
750function bugnote_format_id( $p_bugnote_id ) {
751	$t_padding = config_get( 'display_bugnote_padding' );
752
753	return utf8_str_pad( $p_bugnote_id, $t_padding, '0', STR_PAD_LEFT );
754}
755
756/**
757 * Returns an array of bugnote stats
758 * @param integer $p_bug_id A bug identifier.
759 * @param string  $p_from   Starting date (yyyy-mm-dd) inclusive, if blank, then ignored.
760 * @param string  $p_to     Ending date (yyyy-mm-dd) inclusive, if blank, then ignored.
761 * @return array array of bugnote stats
762 * @access public
763 */
764function bugnote_stats_get_events_array( $p_bug_id, $p_from, $p_to ) {
765	$c_to = strtotime( $p_to ) + SECONDS_PER_DAY - 1;
766	$c_from = strtotime( $p_from );
767
768	if( !is_blank( $c_from ) ) {
769		$t_from_where = ' AND bn.date_submitted >= ' . $c_from;
770	} else {
771		$t_from_where = '';
772	}
773
774	if( !is_blank( $c_to ) ) {
775		$t_to_where = ' AND bn.date_submitted <= ' . $c_to;
776	} else {
777		$t_to_where = '';
778	}
779
780	$t_results = array();
781
782	db_param_push();
783	$t_query = 'SELECT u.id AS user_id, username, realname, SUM(time_tracking) AS sum_time_tracking
784				FROM {user} u, {bugnote} bn
785				WHERE u.id = bn.reporter_id AND bn.time_tracking != 0 AND
786				bn.bug_id = ' . db_param() . $t_from_where . $t_to_where .
787				' GROUP BY u.id, u.username, u.realname';
788	$t_result = db_query( $t_query, array( $p_bug_id ) );
789
790	while( $t_row = db_fetch_array( $t_result ) ) {
791		$t_row['name'] = user_get_name_from_row( $t_row );
792		$t_results[(int)$t_row['user_id']] = $t_row;
793	}
794
795	return $t_results;
796}
797
798/**
799 * Clear a bugnote from the cache or all bug notes if no bugnote id specified.
800 * @param integer $p_bugnote_id Identifier to clear (optional).
801 * @return boolean
802 * @access public
803 */
804function bugnote_clear_cache( $p_bugnote_id = null ) {
805	global $g_cache_bugnotes_by_id, $g_cache_bugnotes_by_bug_id;
806
807	if( null === $p_bugnote_id ) {
808		$g_cache_bugnotes_by_id = array();
809		$g_cache_bugnotes_by_bug_id = array();
810	} else {
811		$p_bugnote_id = (int)$p_bugnote_id;
812		if( isset( $g_cache_bugnotes_by_id[$p_bugnote_id] ) ) {
813			$t_note_obj = $g_cache_bugnotes_by_id[$p_bugnote_id];
814			unset($g_cache_bugnotes_by_id[$p_bugnote_id]);
815
816			# Clear the bug-level cache for the bugnote's parent bug
817			bugnote_clear_bug_cache( $t_note_obj->bug_id );
818		}
819	}
820
821	return true;
822}
823
824/**
825 * Clear the bugnotes related to a bug, or all bugs if no bug id specified.
826 * @param integer $p_bug_id Identifier to clear (optional).
827 * @return boolean
828 * @access public
829 */
830function bugnote_clear_bug_cache( $p_bug_id = null ) {
831	global $g_cache_bugnotes_by_bug_id, $g_cache_bugnotes_by_id;
832
833	if( null === $p_bug_id ) {
834		$g_cache_bugnotes_by_bug_id = array();
835		$g_cache_bugnotes_by_id = array();
836	} else {
837		if( isset( $g_cache_bugnotes_by_bug_id[(int)$p_bug_id] ) ) {
838			foreach( $g_cache_bugnotes_by_bug_id[(int)$p_bug_id] as $t_note_obj ) {
839				unset( $g_cache_bugnotes_by_id[(int)$t_note_obj->id] );
840			}
841			unset( $g_cache_bugnotes_by_bug_id[(int)$p_bug_id] );
842		}
843	}
844
845	return true;
846}
847