PageRenderTime 68ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 1ms

/core/history_api.php

https://github.com/Kirill/mantisbt
PHP | 971 lines | 701 code | 79 blank | 191 comment | 126 complexity | d93afbbb2c7b138319d4bed206ca8c9f 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 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. }
  379. # monitoring
  380. if( $v_type == BUG_MONITOR || $v_type == BUG_UNMONITOR ) {
  381. if( !access_has_bug_level( config_get( 'show_monitor_list_threshold' ), $v_bug_id, $t_user_id ) ) {
  382. continue;
  383. }
  384. }
  385. # relationships
  386. if( $v_type == BUG_ADD_RELATIONSHIP || $v_type == BUG_DEL_RELATIONSHIP || $v_type == BUG_REPLACE_RELATIONSHIP ) {
  387. $t_related_bug_id = $v_new_value;
  388. # If bug doesn't exist, then we don't know whether to expose it or not based on the fact whether it was
  389. # accessible to user or not. This also simplifies client code that is accessing the history log.
  390. if( !bug_exists( $t_related_bug_id ) || !access_has_bug_level( config_get( 'view_bug_threshold' ), $t_related_bug_id, $t_user_id ) ) {
  391. continue;
  392. }
  393. }
  394. $t_event = array();
  395. $t_event['bug_id'] = $v_bug_id;
  396. $t_event['date'] = $v_date_modified;
  397. $t_event['userid'] = $v_user_id;
  398. $t_event['username'] = user_get_name( $v_user_id );
  399. $t_event['field'] = $v_field_name;
  400. $t_event['type'] = $v_type;
  401. $t_event['old_value'] = $v_old_value;
  402. $t_event['new_value'] = $v_new_value;
  403. return $t_event;
  404. }
  405. return false;
  406. }
  407. /**
  408. * Retrieves the raw history events for the specified bug id and returns it in an array
  409. * The array is indexed from 0 to N-1. The second dimension is: 'date', 'userid', 'username',
  410. * 'field','type','old_value','new_value'
  411. * @param integer $p_bug_id A valid bug identifier or null to not filter by bug. If no bug id is specified,
  412. * then returned array will have a field for bug_id, otherwise it won't.
  413. * @param integer $p_user_id A valid user identifier.
  414. * @param integer $p_start_time The start time to filter by, or null for all.
  415. * @param integer $p_end_time The end time to filter by, or null for all.
  416. * @return array
  417. */
  418. function history_get_raw_events_array( $p_bug_id, $p_user_id = null, $p_start_time = null, $p_end_time = null ) {
  419. $t_user_id = (( null === $p_user_id ) ? auth_get_current_user_id() : $p_user_id );
  420. $t_query_options = array(
  421. 'bug_id' => $p_bug_id,
  422. 'start_time' => $p_start_time,
  423. 'end_time' => $p_end_time
  424. );
  425. $t_result = history_query_result( $t_query_options );
  426. $t_raw_history = array();
  427. $j = 0;
  428. while( true ) {
  429. $t_event = history_get_event_from_row( $t_result, $t_user_id, /* check access */ true );
  430. if ( $t_event === false ) {
  431. break;
  432. }
  433. $t_raw_history[$j] = $t_event;
  434. $j++;
  435. }
  436. # end for loop
  437. return $t_raw_history;
  438. }
  439. /**
  440. * Localize the specified field name for native or custom fields.
  441. *
  442. * @param string $p_field_name The field name.
  443. * @return string The localized field name.
  444. */
  445. function history_localize_field_name( $p_field_name ) {
  446. switch( $p_field_name ) {
  447. case 'category':
  448. $t_field_localized = lang_get( 'category' );
  449. break;
  450. case 'status':
  451. $t_field_localized = lang_get( 'status' );
  452. break;
  453. case 'severity':
  454. $t_field_localized = lang_get( 'severity' );
  455. break;
  456. case 'reproducibility':
  457. $t_field_localized = lang_get( 'reproducibility' );
  458. break;
  459. case 'resolution':
  460. $t_field_localized = lang_get( 'resolution' );
  461. break;
  462. case 'priority':
  463. $t_field_localized = lang_get( 'priority' );
  464. break;
  465. case 'eta':
  466. $t_field_localized = lang_get( 'eta' );
  467. break;
  468. case 'view_state':
  469. $t_field_localized = lang_get( 'view_status' );
  470. break;
  471. case 'projection':
  472. $t_field_localized = lang_get( 'projection' );
  473. break;
  474. case 'sticky':
  475. $t_field_localized = lang_get( 'sticky_issue' );
  476. break;
  477. case 'project_id':
  478. $t_field_localized = lang_get( 'email_project' );
  479. break;
  480. case 'handler_id':
  481. $t_field_localized = lang_get( 'assigned_to' );
  482. break;
  483. case 'reporter_id':
  484. $t_field_localized = lang_get( 'reporter' );
  485. break;
  486. case 'version':
  487. $t_field_localized = lang_get( 'product_version' );
  488. break;
  489. case 'fixed_in_version':
  490. $t_field_localized = lang_get( 'fixed_in_version' );
  491. break;
  492. case 'target_version':
  493. $t_field_localized = lang_get( 'target_version' );
  494. break;
  495. case 'date_submitted':
  496. $t_field_localized = lang_get( 'date_submitted' );
  497. break;
  498. case 'last_updated':
  499. $t_field_localized = lang_get( 'last_update' );
  500. break;
  501. case 'os':
  502. $t_field_localized = lang_get( 'os' );
  503. break;
  504. case 'os_build':
  505. $t_field_localized = lang_get( 'os_version' );
  506. break;
  507. case 'build':
  508. $t_field_localized = lang_get( 'build' );
  509. break;
  510. case 'platform':
  511. $t_field_localized = lang_get( 'platform' );
  512. break;
  513. case 'summary':
  514. $t_field_localized = lang_get( 'summary' );
  515. break;
  516. case 'duplicate_id':
  517. $t_field_localized = lang_get( 'duplicate_id' );
  518. break;
  519. case 'sponsorship_total':
  520. $t_field_localized = lang_get( 'sponsorship_total' );
  521. break;
  522. case 'due_date':
  523. $t_field_localized = lang_get( 'due_date' );
  524. break;
  525. default:
  526. # assume it's a custom field name
  527. $t_field_localized = lang_get_defaulted( $p_field_name );
  528. break;
  529. }
  530. return $t_field_localized;
  531. }
  532. /**
  533. * Get name of the change type.
  534. *
  535. * @param integer $p_type The type code.
  536. * @return string The type name.
  537. */
  538. function history_get_type_name( $p_type ) {
  539. $t_type = (int)$p_type;
  540. switch( $t_type ) {
  541. case NORMAL_TYPE:
  542. $t_type_name = 'field-updated';
  543. break;
  544. case NEW_BUG:
  545. $t_type_name = 'issue-new';
  546. break;
  547. case BUGNOTE_ADDED:
  548. $t_type_name = 'note-added';
  549. break;
  550. case BUGNOTE_UPDATED:
  551. $t_type_name = 'note-updated';
  552. break;
  553. case BUGNOTE_DELETED:
  554. $t_type_name = 'note-deleted';
  555. break;
  556. case DESCRIPTION_UPDATED:
  557. $t_type_name = 'issue-description-updated';
  558. break;
  559. case ADDITIONAL_INFO_UPDATED:
  560. $t_type_name = 'issue-additional-info-updated';
  561. break;
  562. case STEP_TO_REPRODUCE_UPDATED:
  563. $t_type_name = 'issue-steps-to-reproduce-updated';
  564. break;
  565. case FILE_ADDED:
  566. $t_type_name = 'file-added';
  567. break;
  568. case FILE_DELETED:
  569. $t_type_name = 'file-deleted';
  570. break;
  571. case BUGNOTE_STATE_CHANGED:
  572. $t_type_name = 'note-view-state-updated';
  573. break;
  574. case BUG_MONITOR:
  575. $t_type_name = 'monitor-added';
  576. break;
  577. case BUG_UNMONITOR:
  578. $t_type_name = 'monitor-deleted';
  579. break;
  580. case BUG_DELETED:
  581. $t_type_name = 'issue-deleted';
  582. break;
  583. case BUG_ADD_SPONSORSHIP:
  584. $t_type_name = 'sponsorship-added';
  585. break;
  586. case BUG_UPDATE_SPONSORSHIP:
  587. $t_type_name = 'sponsorship-updated';
  588. break;
  589. case BUG_DELETE_SPONSORSHIP:
  590. $t_type_name = 'sponsorship-deleted';
  591. break;
  592. case BUG_PAID_SPONSORSHIP:
  593. $t_type_name = 'sponsorship-paid';
  594. break;
  595. case BUG_ADD_RELATIONSHIP:
  596. $t_type_name = 'relationship-added';
  597. break;
  598. case BUG_REPLACE_RELATIONSHIP:
  599. $t_type_name = 'relationship-updated';
  600. break;
  601. case BUG_DEL_RELATIONSHIP:
  602. $t_type_name = 'relationship-deleted';
  603. break;
  604. case BUG_CLONED_TO:
  605. $t_type_name = 'issue-cloned-to';
  606. break;
  607. case BUG_CREATED_FROM:
  608. $t_type_name = 'issue-cloned-from';
  609. break;
  610. case TAG_ATTACHED:
  611. $t_type_name = 'tag-added';
  612. break;
  613. case TAG_DETACHED:
  614. $t_type_name = 'tag-deleted';
  615. break;
  616. case TAG_RENAMED:
  617. $t_type_name = 'tag-updated';
  618. break;
  619. case BUG_REVISION_DROPPED:
  620. $t_type_name = 'revision-deleted';
  621. break;
  622. case BUGNOTE_REVISION_DROPPED:
  623. $t_type_name = 'note-revision-deleted';
  624. break;
  625. default:
  626. $t_type_name = '';
  627. break;
  628. }
  629. return $t_type_name;
  630. }
  631. /**
  632. * Localizes one raw history item specified by set the next parameters: $p_field_name, $p_type, $p_old_value, $p_new_value
  633. * Returns array with two elements indexed as 'note' and 'change'
  634. * @param string $p_field_name The field name of the field being localized.
  635. * @param integer $p_type The type of the history entry.
  636. * @param string $p_old_value The old value of the field.
  637. * @param string $p_new_value The new value of the field.
  638. * @param boolean $p_linkify Whether to return a string containing hyperlinks.
  639. * @return array
  640. */
  641. function history_localize_item( $p_field_name, $p_type, $p_old_value, $p_new_value, $p_linkify = true ) {
  642. $t_note = '';
  643. $t_change = '';
  644. $t_raw = true;
  645. if( PLUGIN_HISTORY == $p_type ) {
  646. $t_note = lang_get_defaulted( 'plugin_' . $p_field_name, $p_field_name );
  647. $t_change = ( isset( $p_new_value ) ? $p_old_value . ' => ' . $p_new_value : $p_old_value );
  648. return array( 'note' => $t_note, 'change' => $t_change, 'raw' => true );
  649. }
  650. $t_field_localized = history_localize_field_name( $p_field_name );
  651. switch( $p_field_name ) {
  652. case 'status':
  653. $p_old_value = get_enum_element( 'status', $p_old_value );
  654. $p_new_value = get_enum_element( 'status', $p_new_value );
  655. break;
  656. case 'severity':
  657. $p_old_value = get_enum_element( 'severity', $p_old_value );
  658. $p_new_value = get_enum_element( 'severity', $p_new_value );
  659. break;
  660. case 'reproducibility':
  661. $p_old_value = get_enum_element( 'reproducibility', $p_old_value );
  662. $p_new_value = get_enum_element( 'reproducibility', $p_new_value );
  663. break;
  664. case 'resolution':
  665. $p_old_value = get_enum_element( 'resolution', $p_old_value );
  666. $p_new_value = get_enum_element( 'resolution', $p_new_value );
  667. break;
  668. case 'priority':
  669. $p_old_value = get_enum_element( 'priority', $p_old_value );
  670. $p_new_value = get_enum_element( 'priority', $p_new_value );
  671. break;
  672. case 'eta':
  673. $p_old_value = get_enum_element( 'eta', $p_old_value );
  674. $p_new_value = get_enum_element( 'eta', $p_new_value );
  675. break;
  676. case 'view_state':
  677. $p_old_value = get_enum_element( 'view_state', $p_old_value );
  678. $p_new_value = get_enum_element( 'view_state', $p_new_value );
  679. break;
  680. case 'projection':
  681. $p_old_value = get_enum_element( 'projection', $p_old_value );
  682. $p_new_value = get_enum_element( 'projection', $p_new_value );
  683. break;
  684. case 'sticky':
  685. $p_old_value = gpc_string_to_bool( $p_old_value ) ? lang_get( 'yes' ) : lang_get( 'no' );
  686. $p_new_value = gpc_string_to_bool( $p_new_value ) ? lang_get( 'yes' ) : lang_get( 'no' );
  687. break;
  688. case 'project_id':
  689. if( project_exists( $p_old_value ) ) {
  690. $p_old_value = project_get_field( $p_old_value, 'name' );
  691. } else {
  692. $p_old_value = '@' . $p_old_value . '@';
  693. }
  694. # Note that the new value maybe an intermediately project and not the
  695. # current one.
  696. if( project_exists( $p_new_value ) ) {
  697. $p_new_value = project_get_field( $p_new_value, 'name' );
  698. } else {
  699. $p_new_value = '@' . $p_new_value . '@';
  700. }
  701. break;
  702. case 'handler_id':
  703. case 'reporter_id':
  704. if( 0 == $p_old_value ) {
  705. $p_old_value = '';
  706. } else {
  707. $p_old_value = user_get_name( $p_old_value );
  708. }
  709. if( 0 == $p_new_value ) {
  710. $p_new_value = '';
  711. } else {
  712. $p_new_value = user_get_name( $p_new_value );
  713. }
  714. break;
  715. case 'date_submitted':
  716. $p_old_value = date( config_get( 'normal_date_format' ), $p_old_value );
  717. $p_new_value = date( config_get( 'normal_date_format' ), $p_new_value );
  718. break;
  719. case 'last_updated':
  720. $p_old_value = date( config_get( 'normal_date_format' ), $p_old_value );
  721. $p_new_value = date( config_get( 'normal_date_format' ), $p_new_value );
  722. break;
  723. case 'due_date':
  724. if( $p_old_value !== '' ) {
  725. $p_old_value = date( config_get( 'normal_date_format' ), (int)$p_old_value );
  726. }
  727. if( $p_new_value !== '' ) {
  728. $p_new_value = date( config_get( 'normal_date_format' ), (int)$p_new_value );
  729. }
  730. break;
  731. default:
  732. # assume it's a custom field name
  733. $t_field_id = custom_field_get_id_from_name( $p_field_name );
  734. if( false !== $t_field_id ) {
  735. $t_cf_type = custom_field_type( $t_field_id );
  736. if( '' != $p_old_value ) {
  737. $p_old_value = string_custom_field_value_for_email( $p_old_value, $t_cf_type );
  738. }
  739. $p_new_value = string_custom_field_value_for_email( $p_new_value, $t_cf_type );
  740. }
  741. }
  742. if( NORMAL_TYPE != $p_type ) {
  743. switch( $p_type ) {
  744. case NEW_BUG:
  745. $t_note = lang_get( 'new_bug' );
  746. break;
  747. case BUGNOTE_ADDED:
  748. $t_note = lang_get( 'bugnote_added' ) . ': ' . $p_old_value;
  749. break;
  750. case BUGNOTE_UPDATED:
  751. $t_note = lang_get( 'bugnote_edited' ) . ': ' . $p_old_value;
  752. $t_old_value = (int)$p_old_value;
  753. $t_new_value = (int)$p_new_value;
  754. if( $p_linkify && bug_revision_exists( $t_new_value ) ) {
  755. if( bugnote_exists( $t_old_value ) ) {
  756. $t_bug_revision_view_page_argument = 'bugnote_id=' . $t_old_value . '#r' . $t_new_value;
  757. } else {
  758. $t_bug_revision_view_page_argument = 'rev_id=' . $t_new_value;
  759. }
  760. $t_change = '<a href="bug_revision_view_page.php?' . $t_bug_revision_view_page_argument . '">' .
  761. lang_get( 'view_revisions' ) . '</a>';
  762. $t_raw = false;
  763. }
  764. break;
  765. case BUGNOTE_DELETED:
  766. $t_note = lang_get( 'bugnote_deleted' ) . ': ' . $p_old_value;
  767. break;
  768. case DESCRIPTION_UPDATED:
  769. $t_note = lang_get( 'description_updated' );
  770. $t_old_value = (int)$p_old_value;
  771. if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
  772. $t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
  773. lang_get( 'view_revisions' ) . '</a>';
  774. $t_raw = false;
  775. }
  776. break;
  777. case ADDITIONAL_INFO_UPDATED:
  778. $t_note = lang_get( 'additional_information_updated' );
  779. $t_old_value = (int)$p_old_value;
  780. if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
  781. $t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
  782. lang_get( 'view_revisions' ) . '</a>';
  783. $t_raw = false;
  784. }
  785. break;
  786. case STEP_TO_REPRODUCE_UPDATED:
  787. $t_note = lang_get( 'steps_to_reproduce_updated' );
  788. $t_old_value = (int)$p_old_value;
  789. if( $p_linkify && bug_revision_exists( $t_old_value ) ) {
  790. $t_change = '<a href="bug_revision_view_page.php?rev_id=' . $t_old_value . '#r' . $t_old_value . '">' .
  791. lang_get( 'view_revisions' ) . '</a>';
  792. $t_raw = false;
  793. }
  794. break;
  795. case FILE_ADDED:
  796. $t_note = lang_get( 'file_added' ) . ': ' . $p_old_value;
  797. break;
  798. case FILE_DELETED:
  799. $t_note = lang_get( 'file_deleted' ) . ': ' . $p_old_value;
  800. break;
  801. case BUGNOTE_STATE_CHANGED:
  802. $p_old_value = get_enum_element( 'view_state', $p_old_value );
  803. $t_note = lang_get( 'bugnote_view_state' ) . ': ' . $p_new_value . ': ' . $p_old_value;
  804. break;
  805. case BUG_MONITOR:
  806. $p_old_value = user_get_name( $p_old_value );
  807. $t_note = lang_get( 'bug_monitor' ) . ': ' . $p_old_value;
  808. break;
  809. case BUG_UNMONITOR:
  810. if( $p_old_value !== '' ) {
  811. $p_old_value = user_get_name( $p_old_value );
  812. }
  813. $t_note = lang_get( 'bug_end_monitor' ) . ': ' . $p_old_value;
  814. break;
  815. case BUG_DELETED:
  816. $t_note = lang_get( 'bug_deleted' ) . ': ' . $p_old_value;
  817. break;
  818. case BUG_ADD_SPONSORSHIP:
  819. $t_note = lang_get( 'sponsorship_added' );
  820. $t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
  821. break;
  822. case BUG_UPDATE_SPONSORSHIP:
  823. $t_note = lang_get( 'sponsorship_updated' );
  824. $t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
  825. break;
  826. case BUG_DELETE_SPONSORSHIP:
  827. $t_note = lang_get( 'sponsorship_deleted' );
  828. $t_change = user_get_name( $p_old_value ) . ': ' . sponsorship_format_amount( $p_new_value );
  829. break;
  830. case BUG_PAID_SPONSORSHIP:
  831. $t_note = lang_get( 'sponsorship_paid' );
  832. $t_change = user_get_name( $p_old_value ) . ': ' . get_enum_element( 'sponsorship', $p_new_value );
  833. break;
  834. case BUG_ADD_RELATIONSHIP:
  835. $t_note = lang_get( 'relationship_added' );
  836. $t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
  837. break;
  838. case BUG_REPLACE_RELATIONSHIP:
  839. $t_note = lang_get( 'relationship_replaced' );
  840. $t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
  841. break;
  842. case BUG_DEL_RELATIONSHIP:
  843. $t_note = lang_get( 'relationship_deleted' );
  844. # Fix for #7846: There are some cases where old value is empty, this may be due to an old bug.
  845. if( !is_blank( $p_old_value ) && $p_old_value > 0 ) {
  846. $t_change = relationship_get_description_for_history( $p_old_value ) . ' ' . bug_format_id( $p_new_value );
  847. } else {
  848. $t_change = bug_format_id( $p_new_value );
  849. }
  850. break;
  851. case BUG_CLONED_TO:
  852. $t_note = lang_get( 'bug_cloned_to' ) . ': ' . bug_format_id( $p_new_value );
  853. break;
  854. case BUG_CREATED_FROM:
  855. $t_note = lang_get( 'bug_created_from' ) . ': ' . bug_format_id( $p_new_value );
  856. break;
  857. case TAG_ATTACHED:
  858. $t_note = lang_get( 'tag_history_attached' ) . ': ' . $p_old_value;
  859. break;
  860. case TAG_DETACHED:
  861. $t_note = lang_get( 'tag_history_detached' ) . ': ' . $p_old_value;
  862. break;
  863. case TAG_RENAMED:
  864. $t_note = lang_get( 'tag_history_renamed' );
  865. $t_change = $p_old_value . ' => ' . $p_new_value;
  866. break;
  867. case BUG_REVISION_DROPPED:
  868. $t_note = lang_get( 'bug_revision_dropped_history' ) . ': ' . bug_revision_get_type_name( $p_new_value ) . ': ' . $p_old_value;
  869. break;
  870. case BUGNOTE_REVISION_DROPPED:
  871. $t_note = lang_get( 'bugnote_revision_dropped_history' ) . ': ' . $p_new_value . ': ' . $p_old_value;
  872. break;
  873. }
  874. }
  875. # output special cases
  876. if( NORMAL_TYPE == $p_type ) {
  877. $t_note = $t_field_localized;
  878. $t_change = $p_old_value . ' => ' . $p_new_value;
  879. }
  880. # end if DEFAULT
  881. return array( 'note' => $t_note, 'change' => $t_change, 'raw' => $t_raw );
  882. }
  883. /**
  884. * delete all history associated with a bug
  885. * @param integer $p_bug_id A valid bug identifier.
  886. * @return void
  887. */
  888. function history_delete( $p_bug_id ) {
  889. db_param_push();
  890. $t_query = 'DELETE FROM {bug_history} WHERE bug_id=' . db_param();
  891. db_query( $t_query, array( $p_bug_id ) );
  892. }