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 * HTML API
19 *
20 * These functions control the HTML output of each page.
21 *
22 *
23 * @package CoreAPI
24 * @subpackage HTMLAPI
25 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
26 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
27 * @link http://www.mantisbt.org
28 *
29 * @uses access_api.php
30 * @uses authentication_api.php
31 * @uses bug_api.php
32 * @uses config_api.php
33 * @uses constant_inc.php
34 * @uses current_user_api.php
35 * @uses database_api.php
36 * @uses error_api.php
37 * @uses event_api.php
38 * @uses file_api.php
39 * @uses filter_api.php
40 * @uses filter_constants_inc.php
41 * @uses form_api.php
42 * @uses helper_api.php
43 * @uses lang_api.php
44 * @uses news_api.php
45 * @uses php_api.php
46 * @uses print_api.php
47 * @uses project_api.php
48 * @uses rss_api.php
49 * @uses string_api.php
50 * @uses user_api.php
51 * @uses utility_api.php
52 * @uses layout_api.php
53 * @uses api_token_api.php
54 */
55
56require_api( 'access_api.php' );
57require_api( 'authentication_api.php' );
58require_api( 'bug_api.php' );
59require_api( 'config_api.php' );
60require_api( 'constant_inc.php' );
61require_api( 'current_user_api.php' );
62require_api( 'database_api.php' );
63require_api( 'error_api.php' );
64require_api( 'event_api.php' );
65require_api( 'file_api.php' );
66require_api( 'filter_api.php' );
67require_api( 'filter_constants_inc.php' );
68require_api( 'form_api.php' );
69require_api( 'helper_api.php' );
70require_api( 'lang_api.php' );
71require_api( 'news_api.php' );
72require_api( 'php_api.php' );
73require_api( 'print_api.php' );
74require_api( 'project_api.php' );
75require_api( 'rss_api.php' );
76require_api( 'string_api.php' );
77require_api( 'user_api.php' );
78require_api( 'utility_api.php' );
79require_api( 'layout_api.php' );
80require_api( 'api_token_api.php' );
81
82$g_rss_feed_url = null;
83
84$g_robots_meta = '';
85
86# flag for error handler to skip header menus
87$g_error_send_page_header = true;
88
89$g_stylesheets_included = array();
90$g_scripts_included = array();
91
92/**
93 * Sets the url for the rss link associated with the current page.
94 * null: means no feed (default).
95 * @param string $p_rss_feed_url RSS feed URL.
96 * @return void
97 */
98function html_set_rss_link( $p_rss_feed_url ) {
99	if( OFF != config_get( 'rss_enabled' ) ) {
100		global $g_rss_feed_url;
101		$g_rss_feed_url = $p_rss_feed_url;
102	}
103}
104
105/**
106 * This method marks the page as not for indexing by search engines
107 * @return void
108 */
109function html_robots_noindex() {
110	global $g_robots_meta;
111	$g_robots_meta = 'noindex,follow';
112}
113
114/**
115 * Prints the link that allows auto-detection of the associated feed.
116 * @return void
117 */
118function html_rss_link() {
119	global $g_rss_feed_url;
120
121	if( $g_rss_feed_url !== null ) {
122		echo '<link rel="alternate" type="application/rss+xml" title="RSS" href="' . string_attribute( $g_rss_feed_url ) . '" />' . "\n";
123	}
124}
125
126/**
127 * Prints a <script> tag to include a JavaScript file.
128 * @param string $p_filename Name of JavaScript file (with extension) to include.
129 * @return void
130 */
131function html_javascript_link( $p_filename ) {
132	echo "\t", '<script type="text/javascript" src="', helper_mantis_url( 'js/' . $p_filename ), '"></script>', "\n";
133}
134
135/**
136 * Prints a <script> tag to include a JavaScript file.
137 * @param string $p_url fully qualified domain name for the cdn js file
138 * @param string $p_hash resource hash to perform subresource integrity check
139 * @return void
140 */
141function html_javascript_cdn_link( $p_url, $p_hash = '' ) {
142	$t_integrity = '';
143	if( $p_hash !== '' ) {
144		$t_integrity = 'integrity="' . $p_hash . '" ';
145	}
146	echo "\t", '<script type="text/javascript" src="', $p_url, '" ', $t_integrity, 'crossorigin="anonymous"></script>', "\n";
147}
148
149/**
150 * Print the document type and the opening <html> tag
151 * @return void
152 */
153function html_begin() {
154	echo '<!DOCTYPE html>', "\n";
155	echo '<html>', "\n";
156}
157
158/**
159 * Begin the <head> section
160 * @return void
161 */
162function html_head_begin() {
163	echo '<head>', "\n";
164}
165
166/**
167 * Print the content-type
168 * @return void
169 */
170function html_content_type() {
171	echo "\t", '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />', "\n";
172}
173
174/**
175 * Print the window title
176 * @param string $p_page_title Window title.
177 * @return void
178 */
179function html_title( $p_page_title = null ) {
180	$t_page_title = string_html_specialchars( $p_page_title );
181	$t_title = string_html_specialchars( config_get( 'window_title' ) );
182	echo "\t", '<title>';
183	if( empty( $t_page_title ) ) {
184		echo $t_title;
185	} else {
186		if( empty( $t_title ) ) {
187			echo $t_page_title;
188		} else {
189			echo $t_page_title . ' - ' . $t_title;
190		}
191	}
192	echo '</title>', "\n";
193}
194
195/**
196 * Require a CSS file to be in html page headers
197 * @param string $p_stylesheet_path Path to CSS style sheet.
198 * @return void
199 */
200function require_css( $p_stylesheet_path ) {
201	global $g_stylesheets_included;
202	$g_stylesheets_included[$p_stylesheet_path] = $p_stylesheet_path;
203}
204
205/**
206 * Print the link to include the CSS file
207 * @return void
208 */
209function html_css() {
210	global $g_stylesheets_included;
211	html_css_link( config_get_global( 'css_include_file' ) );
212	# Add right-to-left css if needed
213	if( lang_get( 'directionality' ) == 'rtl' ) {
214		html_css_link( config_get_global( 'css_rtl_include_file' ) );
215	}
216	foreach( $g_stylesheets_included as $t_stylesheet_path ) {
217		# status_config.php is a special css file, dynamically generated.
218		# Add a hash to the query string to differentiate content based on its
219		# relevant properties. This allows a browser to cache them separately and force
220		# a reload when the content may differ.
221		if( $t_stylesheet_path == 'status_config.php' ) {
222			$t_stylesheet_path = helper_url_combine(
223				helper_mantis_url( 'css/status_config.php' ),
224				'cache_key=' . helper_generate_cache_key( array( 'user' ) )
225			);
226		}
227		html_css_link( $t_stylesheet_path );
228	}
229
230	# dropzone css
231	if ( config_get_global( 'cdn_enabled' ) == ON ) {
232		html_css_cdn_link( 'https://cdnjs.cloudflare.com/ajax/libs/dropzone/' . DROPZONE_VERSION . '/min/dropzone.min.css' );
233	} else {
234		html_css_link( 'dropzone-' . DROPZONE_VERSION . '.min.css' );
235	}
236}
237
238/**
239 * Prints a CSS link
240 * @param string $p_filename Filename.
241 * @return void
242 */
243function html_css_link( $p_filename ) {
244	# If no path is specified, look for CSS files in default directory
245	if( $p_filename == basename( $p_filename ) ) {
246		$p_filename = 'css/' . $p_filename;
247	}
248	echo "\t", '<link rel="stylesheet" type="text/css" href="', string_sanitize_url( helper_mantis_url( $p_filename ), true ), '" />', "\n";
249}
250
251/**
252 * Prints a CSS link for CDN
253 * @param string $p_url fully qualified domain name to the js file name
254 * @param string $p_hash resource hash to perform subresource integrity check
255 * @return void
256 */
257function html_css_cdn_link( $p_url, $p_hash = '' ) {
258	$t_integrity = '';
259	if( $p_hash !== '' ) {
260		$t_integrity = 'integrity="' . $p_hash . '" ';
261	}
262	echo "\t", '<link rel="stylesheet" type="text/css" href="', $p_url, '" ', $t_integrity, ' crossorigin="anonymous" />', "\n";
263}
264
265/**
266 * Print an HTML meta tag to redirect to another page
267 * This function is optional and may be called by pages that need a redirect.
268 * $p_time is the number of seconds to wait before redirecting.
269 * If we have handled any errors on this page return false and don't redirect.
270 *
271 * @param string  $p_url      The page to redirect: has to be a relative path.
272 * @param integer $p_time     Seconds to wait for before redirecting.
273 * @param boolean $p_sanitize Apply string_sanitize_url to passed URL.
274 * @return boolean
275 */
276function html_meta_redirect( $p_url, $p_time = null, $p_sanitize = true ) {
277	if( ON == config_get_global( 'stop_on_errors' ) && error_handled() ) {
278		return false;
279	}
280
281	if( null === $p_time ) {
282		$p_time = current_user_get_pref( 'redirect_delay' );
283	}
284
285	$t_url = config_get_global( 'path' );
286	if( $p_sanitize ) {
287		$t_url .= string_sanitize_url( $p_url );
288	} else {
289		$t_url .= $p_url;
290	}
291
292	$t_url = htmlspecialchars( $t_url );
293
294	echo "\t" . '<meta http-equiv="Refresh" content="' . $p_time . '; URL=' . $t_url . '" />' . "\n";
295
296	return true;
297}
298
299/**
300 * Require a javascript file to be in html page headers
301 * @param string $p_script_path Path to javascript file.
302 * @return void
303 */
304function require_js( $p_script_path ) {
305	global $g_scripts_included;
306	$g_scripts_included[$p_script_path] = $p_script_path;
307}
308
309/**
310 * Javascript...
311 * @return void
312 */
313function html_head_javascript() {
314	global $g_scripts_included;
315	# Add a hash to the query string to differentiate content based on its
316	# relevant properties. This allows a browser to cache them separately and force
317	# a reload when the content may differ.
318	$t_javascript_translations = helper_url_combine(
319		helper_mantis_url( 'javascript_translations.php' ),
320		'cache_key=' . helper_generate_cache_key( array( 'lang' ) )
321	);
322	$t_javascript_config = helper_url_combine(
323		helper_mantis_url( 'javascript_config.php' ),
324		'cache_key=' . helper_generate_cache_key( array( 'user' ) )
325	);
326	echo "\t" . '<script type="text/javascript" src="' . $t_javascript_config . '"></script>' . "\n";
327	echo "\t" . '<script type="text/javascript" src="' . $t_javascript_translations . '"></script>' . "\n";
328
329	if ( config_get_global( 'cdn_enabled' ) == ON ) {
330		# JQuery
331		html_javascript_cdn_link( 'https://ajax.googleapis.com/ajax/libs/jquery/' . JQUERY_VERSION . '/jquery.min.js', JQUERY_HASH );
332
333		# Dropzone
334		html_javascript_cdn_link( 'https://cdnjs.cloudflare.com/ajax/libs/dropzone/' . DROPZONE_VERSION . '/min/dropzone.min.js', DROPZONE_HASH );
335	} else {
336		# JQuery
337		html_javascript_link( 'jquery-' . JQUERY_VERSION . '.min.js' );
338
339		# Dropzone
340		html_javascript_link( 'dropzone-' . DROPZONE_VERSION . '.min.js' );
341	}
342
343	html_javascript_link( 'common.js' );
344	foreach ( $g_scripts_included as $t_script_path ) {
345		html_javascript_link( $t_script_path );
346	}
347}
348
349/**
350 * End the <head> section
351 * @return void
352 */
353function html_head_end() {
354	echo '</head>', "\n";
355}
356
357/**
358 * Prints the logo with an URL link.
359 * @param string $p_logo Path to the logo image. If not specified, will get it
360 *                       from $g_logo_image
361 * @return void
362 */
363function html_print_logo( $p_logo = null ) {
364	if( !$p_logo ) {
365		$p_logo = config_get_global( 'logo_image' );
366	}
367
368	if( !is_blank( $p_logo ) ) {
369		$t_logo_url = config_get_global( 'logo_url' );
370		$t_show_url = !is_blank( $t_logo_url );
371
372		if( $t_show_url ) {
373			echo '<a id="logo-link" href="', config_get_global( 'logo_url' ), '">';
374		}
375		$t_alternate_text = string_html_specialchars( config_get( 'window_title' ) );
376		echo '<img id="logo-image" alt="', $t_alternate_text, '" style="max-height: 80px;" src="' . helper_mantis_url( $p_logo ) . '" />';
377		if( $t_show_url ) {
378			echo '</a>';
379		}
380	}
381}
382
383
384
385/**
386 * Print a user-defined banner at the top of the page if there is one.
387 * @return void
388 */
389function html_top_banner() {
390	$t_page = config_get_global( 'top_include_page' );
391	$t_logo_image = config_get_global( 'logo_image' );
392
393	if( !is_blank( $t_page ) && file_exists( $t_page ) && !is_dir( $t_page ) ) {
394		include( $t_page );
395	} else if( !is_blank( $t_logo_image ) ) {
396		echo '<div id="banner">';
397		html_print_logo( $t_logo_image );
398		echo '</div>';
399	}
400
401	event_signal( 'EVENT_LAYOUT_PAGE_HEADER' );
402}
403
404/**
405 * Outputs a message to confirm an operation's result.
406 * @param array   $p_buttons     Array of (URL, label) pairs used to generate
407 *                               the buttons; if label is null or unspecified,
408 *                               the default 'proceed' text will be displayed;
409 *                               If the array is empty or not provided, no
410 *                               buttons will be printed.
411 * @param string  $p_message     Message to display to the user. If none is
412 *                               provided, a default message will be printed
413 * @param integer $p_type        One of the constants CONFIRMATION_TYPE_SUCCESS,
414 *                               CONFIRMATION_TYPE_WARNING, CONFIRMATION_TYPE_FAILURE
415 * @return void
416 */
417function html_operation_confirmation( array $p_buttons = null, $p_message = '', $p_type = CONFIRMATION_TYPE_SUCCESS ) {
418	switch( $p_type ) {
419		case CONFIRMATION_TYPE_FAILURE:
420			$t_alert_css = 'alert-danger';
421			$t_message = 'operation_failed';
422			break;
423		case CONFIRMATION_TYPE_WARNING:
424			$t_alert_css = 'alert-warning';
425			$t_message = 'operation_warnings';
426			break;
427		case CONFIRMATION_TYPE_SUCCESS:
428		default:
429			$t_alert_css = 'alert-success';
430			$t_message = 'operation_successful';
431			break;
432	}
433
434	echo '<div class="container-fluid">';
435	echo '<div class="col-md-12 col-xs-12">';
436	echo '<div class="space-0"></div>';
437	echo '<div class="alert ' . $t_alert_css . ' center">';
438
439	# Print message
440	if( is_blank( $p_message ) ) {
441		$t_message = lang_get( $t_message );
442	} else {
443		$t_message = $p_message;
444	}
445	echo '<p class="bold bigger-110">' . $t_message  . '</p>';
446
447	# Print buttons
448	if( !empty( $p_buttons ) ) {
449		echo '<br />';
450		echo '<div class="btn-group">';
451		foreach( $p_buttons as $t_button ) {
452			$t_url = string_sanitize_url( $t_button[0] );
453			$t_label = isset( $t_button[1] ) ? $t_button[1] : lang_get( 'proceed' );
454
455			print_link_button( $t_url, $t_label );
456		}
457		echo '</div>';
458	}
459
460	echo '</div></div></div>', PHP_EOL;
461}
462
463/**
464 * Outputs an operation successful message with a single redirect link.
465 * @param string $p_redirect_url The url to redirect to.
466 * @param string $p_message      Message to display to the user.
467 * @return void
468 */
469function html_operation_successful( $p_redirect_url, $p_message = '' ) {
470	html_operation_confirmation( array( array( $p_redirect_url ) ), $p_message );
471}
472
473/**
474 * Outputs a warning message with a single redirect link.
475 * @param string $p_redirect_url The url to redirect to.
476 * @param string $p_message      Message to display to the user.
477 * @return void
478 */
479function html_operation_warning( $p_redirect_url, $p_message = '' ) {
480	html_operation_confirmation(
481		array( array( $p_redirect_url ) ),
482		$p_message,
483		CONFIRMATION_TYPE_WARNING
484	);
485}
486
487/**
488 * Outputs an error message with a single redirect link.
489 * @param string $p_redirect_url The url to redirect to.
490 * @param string $p_message      Message to display to the user.
491 * @return void
492 */
493function html_operation_failure( $p_redirect_url, $p_message = '' ) {
494	html_operation_confirmation(
495		array( array( $p_redirect_url ) ),
496		$p_message,
497		CONFIRMATION_TYPE_FAILURE
498	);
499}
500
501/**
502 * End the <body> section
503 * @return void
504 */
505function html_body_end() {
506	# Should code need to be added to this function in the future, it should be
507	# placed *above* this event, which needs to be the last thing to occur
508	# before the actual body ends (see #20084)
509	event_signal( 'EVENT_LAYOUT_BODY_END' );
510
511	echo '</body>', "\n";
512}
513
514/**
515 * Print the closing <html> tag
516 * @return void
517 */
518function html_end() {
519	echo '</html>', "\n";
520
521	if( function_exists( 'fastcgi_finish_request' ) ) {
522		fastcgi_finish_request();
523	}
524}
525
526/**
527 * Print the menu bar with a list of projects to which the user has access
528 *
529 * @see $g_show_project_menu_bar
530 *
531 * @return void
532 */
533function print_project_menu_bar() {
534	$t_project_ids = current_user_get_accessible_projects();
535	$t_current_project_id = helper_get_current_project();
536	$t_button_classes = 'btn btn-xs btn-white btn-info';
537
538	echo '<div class="col-md-12 col-xs-12">' . "\n";
539	echo '<div class="btn-group">' . "\n";
540
541	echo project_link_for_menu(
542			ALL_PROJECTS,
543			$t_current_project_id == ALL_PROJECTS,
544			$t_button_classes
545		);
546	echo "\n";
547	foreach( $t_project_ids as $t_id ) {
548		echo project_link_for_menu(
549				$t_id,
550				$t_current_project_id == $t_id,
551				$t_button_classes
552			);
553		echo "\n";
554		print_subproject_menu_bar( $t_current_project_id, $t_id, array( $t_id ) );
555	}
556
557	echo '</div>' . "\n";
558	echo '<div class="space-4"></div>' . "\n";
559	echo '</div>' . "\n";
560}
561
562/**
563 * Print the menu bar with a list of subprojects to which the user has access
564 *
565 * @param integer $p_current_project_id Selected project id.
566 * @param integer $p_parent_project_id  Parent project id.
567 * @param array   $p_parents            Parent project identifiers.
568 *
569 * @return void
570 */
571function print_subproject_menu_bar( $p_current_project_id, $p_parent_project_id, array $p_parents = array() ) {
572	$t_subprojects = current_user_get_accessible_subprojects( $p_parent_project_id );
573
574	foreach( $t_subprojects as $t_subproject_id ) {
575		echo project_link_for_menu(
576				$t_subproject_id,
577				$t_subproject_id == $p_current_project_id,
578				'btn btn-xs btn-white btn-info',
579				$p_parents,
580				icon_get( 'fa-angle-double-right', 'ace-icon' )
581			);
582		echo "\n";
583
584		# Recursive call to render this subproject's subprojects
585		print_subproject_menu_bar(
586			$p_current_project_id,
587			$t_subproject_id,
588			array_merge( $p_parents, array( $t_subproject_id) )
589		);
590	}
591}
592
593/**
594 * Print a generic menu (tabs).
595 *
596 * @param array  $p_menu_items   List of menu items
597 * @param string $p_current_page Current page's file name to highlight active tab
598 * @param string $p_event        Optional event to signal,
599 */
600function print_menu( array $p_menu_items, $p_current_page = '', $p_event = null ) {
601	echo '<ul class="nav nav-tabs padding-18">' . "\n";
602
603	foreach( $p_menu_items as $t_item ) {
604		$t_active = $p_current_page && strpos( $t_item['url'], $p_current_page ) !== false ? 'active' : '';
605
606		echo '<li class="' . $t_active .  '">';
607		if( $t_item['label'] == '' ) {
608			echo '<a href="'. lang_get_defaulted( $t_item['url'] ) .'">';
609			print_icon( 'fa-info-circle', 'blue ace-icon' );
610			echo '</a>';
611		} else {
612			echo '<a href="'. helper_mantis_url( $t_item['url'] ) .'">' . lang_get_defaulted( $t_item['label'] ) . '</a>';
613		}
614		echo '</li>' . "\n";
615	}
616
617	# Plugins menu items - these are html hyperlinks (<a> tags)
618	foreach( plugin_menu_items( $p_event ) as $t_item ) {
619		$t_active = $p_current_page && strpos( $t_item, $p_current_page ) !== false
620			? 'active'
621			: '';
622		echo '<li class="' . $t_active . '">', $t_item, '</li>', "\n";
623	}
624
625	echo '</ul>' . "\n";
626}
627
628/**
629 * Print a generic submenu (buttons group).
630 *
631 * @param array  $p_menu_items   List of menu items
632 * @param string $p_current_page Current page's file name to highlight active tab
633 * @param string $p_event        Optional event to signal,
634 */
635function print_submenu( array $p_menu_items, $p_current_page = '', $p_event = null ) {
636	# Plugin / Event added options
637	$t_plugin_menu_items = plugin_menu_items( $p_event );
638
639	if( $p_menu_items || $t_plugin_menu_items ) {
640		echo '<div class="space-10"></div>';
641		echo '<div class="col-md-12 col-xs-12 center">';
642		echo '<div class="btn-group">', "\n";
643
644		$t_btn_template = '<a class="btn btn-sm btn-primary btn-white %s" href="%s">%s%s</a>' . "\n";
645
646		foreach( $p_menu_items as $t_item ) {
647			if( is_array( $t_item ) ) {
648				$t_active = $p_current_page && strpos( $t_item['url'], $p_current_page ) !== false
649					? 'active' : '';
650				$t_icon = array_key_exists( 'icon', $t_item )
651					? icon_get( $t_item['icon'] ) . '&nbsp;'
652					: '';
653
654				printf( $t_btn_template,
655					$t_active,
656					$t_item['url'],
657					$t_icon,
658					lang_get_defaulted( $t_item['label'] )
659				);
660			} else {
661				# Cooked link
662				echo $t_item;
663			}
664		}
665
666		# Plugins menu items - these are html hyperlinks (<a> tags)
667		# The plugin is responsible for setting the 'active' class as appropriate
668		foreach( plugin_menu_items( $p_event ) as $t_item ) {
669			echo $t_item;
670		}
671
672		echo '</div></div>', "\n";
673	}
674}
675
676/**
677 * Print the Summary page's submenu.
678 * The submenu is only printed if there is at least one plugin-defined link, in
679 * which case a 'Synthesis' button is added for the summary page itself.
680 * @param string $p_current_page Current page's file name to highlight active menu item
681 * @return void
682 */
683function print_summary_submenu( $p_current_page = '' ) {
684	# Plugin / Event added options
685	$t_menu_items = plugin_menu_items( 'EVENT_SUBMENU_SUMMARY' );
686
687	if( $t_menu_items ) {
688		$t_filter_param = filter_get_temporary_key_param( summary_get_filter() );
689
690		$t_synthesis['summary_page.php'] = array(
691			'url' => helper_url_combine( helper_mantis_url( 'summary_page.php' ), $t_filter_param ),
692			'icon' => 'fa-table',
693			'label' => 'synthesis',
694		);
695
696		if( $p_current_page == '' ) {
697			$p_current_page = 'summary_page.php';
698		}
699		print_submenu( array_merge( $t_synthesis, $t_menu_items ), $p_current_page );
700	}
701}
702
703/**
704 * Print the menu for the manage section
705 *
706 * @param string $p_page Specifies the current page name so it's link can be disabled.
707 * @return void
708 */
709function print_manage_menu( $p_page = '' ) {
710	$t_pages = array();
711
712	if( access_has_global_level( config_get( 'manage_site_threshold' ) ) ) {
713		$t_pages['manage_overview_page.php'] = array( 'url'   => 'manage_overview_page.php', 'label' => '' );
714	}
715	if( access_has_global_level( config_get( 'manage_user_threshold' ) ) ) {
716		$t_pages['manage_user_page.php'] = array( 'url'   => 'manage_user_page.php', 'label' => 'manage_users_link' );
717	}
718	if( access_has_project_level( config_get( 'manage_project_threshold' ) ) ) {
719		$t_pages['manage_proj_page.php'] = array( 'url'   => 'manage_proj_page.php', 'label' => 'manage_projects_link' );
720	}
721	if( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
722		$t_pages['manage_tags_page.php'] = array( 'url'   => 'manage_tags_page.php', 'label' => 'manage_tags_link' );
723	}
724	if( access_has_global_level( config_get( 'manage_custom_fields_threshold' ) ) ) {
725		$t_pages['manage_custom_field_page.php'] = array( 'url'   => 'manage_custom_field_page.php', 'label' => 'manage_custom_field_link' );
726	}
727	if( config_get( 'enable_profiles' ) == ON && access_has_global_level( config_get( 'manage_global_profile_threshold' ) ) ) {
728		$t_pages['manage_prof_menu_page.php'] = array( 'url'   => 'manage_prof_menu_page.php', 'label' => 'manage_global_profiles_link' );
729	}
730	if( access_has_global_level( config_get( 'manage_plugin_threshold' ) ) ) {
731		$t_pages['manage_plugin_page.php'] = array( 'url'   => 'manage_plugin_page.php', 'label' => 'manage_plugin_link' );
732	}
733
734	if( access_has_project_level( config_get( 'manage_configuration_threshold' ) ) ) {
735		$t_pages['adm_permissions_report.php'] = array(
736			'url'   => 'adm_permissions_report.php',
737			'label' => 'manage_config_link'
738		);
739	}
740
741	print_menu( $t_pages, $p_page, 'EVENT_MENU_MANAGE' );
742}
743
744/**
745 * Print the menu for the manage configuration section
746 * @param string $p_page Specifies the current page name so it's link can be disabled.
747 * @return void
748 */
749function print_manage_config_menu( $p_page = '' ) {
750	if( !access_has_project_level( config_get( 'manage_configuration_threshold' ) ) ) {
751		return;
752	}
753
754	$t_pages = array();
755
756	$t_pages['adm_permissions_report.php'] = array( 'url'   => 'adm_permissions_report.php',
757	                                                'label' => 'permissions_summary_report' );
758
759	if( access_has_global_level( config_get( 'view_configuration_threshold' ) ) ) {
760		$t_pages['adm_config_report.php'] = array( 'url'   => 'adm_config_report.php',
761		                                           'label' => 'configuration_report' );
762	}
763
764	$t_pages['manage_config_work_threshold_page.php'] = array( 'url'   => 'manage_config_work_threshold_page.php',
765	                                                           'label' => 'manage_threshold_config' );
766
767	$t_pages['manage_config_workflow_page.php'] = array( 'url'   => 'manage_config_workflow_page.php',
768	                                                     'label' => 'manage_workflow_config' );
769
770	if( config_get( 'relationship_graph_enable' ) ) {
771		$t_pages['manage_config_workflow_graph_page.php'] = array( 'url'   => 'manage_config_workflow_graph_page.php',
772		                                                           'label' => 'manage_workflow_graph' );
773	}
774
775	if( config_get( 'enable_email_notification' ) == ON ) {
776		$t_pages['manage_config_email_page.php'] = array( 'url'   => 'manage_config_email_page.php',
777		                                                  'label' => 'manage_email_config' );
778	}
779
780	$t_pages['manage_config_columns_page.php'] = array( 'url'   => 'manage_config_columns_page.php',
781	                                                    'label' => 'manage_columns_config' );
782
783	# Plugin / Event added options
784	$t_event_menu_options = event_signal( 'EVENT_MENU_MANAGE_CONFIG' );
785	$t_menu_options = array();
786	foreach ( $t_event_menu_options as $t_plugin => $t_plugin_menu_options ) {
787		foreach ( $t_plugin_menu_options as $t_callback => $t_callback_menu_options ) {
788			if( is_array( $t_callback_menu_options ) ) {
789				$t_menu_options = array_merge( $t_menu_options, $t_callback_menu_options );
790			} else {
791				if( !is_null( $t_callback_menu_options ) ) {
792					$t_menu_options[] = $t_callback_menu_options;
793				}
794			}
795		}
796	}
797
798	echo '<div class="space-10"></div>' . "\n";
799	echo '<div class="center">' . "\n";
800	echo '<div class="btn-toolbar inline">' . "\n";
801	echo '<div class="btn-group">' . "\n";
802
803	foreach ( $t_pages as $t_page ) {
804		$t_active =  $t_page['url'] == $p_page ? 'active' : '';
805		echo '<a class="btn btn-sm btn-white btn-primary ' . $t_active . '" href="'. helper_mantis_url( $t_page['url'] ) .'">' . "\n";
806		echo lang_get_defaulted( $t_page['label'] );
807		echo '</a>' . "\n";
808	}
809
810	foreach ( $t_menu_options as $t_menu_item ) {
811		echo $t_menu_item;
812	}
813
814	echo '</div>' . "\n";
815	echo '</div>' . "\n";
816	echo '</div>' . "\n";
817}
818
819/**
820 * Print the menu for the account section
821 * @param string $p_page Specifies the current page name so it's link can be disabled.
822 * @return void
823 */
824function print_account_menu( $p_page = '' ) {
825	$t_pages['account_page.php'] = array( 'url'=>'account_page.php', 'label'=>'account_link' );
826	$t_pages['account_prefs_page.php'] = array( 'url'=>'account_prefs_page.php', 'label'=>'change_preferences_link' );
827	$t_pages['account_manage_columns_page.php'] = array( 'url'=>'account_manage_columns_page.php', 'label'=>'manage_columns_config' );
828
829	if( config_get( 'enable_profiles' ) == ON && access_has_project_level( config_get( 'add_profile_threshold' ) ) ) {
830		$t_pages['account_prof_menu_page.php'] = array( 'url'=>'account_prof_menu_page.php', 'label'=>'manage_profiles_link' );
831	}
832
833	if( config_get( 'enable_sponsorship' ) == ON && access_has_project_level( config_get( 'view_sponsorship_total_threshold' ) ) && !current_user_is_anonymous() ) {
834		$t_pages['account_sponsor_page.php'] = array( 'url'=>'account_sponsor_page.php', 'label'=>'my_sponsorship' );
835	}
836
837	if( api_token_can_create() ) {
838		$t_pages['api_tokens_page.php'] = array( 'url' => 'api_tokens_page.php', 'label' => 'api_tokens_link' );
839	}
840
841	print_menu( $t_pages, $p_page, 'EVENT_MENU_ACCOUNT' );
842}
843
844/**
845 * Print the menu for the documentation section
846 * @param string $p_page Specifies the current page name so it's link can be disabled.
847 * @return void
848 */
849function print_doc_menu( $p_page = '' ) {
850	# User Documentation
851	$t_doc_url = config_get_global( 'manual_url' );
852	if( is_null( parse_url( $t_doc_url, PHP_URL_SCHEME ) ) ) {
853		# URL has no scheme, so it is relative to MantisBT root
854		if( is_blank( $t_doc_url ) ||
855			!file_exists( config_get_global( 'absolute_path' ) . $t_doc_url )
856		) {
857			# Local documentation not available, use online docs
858			$t_doc_url = 'http://www.mantisbt.org/documentation.php';
859		} else {
860			$t_doc_url = helper_mantis_url( $t_doc_url );
861		}
862	}
863
864	$t_pages[$t_doc_url] = array(
865		'url'   => $t_doc_url,
866		'label' => 'user_documentation'
867	);
868
869	# Project Documentation
870	$t_pages['proj_doc_page.php'] = array(
871		'url'   => 'proj_doc_page.php',
872		'label' => 'project_documentation'
873	);
874
875	# Add File
876	if( file_allow_project_upload() ) {
877		$t_pages['proj_doc_add_page.php'] = array(
878			'url'   => 'proj_doc_add_page.php',
879			'label' => 'add_file'
880		);
881	}
882
883	print_menu( $t_pages, $p_page );
884}
885
886/**
887 * Print the menu for the summary section.
888 * @param string $p_page Specifies the current page name so it's link can be disabled.
889 * @param array $p_filter Filter array, the one in use for summary pages.
890 * @return void
891 */
892function print_summary_menu( $p_page = '', array $p_filter = null ) {
893	$t_link = 'summary_page.php';
894	$t_filter_param = $p_filter ? filter_get_temporary_key_param( $p_filter ) : null;
895	if( $t_filter_param ) {
896		$t_link = helper_url_combine( $t_link, $t_filter_param );
897	}
898	$t_pages['summary_page.php'] = array(
899		'url' => $t_link,
900		'label' => 'summary_link',
901	);
902
903	print_menu( $t_pages, $p_page, 'EVENT_MENU_SUMMARY' );
904
905	summary_print_filter_info( $p_filter );
906}
907
908/**
909 * Print the admin tab bar.
910 * @param string $p_page Specifies the current page name so it is set as active.
911 * @return void
912 */
913function print_admin_menu_bar( $p_page ) {
914	# Build array with admin menu items, add Upgrade tab if necessary
915	$t_menu_items['index.php'] = icon_get( 'fa-info-circle', 'blue ace-icon' );
916
917	# At the beginning of admin checks, the DB is not yet loaded so we can't
918	# check the schema to inform user that an upgrade is needed
919	if( $p_page == 'check/index.php' ) {
920		# Relative URL up one level to ensure valid links on Admin Checks page
921		$t_path = '../';
922	} else {
923		global $g_upgrade;
924		include_once( 'schema.php' );
925		if( count( $g_upgrade ) - 1 != config_get( 'database_version', -1, ALL_USERS, ALL_PROJECTS ) ) {
926			$t_menu_items['install.php'] = 'Upgrade your installation';
927		}
928
929		$t_path = '';
930	}
931
932	$t_menu_items += array(
933		'check/index.php' => 'Check Installation',
934		'system_utils.php' => 'System Utilities',
935		'test_langs.php' => 'Test Lang',
936		'email_queue.php' => 'Email Queue',
937	);
938
939	echo '<div class="space-10"></div>' . "\n";
940	echo '<ul class="nav nav-tabs padding-18">' . "\n";
941
942	foreach( $t_menu_items as $t_menu_page => $t_description ) {
943		$t_class_active = $t_menu_page == $p_page ? ' class="active"' : '';
944		$t_class_green = $t_menu_page == 'install.php' ? 'class="bold green" ' : '';
945
946		echo "\t<li$t_class_active>";
947		echo "<a " . $t_class_green
948			. 'href="' . $t_path . $t_menu_page . '">'
949			. $t_description . "</a>";
950		echo '</li>' . "\n";
951	}
952
953	echo '</ul>' . "\n";
954}
955
956/**
957 * Print an html button inside a form
958 * @param string $p_action      Form Action.
959 * @param string $p_button_text Button Text.
960 * @param array  $p_fields      An array of hidden fields to include on the form.
961 * @param string $p_method      Form submit method - default post.
962 * @return void
963 */
964function html_button( $p_action, $p_button_text, array $p_fields = array(), $p_method = 'post' ) {
965	$t_form_name = explode( '.php', $p_action, 2 );
966	$p_action = urlencode( $p_action );
967	$p_button_text = string_attribute( $p_button_text );
968
969	if( strtolower( $p_method ) == 'get' ) {
970		$t_method = 'get';
971	} else {
972		$t_method = 'post';
973	}
974
975	echo '<form method="' . $t_method . '" action="' . $p_action . '" class="form-inline">' . "\n";
976	echo "\t" . '<fieldset>';
977	# Add a CSRF token only when the form is being sent via the POST method
978	if( $t_method == 'post' ) {
979		echo form_security_field( $t_form_name[0] );
980	}
981
982	foreach( $p_fields as $t_key => $t_val ) {
983		$t_key = string_attribute( $t_key );
984		$t_val = string_attribute( $t_val );
985
986		echo "\t\t" . '<input type="hidden" name="' . $t_key . '" value="' . $t_val . '" />' . "\n";
987	}
988
989	echo "\t\t" . '<input type="submit" class="btn btn-primary btn-sm btn-white btn-round" value="' . $p_button_text . '" />' . "\n";
990	echo "\t" . '</fieldset>';
991	echo '</form>' . "\n";
992}
993
994/**
995 * Get the foreground color CSS class for the given status, user and project.
996 * @see html_get_status_css_bg() for background color
997 *
998 * @param integer $p_status  An enumeration value.
999 * @param integer $p_user    A valid user identifier.
1000 * @param integer $p_project A valid project identifier.
1001 * @return string
1002 *
1003 * @todo This does not work properly when displaying issues from a project other
1004 * than then current one, if the other project has custom status or colors.
1005 * This is due to the dynamic css for color coding (css/status_config.php).
1006 * Build CSS including project or even user-specific colors ?
1007 */
1008function html_get_status_css_fg( $p_status, $p_user = null, $p_project = null ) {
1009	$t_status_enum = config_get( 'status_enum_string', null, $p_user, $p_project );
1010	if( MantisEnum::hasValue( $t_status_enum, $p_status ) ) {
1011		return 'status-' . $p_status . '-fg';
1012	} else {
1013		return '';
1014	}
1015}
1016
1017/**
1018 * Get the background color CSS class for the given status, user and project.
1019 * @see html_get_status_css_fg() for foreground color
1020 *
1021 * @param integer $p_status  An enumeration value.
1022 * @param integer $p_user    A valid user identifier.
1023 * @param integer $p_project A valid project identifier.
1024 *
1025 * @return string
1026 */
1027function html_get_status_css_bg( $p_status, $p_user = null, $p_project = null ) {
1028	$t_status_enum = config_get( 'status_enum_string', null, $p_user, $p_project );
1029	if( MantisEnum::hasValue( $t_status_enum, $p_status ) ) {
1030		return 'status-' . $p_status . '-bg';
1031	} else {
1032		return '';
1033	}
1034}
1035
1036/**
1037 * Get the css class name for the given status, user and project.
1038 *
1039 * @param integer $p_status  An enumeration value.
1040 * @param integer $p_user    A valid user identifier.
1041 * @param integer $p_project A valid project identifier.
1042 * @return string
1043 *
1044 * @deprecated 2.21.0 Use html_get_status_css_fg() or html_get_status_css_bg() instead
1045 */
1046function html_get_status_css_class( $p_status, $p_user = null, $p_project = null ) {
1047	error_parameters(
1048		__FUNCTION__ . '()',
1049		'html_get_status_css_fg() or html_get_status_css_bg()'
1050	);
1051	trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
1052
1053	$t_class = html_get_status_css_fg( $p_status, $p_user, $p_project )
1054		. ' '
1055		. html_get_status_css_bg( $p_status, $p_user, $p_project );
1056
1057	return trim( $t_class );
1058}
1059
1060/**
1061 * Class that provides managed generation of an HTML table content, consisting of <tr> and <td> elements
1062 * which are arranged sequentially on a grid.
1063 * Items consist of "header" and "content", which are rendered to separate table cells.
1064 * An option is provided to arrange the header and content in vertical or horizontal orientation.
1065 * Vertical orientation places header on top of content, while horizontal orientation places the header
1066 * to the left of content cell.
1067 * Each item can have a different colspan, which is used to arrange the items efficiently. When the
1068 * arrangement is made, an item with higher colspan than current free space may be placed in next row,
1069 * but still fill the current row with next items if they fit. This may cause a variation in expected
1070 * order, but allows for a more compact fill for rows.
1071 */
1072class TableGridLayout {
1073	const ORIENTATION_VERTICAL = 0;
1074	const ORIENTATION_HORIZONTAL = 1;
1075
1076	protected $cols;
1077	private $_max_colspan;
1078
1079	public $items = array();
1080	public $item_orientation;
1081
1082	/**
1083	 * Set this variable to add a class attribute for each <tr>
1084	 * @var string
1085	 */
1086	public $tr_class = null;
1087
1088	/**
1089	 * Constructor.
1090	 * $p_orientation may be one of this class constants:
1091	 * ORIENTATION_VERTICAL, ORIENTATION_HORIZONTAL
1092	 * @param integer $p_cols	Number of columns for the table
1093	 * @param integer $p_orientation	Orientation for header and content cells
1094	 */
1095	public function __construct( $p_cols, $p_orientation = null ) {
1096		# sanitize values
1097		switch( $p_orientation ) {
1098			case self::ORIENTATION_HORIZONTAL:
1099				if( $p_cols < 2 ) {
1100					$p_cols = 2;
1101				}
1102				$this->_max_colspan = $p_cols-1;
1103				break;
1104			case self::ORIENTATION_VERTICAL:
1105			default:
1106				$p_orientation = self::ORIENTATION_VERTICAL;
1107				if( $p_cols < 1 ) {
1108					$p_cols = 1;
1109				}
1110				$this->_max_colspan = $p_cols;
1111		}
1112
1113		$this->cols = $p_cols;
1114		$this->item_orientation = $p_orientation;
1115	}
1116
1117	/**
1118	 * Adds a item to the collection
1119	 * @param TableFieldsItem $p_item An item
1120	 */
1121	public function add_item( TableFieldsItem $p_item ) {
1122		if( $p_item->colspan > $this->_max_colspan ) {
1123			$p_item->colspan = $this->_max_colspan;
1124		}
1125		$this->items[] = $p_item;
1126	}
1127
1128	/**
1129	 * Prints the HTMl for the generated table cells, for all items contained
1130	 */
1131	public function render() {
1132		$t_rows_items = array();
1133		$t_rows_freespace = array();
1134		$t_used_rows = 0;
1135
1136		# Arrange the items in rows accounting for their actual cell space
1137		foreach( $this->items as $t_item ) {
1138			# Get the actual table columns needed to render the item
1139			$t_item_cols = ( $this->item_orientation == self::ORIENTATION_VERTICAL ) ? $t_item->colspan : $t_item->colspan + 1;
1140			# Search for a row with enough space to fit the item
1141			$t_found = false;
1142			for( $t_ix = 0; $t_ix < $t_used_rows; $t_ix++ ) {
1143				if( $t_rows_freespace[$t_ix] >= $t_item_cols ) {
1144					# Found a row with available space. Add the item here
1145					$t_found = true;
1146					$t_rows_freespace[$t_ix] -= $t_item_cols;
1147					$t_rows_items[$t_ix][] = $t_item;
1148					break;
1149				}
1150			}
1151			# If no suitable row was found, create new one and add the item here
1152			if( !$t_found ) {
1153				$t_rows_items[] = array( $t_item );
1154				$t_used_rows++;
1155				$t_rows_freespace[] = $this->cols - $t_item_cols;
1156			}
1157		}
1158
1159		# Render the arranged items
1160		if( $this->tr_class ) {
1161			$p_tr_attr_class = ' class="' . $this->tr_class . '"';
1162		} else {
1163			$p_tr_attr_class = '';
1164		}
1165		foreach( $t_rows_items as $t_row ) {
1166			switch( $this->item_orientation ) {
1167
1168				case self::ORIENTATION_HORIZONTAL:
1169					$t_cols_left = $this->cols;
1170					echo '<tr' . $p_tr_attr_class . '>';
1171					foreach( $t_row as $t_item ) {
1172						$this->render_td_item_header( $t_item, 1 );
1173						$this->render_td_item_content( $t_item, $t_item->colspan );
1174						$t_cols_left -= ( $t_item->colspan + 1 );
1175					}
1176					if( $t_cols_left > 0 ) {
1177						$this->render_td_empty($t_cols_left);
1178					}
1179					echo '</tr>';
1180					break;
1181
1182				# default is vertical orientation
1183				default:
1184					# row for headers
1185					$t_cols_left = $this->cols;
1186					echo '<tr' . $p_tr_attr_class . '>';
1187					foreach( $t_row as $t_item ) {
1188						$this->render_td_item_header( $t_item, $t_item->colspan );
1189						$t_cols_left -= $t_item->colspan;
1190					}
1191					if( $t_cols_left > 0 ) {
1192						$this->render_td_empty_header( $t_cols_left );
1193					}
1194					echo '</tr>';
1195					# row for contents
1196					$t_cols_left = $this->cols;
1197					echo '<tr' . $p_tr_attr_class . '>';
1198					foreach( $t_row as $t_item ) {
1199						$this->render_td_item_content( $t_item, $t_item->colspan );
1200						$t_cols_left -= $t_item->colspan;
1201					}
1202					if( $t_cols_left > 0 ) {
1203						$this->render_td_empty($t_cols_left);
1204					}
1205					echo '</tr>';
1206			}
1207		}
1208	}
1209
1210	/**
1211	 * Prints HTML code for an empty TD cell
1212	 * @param integer $p_colspan Colspan attribute for cell
1213	 */
1214	protected function render_td_empty( $p_colspan ) {
1215		echo '<td';
1216		if( $p_colspan > 1) {
1217			echo ' colspan="' . $p_colspan . '"';
1218		}
1219		echo '>';
1220		echo '&nbsp;';
1221		echo '</td>';
1222	}
1223
1224	/**
1225	 * Prints HTML code for an empty TD cell, of header type
1226	 * @param integer $p_colspan Colspan attribute for cell
1227	 */
1228	protected function render_td_empty_header( $p_colspan ) {
1229		$this->render_td_empty( $p_colspan );
1230	}
1231
1232	/**
1233	 * Prints HTML code for TD cell representing the Item header
1234	 * @abstract
1235	 * @param TableFieldsItem $p_item Item to display
1236	 * @param integer $p_colspan Colspan attribute for cell
1237	 */
1238	protected function render_td_item_header( TableFieldsItem $p_item, $p_colspan ) {
1239		echo '<th';
1240		if( $p_item->attr_class ) {
1241			echo 'class="' . $p_item->attr_class . '"';
1242		}
1243		if( $p_colspan > 1) {
1244			echo ' colspan="' . $p_colspan . '"';
1245		}
1246		if( $p_item->header_attr_id ) {
1247			echo ' id="' . $p_item->header_attr_id . '"';
1248		}
1249		echo '>';
1250		echo $p_item->header;
1251		echo '</th>';
1252	}
1253
1254	/**
1255	 * Prints HTML code for TD cell representing the Item content
1256	 * @abstract
1257	 * @param TableFieldsItem $p_item Item to display
1258	 * @param integer $p_colspan Colspan attribute for cell
1259	 */
1260	protected function render_td_item_content( TableFieldsItem $p_item, $p_colspan  ) {
1261		echo '<td';
1262		if( $p_item->attr_class ) {
1263			echo 'class="' . $p_item->attr_class . '"';
1264		}
1265		if( $p_colspan > 1) {
1266			echo ' colspan="' . $p_colspan . '"';
1267		}
1268		if( $p_item->content_attr_id ) {
1269			echo ' id="' . $p_item->content_attr_id . '"';
1270		}
1271		echo '>';
1272		echo $p_item->header;
1273		echo '</td>';
1274	}
1275}
1276
1277/**
1278 * Class that represent Items to use with TableGridLayout
1279 */
1280class TableFieldsItem {
1281	public $header;
1282	public $content;
1283	public $colspan;
1284	public $attr_class = null;
1285	public $content_attr_id = null;
1286	public $header_attr_id = null;
1287
1288	/**
1289	 * Constructor
1290	 * @param string $p_header		HTMl to be used in header cell
1291	 * @param string $p_content		HTMl to be used in content cell
1292	 * @param integer $p_colspan	Colspan for the content cell
1293	 * @param string $p_class		Class to be added to the cells
1294	 * @param string $p_content_id	Id attribute to use for content cell
1295	 * @param string $p_header_id	Id attribute to use for header cell
1296	 */
1297	public function __construct( $p_header, $p_content, $p_colspan = 1, $p_class = null, $p_content_id = null, $p_header_id = null ) {
1298		$this->header = $p_header;
1299		$this->content = $p_content;
1300		if( $p_colspan < 1 ) {
1301			$p_colspan = 1;
1302		}
1303		$this->colspan = $p_colspan;
1304		$this->attr_class = $p_class;
1305		$this->content_attr_id = $p_content_id;
1306		$this->header_attr_id = $p_header_id;
1307	}
1308}
1309
1310