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 * String Processing API
19 *
20 * @package CoreAPI
21 * @subpackage StringProcessingAPI
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 bug_api.php
29 * @uses bugnote_api.php
30 * @uses config_api.php
31 * @uses constant_inc.php
32 * @uses email_api.php
33 * @uses event_api.php
34 * @uses helper_api.php
35 * @uses lang_api.php
36 * @uses user_api.php
37 * @uses utility_api.php
38 */
39
40require_api( 'access_api.php' );
41require_api( 'authentication_api.php' );
42require_api( 'bug_api.php' );
43require_api( 'bugnote_api.php' );
44require_api( 'config_api.php' );
45require_api( 'constant_inc.php' );
46require_api( 'email_api.php' );
47require_api( 'event_api.php' );
48require_api( 'helper_api.php' );
49require_api( 'lang_api.php' );
50require_api( 'user_api.php' );
51require_api( 'utility_api.php' );
52
53$g_cache_html_valid_tags = '';
54$g_cache_html_valid_tags_single_line = '';
55
56/**
57 * Preserve spaces at beginning of lines.
58 * Lines must be separated by \n rather than <br />
59 * @param string $p_string String to be processed.
60 * @return string
61 */
62function string_preserve_spaces_at_bol( $p_string ) {
63	$t_lines = explode( "\n", $p_string );
64	$t_line_count = count( $t_lines );
65	for( $i = 0;$i < $t_line_count;$i++ ) {
66		$t_count = 0;
67		$t_prefix = '';
68
69		$t_char = mb_substr( $t_lines[$i], $t_count, 1 );
70		$t_spaces = 0;
71		while( ( $t_char == ' ' ) || ( $t_char == "\t" ) ) {
72			if( $t_char == ' ' ) {
73				$t_spaces++;
74			} else {
75				$t_spaces += 4;
76			}
77
78			# 1 tab = 4 spaces, can be configurable.
79
80			$t_count++;
81			$t_char = mb_substr( $t_lines[$i], $t_count, 1 );
82		}
83
84		for( $j = 0;$j < $t_spaces;$j++ ) {
85			$t_prefix .= '&#160;';
86		}
87
88		$t_lines[$i] = $t_prefix . mb_substr( $t_lines[$i], $t_count );
89	}
90	return implode( "\n", $t_lines );
91}
92
93/**
94 * Prepare a string to be printed without being broken into multiple lines
95 * @param string $p_string String to be processed.
96 * @return string
97 */
98function string_no_break( $p_string ) {
99	if( strpos( $p_string, ' ' ) !== false ) {
100		return '<span class="nowrap">' . $p_string . '</span>';
101	} else {
102		return $p_string;
103	}
104}
105
106/**
107 * Similar to nl2br, but fixes up a problem where new lines are doubled between
108 * html pre tags.
109 * additionally, wrap the text an $p_wrap character intervals if the config is set
110 * @param string  $p_string String to be processed.
111 * @param integer $p_wrap   Number of characters to wrap text at.
112 * @return string
113 */
114function string_nl2br( $p_string, $p_wrap = 100 ) {
115	$t_pieces = preg_split( '/(<pre[^>]*>.*?<\/pre>)/is', $p_string, -1, PREG_SPLIT_DELIM_CAPTURE );
116	if( isset( $t_pieces[1] ) ) {
117		$t_output = '';
118		foreach( $t_pieces as $t_piece ) {
119			if( preg_match( '/(<pre[^>]*>.*?<\/pre>)/is', $t_piece ) ) {
120				$t_piece = preg_replace( '/<br[^>]*?>/', '', $t_piece );
121
122				# @@@ thraxisp - this may want to be replaced by html_entity_decode (or equivalent)
123				#     if other encoded characters are a problem
124				$t_piece = preg_replace( '/&#160;/', ' ', $t_piece );
125				if( ON == config_get( 'wrap_in_preformatted_text' ) ) {
126					# Use PCRE_UTF8 modifier to ensure a correct char count
127					$t_output .= preg_replace( '/([^\n]{' . $p_wrap . ',}?[\s]+)(?!<\/pre>)/u', "$1", $t_piece );
128				} else {
129					$t_output .= $t_piece;
130				}
131			} else {
132				$t_output .= nl2br( $t_piece );
133			}
134		}
135		return $t_output;
136	} else {
137		return nl2br( $p_string );
138	}
139}
140
141/**
142 * Prepare a multiple line string for display to HTML
143 * @param string $p_string String to be processed.
144 * @return string
145 */
146function string_display( $p_string ) {
147	return event_signal( 'EVENT_DISPLAY_TEXT', $p_string, true );
148}
149
150/**
151 * Prepare a single line string for display to HTML
152 * @param string $p_string String to be processed.
153 * @return string
154 */
155function string_display_line( $p_string ) {
156	return event_signal( 'EVENT_DISPLAY_TEXT', $p_string, false );
157}
158
159/**
160 * Prepare a string for display to HTML and add href anchors for URLs, emails
161 * and bug references
162 * @param string $p_string String to be processed.
163 * @return string
164 */
165function string_display_links( $p_string ) {
166	return event_signal( 'EVENT_DISPLAY_FORMATTED', $p_string, true );
167}
168
169/**
170 * Prepare a single line string for display to HTML and add href anchors for
171 * URLs, emails and bug references
172 * @param string $p_string String to be processed.
173 * @return string
174 */
175function string_display_line_links( $p_string ) {
176	return event_signal( 'EVENT_DISPLAY_FORMATTED', $p_string, false );
177}
178
179/**
180 * Prepare a string for display in rss
181 * @param string $p_string String to be processed.
182 * @return string
183 */
184function string_rss_links( $p_string ) {
185	# rss can not start with &#160; which spaces will be replaced into by string_display().
186	$t_string = trim( $p_string );
187
188	$t_string = event_signal( 'EVENT_DISPLAY_RSS', $t_string );
189
190	# another escaping to escape the special characters created by the generated links
191	return string_html_specialchars( $t_string );
192}
193
194/**
195 * Prepare a string for plain text display in email
196 * @param string $p_string String to be processed.
197 * @return string
198 */
199function string_email( $p_string ) {
200	return string_strip_hrefs( $p_string );
201}
202
203/**
204 * Prepare a string for plain text display in email and add URLs for bug
205 * links
206 * @param string $p_string String to be processed.
207 * @return string
208 */
209function string_email_links( $p_string ) {
210	return event_signal( 'EVENT_DISPLAY_EMAIL', $p_string );
211}
212
213/**
214 * Process a string for display in a textarea box
215 * @param string $p_string String to be processed.
216 * @return string
217 */
218function string_textarea( $p_string ) {
219	return string_html_specialchars( $p_string );
220}
221
222/**
223 * Process a string for display in a text box
224 * @param string $p_string String to be processed.
225 * @return string
226 */
227function string_attribute( $p_string ) {
228	return string_html_specialchars( $p_string );
229}
230
231/**
232 * Process a string for inclusion in a URL as a GET parameter
233 * @param string $p_string String to be processed.
234 * @return string
235 */
236function string_url( $p_string ) {
237	return rawurlencode( $p_string );
238}
239
240/**
241 * validate the url as part of this site before continuing
242 * @param string  $p_url             URL to be processed.
243 * @param boolean $p_return_absolute Whether to return the absolute URL to this Mantis instance.
244 * @return string
245 */
246function string_sanitize_url( $p_url, $p_return_absolute = false ) {
247	$t_url = strip_tags( urldecode( $p_url ) );
248
249	$t_path = rtrim( config_get_global( 'path' ), '/' );
250	$t_short_path = rtrim( config_get_global( 'short_path' ), '/' );
251
252	$t_pattern = '(?:/*(?P<script>[^\?#]*))(?:\?(?P<query>[^#]*))?(?:#(?P<anchor>[^#]*))?';
253
254	# Break the given URL into pieces for path, script, query, and anchor
255	$t_type = 0;
256	if( preg_match( '@^(?P<path>' . preg_quote( $t_path, '@' ) . ')' . $t_pattern . '$@', $t_url, $t_matches ) ) {
257		$t_type = 1;
258	} else if( !empty( $t_short_path )
259			&& preg_match( '@^(?P<path>' . preg_quote( $t_short_path, '@' ) . ')' . $t_pattern . '$@', $t_url, $t_matches )
260	) {
261		$t_type = 2;
262	} else if( preg_match( '@^(?P<path>)' . $t_pattern . '$@', $t_url, $t_matches ) ) {
263		$t_type = 3;
264	}
265
266	# Check for URL's pointing to other domains
267	if( 0 == $t_type || empty( $t_matches['script'] ) ||
268		3 == $t_type && preg_match( '@(?:[^:]*)?:/*@', $t_url ) > 0 ) {
269
270		return ( $p_return_absolute ? $t_path . '/' : '' ) . 'index.php';
271	}
272
273	# Start extracting regex matches
274	# Encode backslashes to prevent unwanted escaping of a leading '/' allowing
275	# redirection to external sites
276	$t_script = strtr( $t_matches['script'], array( '\\' => '%5C' ) );
277	$t_script_path = $t_matches['path'];
278
279	# Clean/encode query params
280	$t_query = '';
281	if( isset( $t_matches['query'] ) ) {
282		$t_pairs = array();
283		parse_str( html_entity_decode( $t_matches['query'] ), $t_pairs );
284
285		$t_clean_pairs = array();
286		foreach( $t_pairs as $t_key => $t_value ) {
287			if( is_array( $t_value ) ) {
288				foreach( $t_value as $t_value_each ) {
289					$t_clean_pairs[] .= rawurlencode( $t_key ) . '[]=' . rawurlencode( $t_value_each );
290				}
291			} else {
292				$t_clean_pairs[] = rawurlencode( $t_key ) . '=' . rawurlencode( $t_value );
293			}
294		}
295
296		if( !empty( $t_clean_pairs ) ) {
297			$t_query = '?' . implode( '&', $t_clean_pairs );
298		}
299	}
300
301	# encode link anchor
302	$t_anchor = '';
303	if( isset( $t_matches['anchor'] ) ) {
304		$t_anchor = '#' . rawurlencode( $t_matches['anchor'] );
305	}
306
307	# Return an appropriate re-combined URL string
308	if( $p_return_absolute ) {
309		return $t_path . '/' . $t_script . $t_query . $t_anchor;
310	} else {
311		return ( !empty( $t_script_path ) ? $t_script_path . '/' : '' ) . $t_script . $t_query . $t_anchor;
312	}
313}
314
315/**
316 * Process $p_string, looking for bug ID references and creating bug view
317 * links for them.
318 *
319 * Returns the processed string.
320 *
321 * If $p_include_anchor is true, include the href tag, otherwise just insert
322 * the URL
323 *
324 * The bug tag ('#' by default) must be at the beginning of the string or
325 * preceded by a character that is not a letter, a number or an underscore
326 *
327 * if $p_include_anchor = false, $p_fqdn is ignored and assumed to true.
328 * @param string  $p_string         String to be processed.
329 * @param boolean $p_include_anchor Whether to include the href tag or just the URL.
330 * @param boolean $p_detail_info    Whether to include more detailed information (e.g. title attribute / project) in the returned string.
331 * @param boolean $p_fqdn           Whether to return an absolute or relative link.
332 * @return string
333 */
334function string_process_bug_link( $p_string, $p_include_anchor = true, $p_detail_info = true, $p_fqdn = false ) {
335	static $s_bug_link_callback = array();
336
337	$t_tag = config_get( 'bug_link_tag' );
338
339	# bail if the link tag is blank
340	if( '' == $t_tag || $p_string == '' ) {
341		return $p_string;
342	}
343
344	if( !isset( $s_bug_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] ) ) {
345		if( $p_include_anchor ) {
346			$s_bug_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] =
347				function( $p_array ) use( $p_detail_info, $p_fqdn ) {
348					$c_bug_id = (int)$p_array[2];
349					if( bug_exists( $c_bug_id ) ) {
350						$t_project_id = bug_get_field( $c_bug_id, 'project_id' );
351						$t_view_bug_threshold = config_get( 'view_bug_threshold', null, null, $t_project_id );
352						if( access_has_bug_level( $t_view_bug_threshold, $c_bug_id ) ) {
353							return $p_array[1] .
354								string_get_bug_view_link(
355									$c_bug_id,
356									(boolean)$p_detail_info,
357									(boolean)$p_fqdn
358								);
359						}
360					}
361					return $p_array[0];
362				}; # end of bug link callback closure
363		} else {
364			$s_bug_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] =
365				function( $p_array ) {
366					$c_bug_id = (int)$p_array[2];
367					if( bug_exists( $c_bug_id ) ) {
368						# Create link regardless of user's access to the bug
369						return $p_array[1] .
370							string_get_bug_view_url_with_fqdn( $c_bug_id );
371					}
372					return $p_array[0];
373				}; # end of bug link callback closure
374		}
375	}
376
377	$p_string = preg_replace_callback(
378		'/(^|[^\w&])' . preg_quote( $t_tag, '/' ) . '(\d+)\b/',
379		$s_bug_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn],
380		$p_string
381	);
382	return $p_string;
383}
384
385/**
386 * Process $p_string, looking for bugnote ID references and creating bug view
387 * links for them.
388 *
389 * Returns the processed string.
390 *
391 * If $p_include_anchor is true, include the href tag, otherwise just insert
392 * the URL
393 *
394 * The bugnote tag ('~' by default) must be at the beginning of the string or
395 * preceded by a character that is not a letter, a number or an underscore
396 *
397 * if $p_include_anchor = false, $p_fqdn is ignored and assumed to true.
398 * @param string  $p_string         String to be processed.
399 * @param boolean $p_include_anchor Whether to include the href tag or just the URL.
400 * @param boolean $p_detail_info    Whether to include more detailed information (e.g. title attribute / project) in the returned string.
401 * @param boolean $p_fqdn           Whether to return an absolute or relative link.
402 * @return string
403 */
404function string_process_bugnote_link( $p_string, $p_include_anchor = true, $p_detail_info = true, $p_fqdn = false ) {
405	static $s_bugnote_link_callback = array();
406
407	$t_tag = config_get( 'bugnote_link_tag' );
408
409	# bail if the link tag is blank
410	if( '' == $t_tag || $p_string == '' ) {
411		return $p_string;
412	}
413
414	if( !isset( $s_bugnote_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] ) ) {
415		if( $p_include_anchor ) {
416			$s_bugnote_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] =
417				function( $p_array ) use( $p_detail_info, $p_fqdn ) {
418					global $g_project_override;
419					$c_bugnote_id = (int)$p_array[2];
420					if( bugnote_exists( $c_bugnote_id ) ) {
421						$t_bug_id = bugnote_get_field( $c_bugnote_id, 'bug_id' );
422						if( bug_exists( $t_bug_id ) ) {
423							$g_project_override = bug_get_field( $t_bug_id, 'project_id' );
424							if(   access_compare_level(
425										user_get_access_level( auth_get_current_user_id(),
426										bug_get_field( $t_bug_id, 'project_id' ) ),
427										config_get( 'private_bugnote_threshold' )
428								   )
429								|| bugnote_get_field( $c_bugnote_id, 'reporter_id' ) == auth_get_current_user_id()
430								|| bugnote_get_field( $c_bugnote_id, 'view_state' ) == VS_PUBLIC
431							) {
432								$g_project_override = null;
433								return $p_array[1] .
434									string_get_bugnote_view_link(
435										$t_bug_id,
436										$c_bugnote_id,
437										(boolean)$p_detail_info,
438										(boolean)$p_fqdn
439									);
440							}
441							$g_project_override = null;
442						}
443					}
444					return $p_array[0];
445				}; # end of bugnote link callback closure
446		} else {
447			$s_bugnote_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn] =
448				function( $p_array ) {
449					$c_bugnote_id = (int)$p_array[2];
450					if( bugnote_exists( $c_bugnote_id ) ) {
451						$t_bug_id = bugnote_get_field( $c_bugnote_id, 'bug_id' );
452						if( $t_bug_id && bug_exists( $t_bug_id ) ) {
453							return $p_array[1] .
454								string_get_bugnote_view_url_with_fqdn( $t_bug_id, $c_bugnote_id );
455						}
456					}
457					return $p_array[0];
458				}; # end of bugnote link callback closure
459		}
460	}
461	$p_string = preg_replace_callback(
462		'/(^|[^\w])' . preg_quote( $t_tag, '/' ) . '(\d+)\b/',
463		$s_bugnote_link_callback[$p_include_anchor][$p_detail_info][$p_fqdn],
464		$p_string
465	);
466	return $p_string;
467}
468
469/**
470 * Search email addresses and URLs for a few common protocols in the given
471 * string, and replace occurrences with href anchors.
472 * @param string $p_string String to be processed.
473 * @return string
474 */
475function string_insert_hrefs( $p_string ) {
476	static $s_url_regex = null;
477	static $s_email_regex = null;
478
479	if( !config_get( 'html_make_links' ) ) {
480		return $p_string;
481	}
482
483	# Initialize static variables
484	if( is_null( $s_url_regex ) ) {
485		# URL protocol. The regex accepts a small subset from the list of valid
486		# IANA permanent and provisional schemes defined in
487		# http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
488		$t_url_protocol = '(?:https?|s?ftp|file|irc[6s]?|ssh|telnet|nntp|git|svn(?:\+ssh)?|cvs):\/\/';
489
490		# %2A notation in url's
491		$t_url_hex = '%[[:digit:]A-Fa-f]{2}';
492
493		# valid set of characters that may occur in url scheme. Note: - should be first (A-F != -AF).
494		$t_url_valid_chars       = '-_.,!~*\';\/?%^\\\\:@&={\|}+$#[:alnum:]\pL';
495		$t_url_chars             = "(?:${t_url_hex}|[${t_url_valid_chars}\(\)\[\]])";
496		$t_url_chars2            = "(?:${t_url_hex}|[${t_url_valid_chars}])";
497		$t_url_chars_in_brackets = "(?:${t_url_hex}|[${t_url_valid_chars}\(\)])";
498		$t_url_chars_in_parens   = "(?:${t_url_hex}|[${t_url_valid_chars}\[\]])";
499
500		$t_url_part1 = $t_url_chars;
501		$t_url_part2 = "(?:\(${t_url_chars_in_parens}*\)|\[${t_url_chars_in_brackets}*\]|${t_url_chars2})";
502
503		$s_url_regex = "/(${t_url_protocol}(${t_url_part1}*?${t_url_part2}+))/su";
504
505		# e-mail regex
506		$s_email_regex = substr_replace( email_regex_simple(), '(?:mailto:)?', 1, 0 );
507	}
508
509	# Find any URL in a string and replace it with a clickable link
510	$p_string = preg_replace_callback(
511		$s_url_regex,
512		function ( $p_match ) {
513			$t_url_href = 'href="' . rtrim( $p_match[1], '.' ) . '"';
514			if( config_get( 'html_make_links' ) == LINKS_NEW_WINDOW ) {
515				$t_url_target = ' target="_blank"';
516			} else {
517				$t_url_target = '';
518			}
519			return "<a ${t_url_href}${t_url_target}>${p_match[1]}</a>";
520		},
521		$p_string
522	);
523
524	# Find any email addresses in the string and replace them with a clickable
525	# mailto: link, making sure that we skip processing of any existing anchor
526	# tags, to avoid parts of URLs such as https://user@example.com/ or
527	# http://user:password@example.com/ to be not treated as an email.
528	$p_string = string_process_exclude_anchors(
529		$p_string,
530		function( $p_string ) use ( $s_email_regex ) {
531			return preg_replace( $s_email_regex, '<a href="mailto:\0">\0</a>', $p_string );
532		}
533	);
534
535	return $p_string;
536}
537
538/**
539 * Processes a string, ignoring anchor tags.
540 * Applies the specified callback function to the text between anchor tags;
541 * the anchors themselves will be left as-is.
542 * @param string   $p_string   String to process
543 * @param callable $p_callback Function to apply
544 * @return string
545 */
546function string_process_exclude_anchors( $p_string, $p_callback ) {
547	static $s_anchor_regex = '/(<a[^>]*>.*?<\/a>)/is';
548
549	$t_pieces = preg_split( $s_anchor_regex, $p_string, null, PREG_SPLIT_DELIM_CAPTURE );
550
551	$t_string = '';
552	foreach( $t_pieces as $t_piece ) {
553		if( preg_match( $s_anchor_regex, $t_piece ) ) {
554			$t_string .= $t_piece;
555		} else {
556			$t_string .= $p_callback( $t_piece );
557		}
558	}
559
560	return $t_string;
561}
562
563/**
564 * Detect href anchors in the string and replace them with URLs and email addresses
565 * @param string $p_string String to be processed.
566 * @return string
567 */
568function string_strip_hrefs( $p_string ) {
569	# First grab mailto: hrefs.  We don't care whether the URL is actually
570	# correct - just that it's inside an href attribute.
571	$p_string = preg_replace( '/<a\s[^\>]*href="mailto:([^\"]+)"[^\>]*>[^\<]*<\/a>/si', '\1', $p_string );
572
573	# Then grab any other href
574	$p_string = preg_replace( '/<a\s[^\>]*href="([^\"]+)"[^\>]*>[^\<]*<\/a>/si', '\1', $p_string );
575	return $p_string;
576}
577
578/**
579 * This function looks for text with htmlentities
580 * like &lt;b&gt; and converts it into the corresponding
581 * html < b > tag based on the configuration information
582 * @param string  $p_string    String to be processed.
583 * @param boolean $p_multiline Whether the string being processed is a multi-line string.
584 * @return string
585 */
586function string_restore_valid_html_tags( $p_string, $p_multiline = true ) {
587	global $g_cache_html_valid_tags_single_line, $g_cache_html_valid_tags;
588
589	if( is_blank( ( $p_multiline ? $g_cache_html_valid_tags : $g_cache_html_valid_tags_single_line ) ) ) {
590		$t_html_valid_tags = config_get_global( $p_multiline ? 'html_valid_tags' : 'html_valid_tags_single_line' );
591
592		if( OFF === $t_html_valid_tags || is_blank( $t_html_valid_tags ) ) {
593			return $p_string;
594		}
595
596		$t_tags = explode( ',', $t_html_valid_tags );
597		foreach( $t_tags as $t_key => $t_value ) {
598			if( !is_blank( $t_value ) ) {
599				$t_tags[$t_key] = trim( $t_value );
600			}
601		}
602		$t_tags = implode( '|', $t_tags );
603		if( $p_multiline ) {
604			$g_cache_html_valid_tags = $t_tags;
605		} else {
606			$g_cache_html_valid_tags_single_line = $t_tags;
607		}
608	} else {
609		$t_tags = ( $p_multiline ? $g_cache_html_valid_tags : $g_cache_html_valid_tags_single_line );
610	}
611
612	$p_string = preg_replace( '/&lt;(' . $t_tags . ')\s*&gt;/ui', '<\\1>', $p_string );
613	$p_string = preg_replace( '/&lt;\/(' . $t_tags . ')\s*&gt;/ui', '</\\1>', $p_string );
614	return preg_replace( '/&lt;(' . $t_tags . ')\s*\/&gt;/ui', '<\\1 />', $p_string );
615}
616
617/**
618 * return the name of a bug page
619 * $p_action should be something like 'view', 'update', or 'report'
620 * @param string  $p_action  A valid action being performed - currently one of view, update or report.
621 * @return string
622 */
623function string_get_bug_page( $p_action ) {
624	switch( $p_action ) {
625		case 'view':
626			return 'bug_view_page.php';
627		case 'update':
628			return 'bug_update_page.php';
629		case 'report':
630			return 'bug_report_page.php';
631	}
632
633	trigger_error( ERROR_GENERIC, ERROR );
634}
635
636/**
637 * return an href anchor that links to a bug VIEW page for the given bug
638 * @param integer $p_bug_id	     A bug identifier.
639 * @param boolean $p_detail_info Whether to include more detailed information (e.g. title attribute / project) in the returned string.
640 * @param boolean $p_fqdn        Whether to return an absolute or relative link.
641 * @return string
642 */
643function string_get_bug_view_link( $p_bug_id, $p_detail_info = true, $p_fqdn = false ) {
644	if( bug_exists( $p_bug_id ) ) {
645		$t_link = '<a href="';
646		if( $p_fqdn ) {
647			$t_link .= config_get_global( 'path' );
648		} else {
649			$t_link .= config_get_global( 'short_path' );
650		}
651		$t_link .= string_get_bug_view_url( $p_bug_id ) . '"';
652		if( $p_detail_info ) {
653			$t_summary = string_attribute( bug_get_field( $p_bug_id, 'summary' ) );
654			$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
655			$t_status = string_attribute( get_enum_element( 'status', bug_get_field( $p_bug_id, 'status' ), $t_project_id ) );
656			$t_link .= ' title="[' . $t_status . '] ' . $t_summary . '"';
657
658			$t_resolved = bug_get_field( $p_bug_id, 'status' ) >= config_get( 'bug_resolved_status_threshold', null, null, $t_project_id );
659			if( $t_resolved ) {
660				$t_link .= ' class="resolved"';
661			}
662		}
663		$t_link .= '>' . bug_format_id( $p_bug_id ) . '</a>';
664	} else {
665		$t_link = bug_format_id( $p_bug_id );
666	}
667
668	return $t_link;
669}
670
671/**
672 * return an href anchor that links to a bug VIEW page for the given bug
673 * @param integer $p_bug_id      A bug identifier.
674 * @param integer $p_bugnote_id  A bugnote identifier.
675 * @param boolean $p_detail_info Whether to include more detailed information (e.g. title attribute / project) in the returned string.
676 * @param boolean $p_fqdn        Whether to return an absolute or relative link.
677 * @return string
678 */
679function string_get_bugnote_view_link( $p_bug_id, $p_bugnote_id, $p_detail_info = true, $p_fqdn = false ) {
680	$t_bug_id = (int)$p_bug_id;
681
682	if( bug_exists( $t_bug_id ) && bugnote_exists( $p_bugnote_id ) ) {
683		$t_link = '<a href="';
684		if( $p_fqdn ) {
685			$t_link .= config_get_global( 'path' );
686		} else {
687			$t_link .= config_get_global( 'short_path' );
688		}
689
690		$t_link .= string_get_bugnote_view_url( $p_bug_id, $p_bugnote_id ) . '"';
691		if( $p_detail_info ) {
692			$t_reporter = string_attribute( user_get_name( bugnote_get_field( $p_bugnote_id, 'reporter_id' ) ) );
693			$t_update_date = string_attribute( date( config_get( 'normal_date_format' ), ( bugnote_get_field( $p_bugnote_id, 'last_modified' ) ) ) );
694			$t_link .= ' title="' . bug_format_id( $t_bug_id ) . ': [' . $t_update_date . '] ' . $t_reporter . '"';
695		}
696
697		$t_link .= '>' . bug_format_id( $t_bug_id ) . ':' . bugnote_format_id( $p_bugnote_id ) . '</a>';
698	} else {
699		$t_link = bugnote_format_id( $t_bug_id ) . ':' . bugnote_format_id( $p_bugnote_id );
700	}
701
702	return $t_link;
703}
704
705/**
706 * return the name and GET parameters of a bug VIEW page for the given bug
707 * @param integer $p_bug_id A bug identifier.
708 * @return string
709 */
710function string_get_bug_view_url( $p_bug_id ) {
711	return 'view.php?id=' . $p_bug_id;
712}
713
714/**
715 * return the name and GET parameters of a bug VIEW page for the given bug
716 * @param integer $p_bug_id     A bug identifier.
717 * @param integer $p_bugnote_id A bugnote identifier.
718 * @return string
719 */
720function string_get_bugnote_view_url( $p_bug_id, $p_bugnote_id ) {
721	return 'view.php?id=' . $p_bug_id . '#c' . $p_bugnote_id;
722}
723
724/**
725 * return the name and GET parameters of a bug VIEW page for the given bug
726 * account for the user preference and site override
727 * The returned url includes the fully qualified domain, hence it is suitable to be included
728 * in emails.
729 * @param integer $p_bug_id     A bug identifier.
730 * @param integer $p_bugnote_id A bug note identifier.
731 * @return string
732 */
733function string_get_bugnote_view_url_with_fqdn( $p_bug_id, $p_bugnote_id ) {
734	return config_get_global( 'path' ) . string_get_bug_view_url( $p_bug_id ) . '#c' . $p_bugnote_id;
735}
736
737/**
738 * return the name and GET parameters of a bug VIEW page for the given bug
739 * account for the user preference and site override
740 * The returned url includes the fully qualified domain, hence it is suitable to be included in emails.
741 * @param integer $p_bug_id  A bug identifier.
742 * @return string
743 */
744function string_get_bug_view_url_with_fqdn( $p_bug_id ) {
745	return config_get_global( 'path' ) . string_get_bug_view_url( $p_bug_id );
746}
747
748/**
749 * return an href anchor that links to a bug UPDATE page for the given bug
750 * @param integer $p_bug_id  A bug identifier.
751 * @return string
752 */
753function string_get_bug_update_link( $p_bug_id ) {
754	$t_summary = string_attribute( bug_get_field( $p_bug_id, 'summary' ) );
755	return '<a href="' . helper_mantis_url( string_get_bug_update_url( $p_bug_id ) ) . '" title="' . $t_summary . '">' . bug_format_id( $p_bug_id ) . '</a>';
756}
757
758/**
759 * return the name and GET parameters of a bug UPDATE page
760 * @param integer $p_bug_id  A bug identifier.
761 * @return string
762 */
763function string_get_bug_update_url( $p_bug_id ) {
764	return string_get_bug_update_page() . '?bug_id=' . $p_bug_id;
765}
766
767/**
768 * return the name of a bug UPDATE page
769 * @return string
770 */
771function string_get_bug_update_page() {
772	return string_get_bug_page( 'update' );
773}
774
775/**
776 * return an href anchor that links to a bug REPORT page
777 * @return string
778 */
779function string_get_bug_report_link() {
780	return '<a href="' . helper_mantis_url( string_get_bug_report_url() ) . '">' . lang_get( 'report_bug_link' ) . '</a>';
781}
782
783/**
784 * return the name of a bug REPORT page
785 * @return string
786 */
787function string_get_bug_report_url() {
788	return string_get_bug_page( 'report' );
789}
790
791/**
792 * return the complete URL link to the verify page including the confirmation hash
793 * @param integer $p_user_id      A valid user identifier.
794 * @param string  $p_confirm_hash The confirmation hash value to include in the link.
795 * @return string
796 */
797function string_get_confirm_hash_url( $p_user_id, $p_confirm_hash ) {
798	return config_get_global( 'path' ) . 'verify.php?id=' . string_url( $p_user_id ) . '&confirm_hash=' . string_url( $p_confirm_hash );
799}
800
801/**
802 * Format date for display
803 * @param integer $p_date A date value to process.
804 * @return string
805 */
806function string_format_complete_date( $p_date ) {
807	return date( config_get( 'complete_date_format' ), $p_date );
808}
809
810/**
811 * Shorten a string for display on a dropdown to prevent the page rendering too wide
812 * ref issues #4630, #5072, #5131
813 * @param string  $p_string The string to process.
814 * @param integer $p_max    The maximum length of the string to use.
815 *                          If not set, defaults to max_dropdown_length configuration variable.
816 * @return string
817 */
818function string_shorten( $p_string, $p_max = null ) {
819	if( $p_max === null ) {
820		$t_max = config_get( 'max_dropdown_length' );
821	} else {
822		$t_max = (int)$p_max;
823	}
824
825	if( ( $t_max > 0 ) && ( mb_strlen( $p_string ) > $t_max ) ) {
826		$t_pattern = '/([\s|.|,|\-|_|\/|\?]+)/';
827		$t_bits = preg_split( $t_pattern, $p_string, -1, PREG_SPLIT_DELIM_CAPTURE );
828
829		$t_string = '';
830		$t_last = $t_bits[count( $t_bits ) - 1];
831		$t_last_len = strlen( $t_last );
832
833		if( count( $t_bits ) == 1 ) {
834			$t_string .= mb_substr( $t_last, 0, $t_max - 3 );
835			$t_string .= '...';
836		} else {
837			foreach( $t_bits as $t_bit ) {
838				if( ( mb_strlen( $t_string ) + mb_strlen( $t_bit ) + $t_last_len + 3 <= $t_max ) || ( strpos( $t_bit, '.,-/?' ) > 0 ) ) {
839					$t_string .= $t_bit;
840				} else {
841					break;
842				}
843			}
844			$t_string .= '...' . $t_last;
845		}
846		return $t_string;
847	} else {
848		return $p_string;
849	}
850}
851
852/**
853 * Normalize a string by removing leading, trailing and excessive internal spaces
854 * note a space is used as the pattern instead of '\s' to make it work with UTF-8 strings
855 * @param string $p_string The string to process.
856 * @return string
857 */
858function string_normalize( $p_string ) {
859	return preg_replace( '/ +/', ' ', trim( $p_string ) );
860}
861
862/**
863 * remap a field name to a string name (for sort filter)
864 * @param string $p_string The string to process.
865 * @return string
866 */
867function string_get_field_name( $p_string ) {
868	$t_map = array(
869		'attachment_count' => 'attachments',
870		'category_id' => 'category',
871		'handler_id' => 'assigned_to',
872		'id' => 'email_bug',
873		'last_updated' => 'updated',
874		'project_id' => 'email_project',
875		'reporter_id' => 'reporter',
876		'view_state' => 'view_status',
877	);
878
879	$t_string = $p_string;
880	if( isset( $t_map[$p_string] ) ) {
881		$t_string = $t_map[$p_string];
882	}
883	return lang_get_defaulted( $t_string );
884}
885
886/**
887 * Calls htmlentities on the specified string, passing along
888 * the current character set.
889 * @param string $p_string The string to process.
890 * @return string
891 */
892function string_html_entities( $p_string ) {
893	return htmlentities( $p_string, ENT_COMPAT, 'utf-8' );
894}
895
896/**
897 * Calls htmlspecialchars on the specified string, handling utf8
898 * @param string $p_string The string to process.
899 * @return string
900 */
901function string_html_specialchars( $p_string ) {
902	# achumakov: @ added to avoid warning output in unsupported codepages
903	# e.g. 8859-2, windows-1257, Korean, which are treated as 8859-1.
904	# This is VERY important for Eastern European, Baltic and Korean languages
905	return preg_replace( '/&amp;(#[0-9]+|[a-z]+);/i', '&$1;', @htmlspecialchars( $p_string, ENT_COMPAT, 'utf-8' ) );
906}
907
908/**
909 * Prepares a string to be used as part of header().
910 * @param string $p_string The string to process.
911 * @return string
912 */
913function string_prepare_header( $p_string ) {
914	$t_string= explode( "\n", $p_string, 2 );
915	$t_string= explode( "\r", $t_string[0], 2 );
916	return $t_string[0];
917}
918
919/**
920 * Replacement for str_pad. $padStr may contain multi-byte characters.
921 *
922 * @author Oliver Saunders <oliver (a) osinternetservices.com>
923 * @param string $input
924 * @param int $length
925 * @param string $padStr
926 * @param int $type ( same constants as str_pad )
927 * @return string
928 * @see http://www.php.net/str_pad
929 * @see utf8_substr
930 */
931function utf8_str_pad( $input, $length, $padStr = ' ', $type = STR_PAD_RIGHT ) {
932
933    $inputLen = mb_strlen($input);
934    if ($length <= $inputLen) {
935        return $input;
936    }
937
938    $padStrLen = mb_strlen($padStr);
939    $padLen = $length - $inputLen;
940
941    if ($type == STR_PAD_RIGHT) {
942        $repeatTimes = ceil($padLen / $padStrLen);
943        return mb_substr($input . str_repeat($padStr, $repeatTimes), 0, $length);
944    }
945
946    if ($type == STR_PAD_LEFT) {
947        $repeatTimes = ceil($padLen / $padStrLen);
948        return mb_substr(str_repeat($padStr, $repeatTimes), 0, floor($padLen)) . $input;
949    }
950
951    if ($type == STR_PAD_BOTH) {
952
953        $padLen/= 2;
954        $padAmountLeft = floor($padLen);
955        $padAmountRight = ceil($padLen);
956        $repeatTimesLeft = ceil($padAmountLeft / $padStrLen);
957        $repeatTimesRight = ceil($padAmountRight / $padStrLen);
958
959        $paddingLeft = mb_substr(str_repeat($padStr, $repeatTimesLeft), 0, $padAmountLeft);
960        $paddingRight = mb_substr(str_repeat($padStr, $repeatTimesRight), 0, $padAmountLeft);
961        return $paddingLeft . $input . $paddingRight;
962    }
963
964    trigger_error('utf8_str_pad: Unknown padding type (' . $type . ')',E_USER_ERROR);
965}
966
967/**
968 * Return the number of UTF-8 characters in a string
969 * @param string $p_string
970 * @return integer number of UTF-8 characters in string
971 * @deprecated mb_strlen() should be used in preference to this function
972 */
973function utf8_strlen( $p_string ) {
974    error_parameters( __FUNCTION__ . '()', 'mb_strlen()' );
975    trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
976    return mb_strlen( $p_string );
977}
978
979/**
980 * Get part of string
981 * @param string $p_string
982 * @param integer $p_offset
983 * @param integer $p_length
984 * @return mixed string or FALSE if failure
985 * @deprecated mb_substr() should be used in preference to this function
986 */
987function utf8_substr( $p_string, $p_offset, $p_length = NULL ) {
988    error_parameters( __FUNCTION__ . '()', 'mb_substr()' );
989    trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
990    return mb_substr( $p_string, $p_offset, $p_length );
991}
992
993/**
994 * Make a string lowercase
995 * @param string $p_string
996 * @return string with all alphabetic characters converted to lowercase
997 * @deprecated mb_strtolower() should be used in preference to this function
998 */
999function utf8_strtolower( $p_string ) {
1000    error_parameters( __FUNCTION__ . '()', 'mb_strtolower()' );
1001    trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
1002    return mb_strtolower( $p_string );
1003}
1004