PageRenderTime 63ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/core/history_api.php

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