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