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 * Helper API 19 * 20 * @package CoreAPI 21 * @subpackage HelperAPI 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 authentication_api.php 28 * @uses config_api.php 29 * @uses constant_inc.php 30 * @uses current_user_api.php 31 * @uses error_api.php 32 * @uses gpc_api.php 33 * @uses html_api.php 34 * @uses lang_api.php 35 * @uses print_api.php 36 * @uses project_api.php 37 * @uses user_api.php 38 * @uses user_pref_api.php 39 * @uses utility_api.php 40 */ 41 42require_api( 'access_api.php' ); 43require_api( 'authentication_api.php' ); 44require_api( 'config_api.php' ); 45require_api( 'constant_inc.php' ); 46require_api( 'current_user_api.php' ); 47require_api( 'error_api.php' ); 48require_api( 'gpc_api.php' ); 49require_api( 'html_api.php' ); 50require_api( 'lang_api.php' ); 51require_api( 'print_api.php' ); 52require_api( 'project_api.php' ); 53require_api( 'user_api.php' ); 54require_api( 'user_pref_api.php' ); 55require_api( 'utility_api.php' ); 56 57use Mantis\Exceptions\ClientException; 58 59/** 60 * alternate classes for table rows 61 * If no index is given, continue alternating based on the last index given 62 * @param int $p_index 63 * @param string $p_odd_class default: row-1 64 * @param string $p_even_class default: row-2 65 * @return string 66 */ 67function helper_alternate_class( $p_index = null, $p_odd_class = 'row-1', $p_even_class = 'row-2' ) { 68 static $t_index = 1; 69 70 if( null !== $p_index ) { 71 $t_index = $p_index; 72 } 73 74 error_parameters( __FUNCTION__, 'CSS' ); 75 trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED ); 76 77 if( 1 == $t_index++ % 2 ) { 78 return "class=\"$p_odd_class\""; 79 } else { 80 return "class=\"$p_even_class\""; 81 } 82} 83 84/** 85 * Transpose a bidimensional array 86 * 87 * e.g. array('a'=>array('k1'=>1,'k2'=>2),'b'=>array('k1'=>3,'k2'=>4)) 88 * becomes array('k1'=>array('a'=>1,'b'=>3),'k2'=>array('a'=>2,'b'=>4)) 89 * 90 * @param array $p_array The array to transpose. 91 * @return array|mixed transposed array or $p_array if not 2-dimensional array 92 */ 93function helper_array_transpose( array $p_array ) { 94 $t_out = array(); 95 foreach( $p_array as $t_key => $t_sub ) { 96 if( !is_array( $t_sub ) ) { 97 # This function can only handle bidimensional arrays 98 trigger_error( ERROR_GENERIC, ERROR ); 99 } 100 101 foreach( $t_sub as $t_subkey => $t_value ) { 102 $t_out[$t_subkey][$t_key] = $t_value; 103 } 104 } 105 return $t_out; 106} 107 108/** 109 * get the color string for the given status, user and project 110 * @param integer $p_status Status value. 111 * @param integer|null $p_user User id, defaults to null (all users). 112 * @param integer|null $p_project Project id, defaults to null (all projects). 113 * @param string $p_default_color Fallback color in case status is not found (defaults to white). 114 * @return string 115 */ 116function get_status_color( $p_status, $p_user = null, $p_project = null, $p_default_color = '#ffffff' ) { 117 $t_status_enum = config_get( 'status_enum_string', null, $p_user, $p_project ); 118 $t_status_colors = config_get( 'status_colors', null, $p_user, $p_project ); 119 $t_status_label = MantisEnum::getLabel( $t_status_enum, $p_status ); 120 121 if( isset( $t_status_colors[$t_status_label] ) ) { 122 return $t_status_colors[$t_status_label]; 123 } 124 return $p_default_color; 125} 126 127/** 128 * get the status percentages 129 * @return array key is the status value, value is the percentage of bugs for the status 130 */ 131function get_percentage_by_status() { 132 $t_project_id = helper_get_current_project(); 133 $t_user_id = auth_get_current_user_id(); 134 135 # checking if it's a per project statistic or all projects 136 $t_specific_where = helper_project_specific_where( $t_project_id, $t_user_id ); 137 138 $t_query = 'SELECT status, COUNT(*) AS num 139 FROM {bug} 140 WHERE ' . $t_specific_where; 141 if( !access_has_project_level( config_get( 'private_bug_threshold' ) ) ) { 142 $t_query .= ' AND view_state < ' . VS_PRIVATE; 143 } 144 $t_query .= ' GROUP BY status'; 145 $t_result = db_query( $t_query ); 146 147 $t_status_count_array = array(); 148 149 while( $t_row = db_fetch_array( $t_result ) ) { 150 $t_status_count_array[$t_row['status']] = $t_row['num']; 151 } 152 $t_bug_count = array_sum( $t_status_count_array ); 153 foreach( $t_status_count_array as $t_status=>$t_value ) { 154 $t_status_count_array[$t_status] = round( ( $t_value / $t_bug_count ) * 100 ); 155 } 156 157 return $t_status_count_array; 158} 159 160/** 161 * Given a enumeration string and number, return the appropriate string for the 162 * specified user/project 163 * @param string $p_enum_name An enumeration string name. 164 * @param integer $p_val An enumeration string value. 165 * @param integer|null $p_user A user identifier, defaults to null (all users). 166 * @param integer|null $p_project A project identifier, defaults to null (all projects). 167 * @return string 168 */ 169function get_enum_element( $p_enum_name, $p_val, $p_user = null, $p_project = null ) { 170 $t_config_var = config_get( $p_enum_name . '_enum_string', null, $p_user, $p_project ); 171 $t_string_var = lang_get( $p_enum_name . '_enum_string' ); 172 173 return MantisEnum::getLocalizedLabel( $t_config_var, $t_string_var, $p_val ); 174} 175 176/** 177 * Compares the 2 specified variables, returns true if equal, false if not. 178 * With strict type checking, will trigger an error if the types of the compared 179 * variables don't match. 180 * This helper function is used by {@link check_checked()} and {@link check_selected()} 181 * @param mixed $p_var1 The variable to compare. 182 * @param mixed $p_var2 The second variable to compare. 183 * @param boolean $p_strict Set to true for strict type checking, false for loose. 184 * @return boolean 185 */ 186function helper_check_variables_equal( $p_var1, $p_var2, $p_strict ) { 187 if( $p_strict ) { 188 if( gettype( $p_var1 ) !== gettype( $p_var2 ) ) { 189 # Reaching this point is a a sign that you need to check the types 190 # of the parameters passed to this function. They should match. 191 trigger_error( ERROR_TYPE_MISMATCH, ERROR ); 192 } 193 194 # We need to be careful when comparing an array of 195 # version number strings (["1.0", "1.1", "1.10"]) to 196 # a selected version number of "1.10". If a == 197 # comparison were to be used, PHP would treat 198 # "1.1" and "1.10" as being the same as the strings 199 # would be converted to numerals before being compared 200 # as numerals. 201 # 202 # This is further complicated by filter dropdowns 203 # containing a mixture of string and integer values. 204 # The following "meta filter values" exist as integer 205 # values in dropdowns: 206 # META_FILTER_MYSELF = -1 207 # META_FILTER_NONE = -2 208 # META_FILTER_CURRENT = -3 209 # META_FILTER_ANY = 0 210 # 211 # For these reasons, a === comparison is required. 212 213 return $p_var1 === $p_var2; 214 } else { 215 return $p_var1 == $p_var2; 216 } 217} 218 219/** 220 * Attach a "checked" attribute to a HTML element if $p_var === $p_val or 221 * a {value within an array passed via $p_var} === $p_val. 222 * 223 * If the second parameter is not given, the first parameter is compared to 224 * the boolean value true. 225 * 226 * @param mixed $p_var The variable to compare. 227 * @param mixed $p_val The value to compare $p_var with. 228 * @param boolean $p_strict Set to false to bypass strict type checking (defaults to true). 229 * @return void 230 */ 231function check_checked( $p_var, $p_val = true, $p_strict = true ) { 232 if( is_array( $p_var ) ) { 233 foreach( $p_var as $t_this_var ) { 234 if( helper_check_variables_equal( $t_this_var, $p_val, $p_strict ) ) { 235 echo ' checked="checked"'; 236 return; 237 } 238 } 239 } else { 240 if( helper_check_variables_equal( $p_var, $p_val, $p_strict ) ) { 241 echo ' checked="checked"'; 242 return; 243 } 244 } 245} 246 247/** 248 * Attach a "selected" attribute to a HTML element if $p_var === $p_val or 249 * a {value within an array passed via $p_var} === $p_val. 250 * 251 * If the second parameter is not given, the first parameter is compared to 252 * the boolean value true. 253 * 254 * @param mixed $p_var The variable to compare. 255 * @param mixed $p_val The value to compare $p_var with. 256 * @param boolean $p_strict Set to false to bypass strict type checking (defaults to true). 257 * @return void 258 */ 259function check_selected( $p_var, $p_val = true, $p_strict = true ) { 260 if( is_array( $p_var ) ) { 261 foreach ( $p_var as $t_this_var ) { 262 if( helper_check_variables_equal( $t_this_var, $p_val, $p_strict ) ) { 263 echo ' selected="selected"'; 264 return; 265 } 266 } 267 } else { 268 if( helper_check_variables_equal( $p_var, $p_val, $p_strict ) ) { 269 echo ' selected="selected"'; 270 } 271 } 272} 273 274/** 275 * If $p_val is true then we PRINT DISABLED to prevent selection of the 276 * current option list item 277 * 278 * @param boolean $p_val Whether to disable the current option value. 279 * @return void 280 */ 281function check_disabled( $p_val = true ) { 282 if( $p_val ) { 283 echo ' disabled="disabled" '; 284 } 285} 286 287/** 288 * Set up PHP for a long process execution 289 * The script timeout is set based on the value of the long_process_timeout config option. 290 * $p_ignore_abort specified whether to ignore user aborts by hitting 291 * the Stop button (the default is not to ignore user aborts) 292 * @param boolean $p_ignore_abort Whether to ignore user aborts from the web browser. 293 * @return integer 294 */ 295function helper_begin_long_process( $p_ignore_abort = false ) { 296 $t_timeout = config_get_global( 'long_process_timeout' ); 297 298 # silent errors or warnings reported when safe_mode is ON. 299 @set_time_limit( $t_timeout ); 300 301 ignore_user_abort( $p_ignore_abort ); 302 return $t_timeout; 303} 304 305# this allows pages to override the current project settings. 306# This typically applies to the view bug pages where the "current" 307# project as used by the filters, etc, does not match the bug being viewed. 308$g_project_override = null; 309$g_cache_current_project = null; 310 311/** 312 * Return the current project id as stored in a cookie 313 * If no cookie exists, the user's default project is returned 314 * @return integer 315 */ 316function helper_get_current_project() { 317 global $g_project_override, $g_cache_current_project; 318 319 if( $g_project_override !== null ) { 320 return $g_project_override; 321 } 322 323 if( $g_cache_current_project === null ) { 324 $t_cookie_name = config_get_global( 'project_cookie' ); 325 326 $t_project_id = gpc_get_cookie( $t_cookie_name, null ); 327 328 if( null === $t_project_id ) { 329 $t_pref = user_pref_get( auth_get_current_user_id(), ALL_PROJECTS ); 330 $t_project_id = $t_pref->default_project; 331 } else { 332 $t_project_id = explode( ';', $t_project_id ); 333 $t_project_id = $t_project_id[count( $t_project_id ) - 1]; 334 } 335 336 if( !project_exists( $t_project_id ) || ( 0 == project_get_field( $t_project_id, 'enabled' ) ) || !access_has_project_level( config_get( 'view_bug_threshold', null, null, $t_project_id ), $t_project_id ) ) { 337 $t_project_id = ALL_PROJECTS; 338 } 339 $g_cache_current_project = (int)$t_project_id; 340 } 341 return $g_cache_current_project; 342} 343 344/** 345 * Return the current project id as stored in a cookie, in an Array 346 * If no cookie exists, the user's default project is returned 347 * If the current project is a subproject, the return value will include 348 * any parent projects 349 * @return array 350 */ 351function helper_get_current_project_trace() { 352 $t_cookie_name = config_get_global( 'project_cookie' ); 353 354 $t_project_id = gpc_get_cookie( $t_cookie_name, null ); 355 356 if( null === $t_project_id ) { 357 $t_bottom = current_user_get_pref( 'default_project' ); 358 $t_parent = $t_bottom; 359 $t_project_id = array( 360 $t_bottom, 361 ); 362 363 while( true ) { 364 $t_parent = project_hierarchy_get_parent( $t_parent ); 365 if( 0 == $t_parent ) { 366 break; 367 } 368 array_unshift( $t_project_id, $t_parent ); 369 } 370 371 } else { 372 $t_project_id = explode( ';', $t_project_id ); 373 $t_bottom = $t_project_id[count( $t_project_id ) - 1]; 374 } 375 376 if( !project_exists( $t_bottom ) || ( 0 == project_get_field( $t_bottom, 'enabled' ) ) || !access_has_project_level( config_get( 'view_bug_threshold', null, null, $t_bottom ), $t_bottom ) ) { 377 $t_project_id = array( 378 ALL_PROJECTS, 379 ); 380 } 381 382 return $t_project_id; 383} 384 385/** 386 * Set the current project id (stored in a cookie) 387 * @param integer $p_project_id A valid project identifier. 388 * @return boolean always true 389 */ 390function helper_set_current_project( $p_project_id ) { 391 global $g_cache_current_project; 392 393 $t_project_cookie_name = config_get_global( 'project_cookie' ); 394 395 $g_cache_current_project = $p_project_id; 396 gpc_set_cookie( $t_project_cookie_name, $p_project_id, true ); 397 398 return true; 399} 400 401/** 402 * Clear all known user preference cookies 403 * @return void 404 */ 405function helper_clear_pref_cookies() { 406 gpc_clear_cookie( config_get_global( 'project_cookie' ) ); 407 gpc_clear_cookie( config_get( 'manage_users_cookie' ) ); 408 gpc_clear_cookie( config_get_global( 'manage_config_cookie' ) ); 409} 410 411/** 412 * Check whether the user has confirmed this action. 413 * 414 * If the user has not confirmed the action, generate a page which asks the user to confirm and 415 * then submits a form back to the current page with all the GET and POST data and an additional 416 * field called _confirmed to indicate that confirmation has been done. 417 * @param string $p_message Confirmation message to display to the end user. 418 * @param string $p_button_label Button label to display to the end user. 419 * @return boolean 420 */ 421function helper_ensure_confirmed( $p_message, $p_button_label ) { 422 if( true == gpc_get_bool( '_confirmed' ) ) { 423 return true; 424 } 425 426 layout_page_header(); 427 layout_page_begin(); 428 429 echo '<div class="col-md-12 col-xs-12">'; 430 echo '<div class="space-10"></div>'; 431 echo '<div class="alert alert-warning center">'; 432 echo '<p class="bigger-110">'; 433 echo "\n" . $p_message . "\n"; 434 echo '</p>'; 435 echo '<div class="space-10"></div>'; 436 437 echo '<form method="post" class="center" action="">' . "\n"; 438 # CSRF protection not required here - user needs to confirm action 439 # before the form is accepted. 440 print_hidden_inputs( $_POST ); 441 print_hidden_inputs( $_GET ); 442 443 echo '<input type="hidden" name="_confirmed" value="1" />' , "\n"; 444 echo '<input type="submit" class="btn btn-primary btn-white btn-round" value="' . $p_button_label . '" />'; 445 echo "\n</form>\n"; 446 447 echo '<div class="space-10"></div>'; 448 echo '</div></div>'; 449 450 layout_page_end(); 451 exit; 452} 453 454/** 455 * Call custom function. 456 * 457 * $p_function - Name of function to call (eg: do_stuff). The function will call custom_function_override_do_stuff() 458 * if found, otherwise, will call custom_function_default_do_stuff(). 459 * $p_args_array - Parameters to function as an array 460 * @param string $p_function Custom function name. 461 * @param array $p_args_array An array of arguments to pass to the custom function. 462 * @return mixed 463 */ 464function helper_call_custom_function( $p_function, array $p_args_array ) { 465 $t_function = 'custom_function_override_' . $p_function; 466 467 if( !function_exists( $t_function ) ) { 468 $t_function = 'custom_function_default_' . $p_function; 469 } 470 471 return call_user_func_array( $t_function, $p_args_array ); 472} 473 474/** 475 * return string to use in db queries containing projects of given user 476 * @param integer $p_project_id A valid project identifier. 477 * @param integer $p_user_id A valid user identifier. 478 * @return string 479 */ 480function helper_project_specific_where( $p_project_id, $p_user_id = null ) { 481 if( null === $p_user_id ) { 482 $p_user_id = auth_get_current_user_id(); 483 } 484 485 $t_project_ids = user_get_all_accessible_projects( $p_user_id, $p_project_id ); 486 487 if( 0 == count( $t_project_ids ) ) { 488 $t_project_filter = ' 1<>1'; 489 } else if( 1 == count( $t_project_ids ) ) { 490 $t_project_filter = ' project_id=' . reset( $t_project_ids ); 491 } else { 492 $t_project_filter = ' project_id IN (' . implode( ',', $t_project_ids ) . ')'; 493 } 494 495 return $t_project_filter; 496} 497 498/** 499 * Get array of columns for given target 500 * @param integer $p_columns_target Target view for the columns. 501 * @param boolean $p_viewable_only Whether to return viewable columns only. 502 * @param integer $p_user_id A valid user identifier. 503 * @return array 504 */ 505function helper_get_columns_to_view( $p_columns_target = COLUMNS_TARGET_VIEW_PAGE, $p_viewable_only = true, $p_user_id = null ) { 506 $t_columns = helper_call_custom_function( 'get_columns_to_view', array( $p_columns_target, $p_user_id ) ); 507 508 # Fix column names for custom field columns that may be stored as lowercase in configuration. See issue #17367 509 # If the system was working fine with lowercase names, then database is case-insensitive, eg: mysql 510 # Fix by forcing a search with current name to get the id, then get the actual name by looking up this id 511 foreach( $t_columns as &$t_column_name ) { 512 $t_cf_name = column_get_custom_field_name( $t_column_name ); 513 if( $t_cf_name ) { 514 $t_cf_id = custom_field_get_id_from_name( $t_cf_name ); 515 $t_column_name = column_get_custom_field_column_name( $t_cf_id ); 516 } 517 } 518 519 if( !$p_viewable_only ) { 520 return $t_columns; 521 } 522 523 $t_keys_to_remove = array(); 524 525 if( $p_columns_target == COLUMNS_TARGET_CSV_PAGE || $p_columns_target == COLUMNS_TARGET_EXCEL_PAGE ) { 526 $t_keys_to_remove[] = 'selection'; 527 $t_keys_to_remove[] = 'edit'; 528 $t_keys_to_remove[] = 'overdue'; 529 } 530 531 $t_current_project_id = helper_get_current_project(); 532 533 if( $t_current_project_id != ALL_PROJECTS && !access_has_project_level( config_get( 'view_handler_threshold' ), $t_current_project_id ) ) { 534 $t_keys_to_remove[] = 'handler_id'; 535 } 536 537 if( $t_current_project_id != ALL_PROJECTS && !access_has_project_level( config_get( 'roadmap_view_threshold' ), $t_current_project_id ) ) { 538 $t_keys_to_remove[] = 'target_version'; 539 } 540 541 foreach( $t_keys_to_remove as $t_key_to_remove ) { 542 $t_keys = array_keys( $t_columns, $t_key_to_remove ); 543 544 foreach( $t_keys as $t_key ) { 545 unset( $t_columns[$t_key] ); 546 } 547 } 548 549 # get the array values to remove gaps in the array which causes issue 550 # if the array is accessed using an index. 551 return array_values( $t_columns ); 552} 553 554/** 555 * if all projects selected, default to <prefix><username><suffix><extension>, otherwise default to 556 * <prefix><projectname><suffix><extension>. 557 * @param string $p_extension_with_dot File name Extension. 558 * @param string $p_prefix File name Prefix. 559 * @param string $p_suffix File name suffix. 560 * @return string 561 */ 562function helper_get_default_export_filename( $p_extension_with_dot, $p_prefix = '', $p_suffix = '' ) { 563 $t_filename = $p_prefix; 564 565 $t_current_project_id = helper_get_current_project(); 566 567 if( ALL_PROJECTS == $t_current_project_id ) { 568 $t_filename .= user_get_name( auth_get_current_user_id() ); 569 } else { 570 $t_filename .= project_get_field( $t_current_project_id, 'name' ); 571 } 572 573 return $t_filename . $p_suffix . $p_extension_with_dot; 574} 575 576/** 577 * returns a tab index value and increments it by one. This is used to give sequential tab index on a form. 578 * @return integer 579 */ 580function helper_get_tab_index_value() { 581 static $s_tab_index = 0; 582 return ++$s_tab_index; 583} 584 585/** 586 * returns a tab index and increments internal state by 1. This is used to give sequential tab index on 587 * a form. For example, this function returns: tabindex="1" 588 * @return string 589 */ 590function helper_get_tab_index() { 591 return 'tabindex="' . helper_get_tab_index_value() . '"'; 592} 593 594/** 595 * returns a boolean indicating whether SQL queries executed should be shown or not. 596 * @return boolean 597 */ 598function helper_log_to_page() { 599 # Check is authenticated before checking access level, otherwise user gets 600 # redirected to login_page.php. See #8461. 601 return config_get_global( 'log_destination' ) === 'page' && auth_is_user_authenticated() && access_has_global_level( config_get( 'show_log_threshold' ) ); 602} 603 604/** 605 * Return a URL relative to the web root, compatible with other applications 606 * @param string $p_url A relative URL to a page within Mantis. 607 * @return string 608 */ 609function helper_mantis_url( $p_url ) { 610 if( is_blank( $p_url ) ) { 611 return $p_url; 612 } 613 614 # Return URL as-is if it already starts with short path 615 $t_short_path = config_get_global( 'short_path' ); 616 if( strpos( $p_url, $t_short_path ) === 0 ) { 617 return $p_url; 618 } 619 620 return $t_short_path . $p_url; 621} 622 623/** 624 * convert a duration string in "[h]h:mm" to an integer (minutes) 625 * @param string $p_hhmm A string in [h]h:mm format to convert. 626 * @param string $p_field The field name. 627 * @return integer 628 */ 629function helper_duration_to_minutes( $p_hhmm, $p_field = 'hhmm' ) { 630 if( is_blank( $p_hhmm ) ) { 631 return 0; 632 } 633 634 $t_a = explode( ':', $p_hhmm ); 635 $t_min = 0; 636 637 # time can be composed of max 3 parts (hh:mm:ss) 638 if( count( $t_a ) > 3 ) { 639 throw new ClientException( 640 sprintf( "Invalid value '%s' for field '%s'.", $p_hhmm, $p_field ), 641 ERROR_INVALID_FIELD_VALUE, 642 array( $p_field ) 643 ); 644 } 645 646 $t_count = count( $t_a ); 647 for( $i = 0;$i < $t_count;$i++ ) { 648 # all time parts should be integers and non-negative. 649 if( !is_numeric( $t_a[$i] ) || ( (integer)$t_a[$i] < 0 ) ) { 650 throw new ClientException( 651 sprintf( "Invalid value '%s' for field '%s'.", $p_hhmm, $p_field ), 652 ERROR_INVALID_FIELD_VALUE, 653 array( $p_field ) 654 ); 655 } 656 657 # minutes and seconds are not allowed to exceed 59. 658 if( ( $i > 0 ) && ( $t_a[$i] > 59 ) ) { 659 throw new ClientException( 660 sprintf( "Invalid value '%s' for field '%s'.", $p_hhmm, $p_field ), 661 ERROR_INVALID_FIELD_VALUE, 662 array( $p_field ) 663 ); 664 } 665 } 666 667 switch( $t_count ) { 668 case 1: 669 $t_min = (integer)$t_a[0]; 670 break; 671 case 2: 672 $t_min = (integer)$t_a[0] * 60 + (integer)$t_a[1]; 673 break; 674 case 3: 675 # if seconds included, approximate it to minutes 676 $t_min = (integer)$t_a[0] * 60 + (integer)$t_a[1]; 677 678 if( (integer)$t_a[2] >= 30 ) { 679 $t_min++; 680 } 681 break; 682 } 683 684 return (int)$t_min; 685} 686 687/** 688 * Global shutdown functions registration 689 * Registers shutdown functions 690 * @return void 691 */ 692function shutdown_functions_register() { 693 register_shutdown_function( 'email_shutdown_function' ); 694} 695 696/** 697 * Filter a set of strings by finding strings that start with a case-insensitive prefix. 698 * @param array $p_set An array of strings to search through. 699 * @param string $p_prefix The prefix to filter by. 700 * @return array An array of strings which match the supplied prefix. 701 */ 702function helper_filter_by_prefix( array $p_set, $p_prefix ) { 703 $t_matches = array(); 704 foreach ( $p_set as $p_item ) { 705 if( mb_strtolower( mb_substr( $p_item, 0, mb_strlen( $p_prefix ) ) ) === mb_strtolower( $p_prefix ) ) { 706 $t_matches[] = $p_item; 707 } 708 } 709 return $t_matches; 710} 711 712/** 713 * Combine a Mantis page with a query string. This handles the case where the page is a native 714 * page or a plugin page. 715 * @param string $p_page The page (relative or full) 716 * @param string $p_query_string The query string 717 * @return string The combined url. 718 */ 719function helper_url_combine( $p_page, $p_query_string ) { 720 $t_url = $p_page; 721 722 if( !is_blank( $p_query_string ) ) { 723 if( stripos( $p_page, '?' ) !== false ) { 724 $t_url .= '&' . $p_query_string; 725 } else { 726 $t_url .= '?' . $p_query_string; 727 } 728 } 729 730 return $t_url; 731} 732 733/** 734 * Generate a hash to be used with dynamically generated content that is expected 735 * to be cached by the browser. This hash can be used to differentiate the generated 736 * content when it may be different based on some runtime attributes like: current user, 737 * project or language. 738 * An optional custom string can be provided to be added to the hash, for additional 739 * differentiating criteria, but this string must be already prepared by the caller. 740 * 741 * @param array $p_runtime_attrs Array of attributes to be calculated from current session. 742 * possible values: 'user', 'project', 'lang' 743 * @param string $p_custom_string Additional string provided by the caller 744 * @return string A hashed md5 string 745 */ 746function helper_generate_cache_key( array $p_runtime_attrs = [], $p_custom_string = '' ) { 747 # always add core version, to force reload of resources after an upgrade. 748 $t_key = $p_custom_string . '+V' . MANTIS_VERSION; 749 $t_user_auth = auth_is_user_authenticated(); 750 foreach( $p_runtime_attrs as $t_attr ) { 751 switch( $t_attr ) { 752 case 'user': 753 $t_key .= '+U' . ( $t_user_auth ? auth_get_current_user_id() : META_FILTER_NONE ); 754 break; 755 case 'project': 756 $t_key .= '+P' . ( $t_user_auth ? helper_get_current_project() : META_FILTER_NONE ); 757 break; 758 case 'lang': 759 $t_key .= '+L' . lang_get_current(); 760 break; 761 default: 762 trigger_error( ERROR_GENERIC, ERROR ); 763 } 764 } 765 return md5( $t_key ); 766} 767 768/** 769 * Parse view state from provided array. 770 * 771 * @param array $p_view_state The view state array (typically would have an id, name or both). 772 * @return integer view state id 773 * @throws ClientException if view state is invalid or array is empty. 774 */ 775function helper_parse_view_state( array $p_view_state ) { 776 $t_view_state_enum = config_get( 'view_state_enum_string' ); 777 778 $t_view_state_id = VS_PUBLIC; 779 if( isset( $p_view_state['id'] ) ) { 780 $t_enum_by_ids = MantisEnum::getAssocArrayIndexedByValues( $t_view_state_enum ); 781 $t_view_state_id = (int)$p_view_state['id']; 782 if( !isset( $t_enum_by_ids[$t_view_state_id] ) ) { 783 throw new ClientException( 784 sprintf( "Invalid view state id '%d'.", $t_view_state_id ), 785 ERROR_INVALID_FIELD_VALUE, 786 array( lang_get( 'view_state' ) ) ); 787 } 788 } else if( isset( $p_view_state['name' ] ) ) { 789 $t_enum_by_labels = MantisEnum::getAssocArrayIndexedByLabels( $t_view_state_enum ); 790 $t_name = $p_view_state['name']; 791 if( !isset( $t_enum_by_labels[$t_name] ) ) { 792 throw new ClientException( 793 sprintf( "Invalid view state id '%d'.", $t_view_state_id ), 794 ERROR_INVALID_FIELD_VALUE, 795 array( lang_get( 'view_state' ) ) ); 796 } 797 798 $t_view_state_id = $t_enum_by_labels[$t_name]; 799 } else { 800 throw new ClientException( 801 "Empty view state", 802 ERROR_EMPTY_FIELD ); 803 } 804 805 return $t_view_state_id; 806} 807 808/** 809 * Parse numeric positive id. 810 * 811 * @param string $p_id The id to parse. 812 * @param string $p_field_name The field name. 813 * @return integer The parsed id. 814 * @throws ClientException Id is not specified or invalid. 815 */ 816function helper_parse_id( $p_id, $p_field_name ) { 817 $t_id = trim( $p_id ); 818 if( !is_numeric( $t_id ) ) { 819 if( empty( $t_id ) ) { 820 throw new ClientException( "'$p_field_name' missing", ERROR_GPC_VAR_NOT_FOUND, array( $p_field_name ) ); 821 } 822 823 throw new ClientException( "'$p_field_name' must be numeric", ERROR_INVALID_FIELD_VALUE, array( $p_field_name ) ); 824 } 825 826 $t_id = (int)$t_id; 827 if( $t_id < 1 ) { 828 throw new ClientException( "'$p_field_name' must be >= 1", ERROR_INVALID_FIELD_VALUE, array( $p_field_name ) ); 829 } 830 831 return $t_id; 832} 833 834/** 835 * Parse issue id. 836 * 837 * @param string $p_issue_id The id to parse. 838 * @param string $p_field_name The field name. 839 * @return integer The issue id. 840 * @throws ClientException Issue is not specified or invalid. 841 */ 842function helper_parse_issue_id( $p_issue_id, $p_field_name = 'issue_id' ) { 843 return helper_parse_id( $p_issue_id, $p_field_name ); 844} 845