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 * Access API
19 *
20 * @package CoreAPI
21 * @subpackage AccessAPI
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 authentication_api.php
27 * @uses bug_api.php
28 * @uses bugnote_api.php
29 * @uses config_api.php
30 * @uses constant_inc.php
31 * @uses current_user_api.php
32 * @uses database_api.php
33 * @uses error_api.php
34 * @uses helper_api.php
35 * @uses lang_api.php
36 * @uses print_api.php
37 * @uses project_api.php
38 * @uses string_api.php
39 * @uses user_api.php
40 */
41
42require_api( 'authentication_api.php' );
43require_api( 'bug_api.php' );
44require_api( 'bugnote_api.php' );
45require_api( 'config_api.php' );
46require_api( 'constant_inc.php' );
47require_api( 'current_user_api.php' );
48require_api( 'database_api.php' );
49require_api( 'error_api.php' );
50require_api( 'helper_api.php' );
51require_api( 'lang_api.php' );
52require_api( 'print_api.php' );
53require_api( 'project_api.php' );
54require_api( 'string_api.php' );
55require_api( 'user_api.php' );
56
57use Mantis\Exceptions\ClientException;
58
59# @global array $g_cache_access_matrix
60$g_cache_access_matrix = array();
61
62# @global array $g_cache_access_matrix_project_ids
63$g_cache_access_matrix_project_ids = array();
64
65# @global array $g_cache_access_matrix_user_ids
66$g_cache_access_matrix_user_ids = array();
67
68/**
69 * Function to be called when a user is attempting to access a page that
70 * he/she is not authorised to.  This outputs an access denied message then
71 * re-directs to the mainpage.
72 *
73 * @return void
74 */
75function access_denied() {
76	if( !auth_is_user_authenticated() ) {
77		if( basename( $_SERVER['SCRIPT_NAME'] ) != auth_login_page() ) {
78			$t_return_page = $_SERVER['SCRIPT_NAME'];
79			if( isset( $_SERVER['QUERY_STRING'] ) ) {
80				$t_return_page .= '?' . $_SERVER['QUERY_STRING'];
81			}
82			$t_return_page = string_url( string_sanitize_url( $t_return_page ) );
83			print_header_redirect( auth_login_page( 'return=' . $t_return_page ) );
84		}
85	} else {
86		if( current_user_is_anonymous() ) {
87			if( basename( $_SERVER['SCRIPT_NAME'] ) != auth_login_page() ) {
88				$t_return_page = $_SERVER['SCRIPT_NAME'];
89				if( isset( $_SERVER['QUERY_STRING'] ) ) {
90					$t_return_page .= '?' . $_SERVER['QUERY_STRING'];
91				}
92				$t_return_page = string_url( string_sanitize_url( $t_return_page ) );
93				echo '<p class="center">' . error_string( ERROR_ACCESS_DENIED ) . '</p><p class="center">';
94				print_link_button( auth_login_page( 'return=' . $t_return_page ), lang_get( 'login' ) );
95				echo '</p><p class="center">';
96				print_link_button(
97					helper_mantis_url( config_get_global( 'default_home_page' ) ),
98					lang_get( 'proceed' )
99				);
100				echo '</p>';
101			}
102		} else {
103			layout_page_header();
104			layout_admin_page_begin();
105			echo '<div class="space-10"></div>';
106			html_operation_failure(
107				helper_mantis_url( config_get_global( 'default_home_page' ) ),
108				error_string( ERROR_ACCESS_DENIED )
109			);
110			layout_admin_page_end();
111		}
112	}
113	exit;
114}
115
116/**
117 * retrieves and returns access matrix for a project from cache or caching if required.
118 * @param integer $p_project_id Integer representing project identifier.
119 * @return array returns an array of users->accesslevel for the given user
120 * @access private
121 */
122function access_cache_matrix_project( $p_project_id ) {
123	global $g_cache_access_matrix, $g_cache_access_matrix_project_ids;
124
125	if( ALL_PROJECTS == (int)$p_project_id ) {
126		return array();
127	}
128
129	if( !in_array( (int)$p_project_id, $g_cache_access_matrix_project_ids ) ) {
130		db_param_push();
131		$t_query = 'SELECT user_id, access_level FROM {project_user_list} WHERE project_id=' . db_param();
132		$t_result = db_query( $t_query, array( (int)$p_project_id ) );
133		while( $t_row = db_fetch_array( $t_result ) ) {
134			$g_cache_access_matrix[(int)$t_row['user_id']][(int)$p_project_id] = (int)$t_row['access_level'];
135		}
136
137		$g_cache_access_matrix_project_ids[] = (int)$p_project_id;
138	}
139
140	$t_results = array();
141
142	foreach( $g_cache_access_matrix as $t_user ) {
143		if( isset( $t_user[(int)$p_project_id] ) ) {
144			$t_results[(int)$p_project_id] = $t_user[(int)$p_project_id];
145		}
146	}
147
148	return $t_results;
149}
150
151/**
152 * retrieves and returns access matrix for a user from cache or caching if required.
153 * @param integer $p_user_id Integer representing user identifier.
154 * @return array returns an array of projects->accesslevel for the given user
155 * @access private
156 */
157function access_cache_matrix_user( $p_user_id ) {
158	global $g_cache_access_matrix, $g_cache_access_matrix_user_ids;
159
160	if( !in_array( (int)$p_user_id, $g_cache_access_matrix_user_ids ) ) {
161		db_param_push();
162		$t_query = 'SELECT project_id, access_level FROM {project_user_list} WHERE user_id=' . db_param();
163		$t_result = db_query( $t_query, array( (int)$p_user_id ) );
164
165		# make sure we always have an array to return
166		$g_cache_access_matrix[(int)$p_user_id] = array();
167
168		while( $t_row = db_fetch_array( $t_result ) ) {
169			$g_cache_access_matrix[(int)$p_user_id][(int)$t_row['project_id']] = (int)$t_row['access_level'];
170		}
171
172		$g_cache_access_matrix_user_ids[] = (int)$p_user_id;
173	}
174
175	return $g_cache_access_matrix[(int)$p_user_id];
176}
177
178/**
179 * Check the a user's access against the given "threshold" and return true
180 * if the user can access, false otherwise.
181 * $p_threshold may be a single value, or an array. If it is a single
182 * value, treat it as a threshold so return true if user is >= threshold.
183 * If it is an array, look for exact matches to one of the values
184 * @param integer       $p_user_access_level User access level.
185 * @param integer|array $p_threshold         Access threshold, defaults to NOBODY.
186 * @return boolean true or false depending on whether given access level matches the threshold
187 * @access public
188 */
189function access_compare_level( $p_user_access_level, $p_threshold = NOBODY ) {
190	if( is_array( $p_threshold ) ) {
191		return( in_array( $p_user_access_level, $p_threshold ) );
192	} else {
193		return( $p_user_access_level >= $p_threshold );
194	}
195}
196
197/**
198 * This function only checks the user's global access level, ignoring any
199 * overrides they might have at a project level
200 * @param integer|null $p_user_id Integer representing user identifier, defaults to null to use current user.
201 * @return integer global access level
202 * @access public
203 */
204function access_get_global_level( $p_user_id = null ) {
205	if( $p_user_id === null ) {
206		$p_user_id = auth_get_current_user_id();
207	}
208
209	# Deal with not logged in silently in this case
210	# @@@ we may be able to remove this and just error
211	#     and once we default to anon login, we can remove it for sure
212	if( empty( $p_user_id ) && !auth_is_user_authenticated() ) {
213		return false;
214	}
215
216	return user_get_field( $p_user_id, 'access_level' );
217}
218
219/**
220 * Check the current user's access against the given value and return true
221 * if the user's access is equal to or higher, false otherwise.
222 * @param integer      $p_access_level Integer representing access level.
223 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
224 * @return boolean whether user has access level specified
225 * @access public
226 */
227function access_has_global_level( $p_access_level, $p_user_id = null ) {
228	# Short circuit the check in this case
229	if( NOBODY == $p_access_level ) {
230		return false;
231	}
232
233	if( $p_user_id === null ) {
234		$p_user_id = auth_get_current_user_id();
235	}
236
237	$t_access_level = access_get_global_level( $p_user_id );
238
239	return access_compare_level( $t_access_level, $p_access_level );
240}
241
242/**
243 * Check if the user has the specified global access level
244 * and deny access to the page if not
245 * @see access_has_global_level
246 * @param integer      $p_access_level Integer representing access level.
247 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
248 * @access public
249 * @return void
250 */
251function access_ensure_global_level( $p_access_level, $p_user_id = null ) {
252	if( !access_has_global_level( $p_access_level, $p_user_id ) ) {
253		access_denied();
254	}
255}
256
257/**
258 * This function checks the project access level first (for the current project
259 * if none is specified) and if the user is not listed, it falls back on the
260 * user's global access level.
261 * @param integer      $p_project_id Integer representing project id to check access against.
262 * @param integer|null $p_user_id    Integer representing user id, defaults to null to use current user.
263 * @return integer access level user has to given project
264 * @access public
265 */
266function access_get_project_level( $p_project_id = null, $p_user_id = null ) {
267	if( null === $p_user_id ) {
268		$p_user_id = auth_get_current_user_id();
269	}
270
271	# Deal with not logged in silently in this case
272	# @todo we may be able to remove this and just error and once we default to anon login, we can remove it for sure
273	if( empty( $p_user_id ) && !auth_is_user_authenticated() ) {
274		return ANYBODY;
275	}
276
277	if( null === $p_project_id ) {
278		$p_project_id = helper_get_current_project();
279	}
280
281	$t_global_access_level = access_get_global_level( $p_user_id );
282
283	if( ALL_PROJECTS == $p_project_id || user_is_administrator( $p_user_id ) ) {
284		return $t_global_access_level;
285	} else {
286		$t_project_access_level = access_get_local_level( $p_user_id, $p_project_id );
287		$t_project_view_state = project_get_field( $p_project_id, 'view_state' );
288
289		# Try to use the project access level.
290		# If the user is not listed in the project, then try to fall back
291		#  to the global access level
292		if( false === $t_project_access_level ) {
293			# If the project is private and the user isn't listed, then they
294			# must have the private_project_threshold access level to get in.
295			if( VS_PRIVATE == $t_project_view_state ) {
296				if( access_compare_level( $t_global_access_level, config_get( 'private_project_threshold', null, null, ALL_PROJECTS ) ) ) {
297					return $t_global_access_level;
298				} else {
299					return ANYBODY;
300				}
301			} else {
302				# project access not set, but the project is public
303				return $t_global_access_level;
304			}
305		} else {
306			# project specific access was set
307			return $t_project_access_level;
308		}
309	}
310}
311
312/**
313 * Check the current user's access against the given value and return true
314 * if the user's access is equal to or higher, false otherwise.
315 * @param integer|array $p_access_level Threshold representing an access level.
316 * @param integer       $p_project_id   Integer representing project id to check access against.
317 * @param integer|null  $p_user_id      Integer representing user id, defaults to null to use current user.
318 * @return boolean whether user has access level specified
319 * @access public
320 */
321function access_has_project_level( $p_access_level, $p_project_id = null, $p_user_id = null ) {
322	# Short circuit the check in this case
323	if( NOBODY == $p_access_level ) {
324		return false;
325	}
326
327	if( null === $p_user_id ) {
328		$p_user_id = auth_get_current_user_id();
329	}
330	if( null === $p_project_id ) {
331		$p_project_id = helper_get_current_project();
332	}
333
334	$t_access_level = access_get_project_level( $p_project_id, $p_user_id );
335
336	return access_compare_level( $t_access_level, $p_access_level );
337}
338
339/**
340 * Filters an array of project ids, based on an access level, returning an array
341 * containing only those projects which meet said access level.
342 * An optional limit for the number of results is provided as a shortcut for access checks.
343 *
344 * @param integer|array|string  $p_access_level Parameter representing access level threshold, may be:
345 *                                              - integer: for a simple threshold
346 *                                              - array: for an array threshold
347 *                                              - string: for a threshold option which will be evaluated
348 *                                                 for each project context
349 * @param array                 $p_project_ids  Array of project ids to check access against, default to null
350 *                                               to use all user accessible projects
351 * @param integer|null          $p_user_id      Integer representing user id, defaults to null to use current user.
352 * @param integer               $p_limit        Maximum number of results, default is 0 for all results
353 * @return array                The filtered array of project ids
354 */
355function access_project_array_filter( $p_access_level, array $p_project_ids = null, $p_user_id = null, $p_limit = 0 ) {
356	# Short circuit the check in this case
357	if( NOBODY == $p_access_level ) {
358		return array();
359	}
360
361	if( null === $p_user_id ) {
362		$p_user_id = auth_get_current_user_id();
363	}
364	if( null === $p_project_ids ) {
365		$p_project_ids = user_get_all_accessible_projects( $p_user_id );
366	}
367
368	# Determine if parameter is a configuration string to be evaluated for each project
369	$t_is_config_string = ( is_string( $p_access_level ) && !is_numeric( $p_access_level ) );
370
371	# if config will be evaluated for each project, prepare a default value
372	if( $t_is_config_string ) {
373		$t_default = config_get( $p_access_level, null, $p_user_id, ALL_PROJECTS );
374		if( null === $t_default ) {
375			$t_default = config_get_global( $p_access_level );
376		}
377	}
378
379	$t_check_level = $p_access_level;
380	$t_filtered_projects = array();
381	foreach( $p_project_ids as $t_project_id ) {
382		# If a config string is provided, evaluate for each project
383		if( $t_is_config_string ) {
384			$t_check_level = config_get( $p_access_level, $t_default, $p_user_id, $t_project_id );
385		}
386		if( access_has_project_level( $t_check_level, $t_project_id, $p_user_id ) ) {
387			$t_filtered_projects[] = $t_project_id;
388			# Shortcut if the result limit has been reached
389			if( --$p_limit == 0 ) {
390				break;
391			}
392		}
393	}
394
395	return $t_filtered_projects;
396}
397
398/**
399 * Check the current user's access against the given value, in each of the provided projects,
400 * and return true if the user's access is equal to or higher in any of the projects, false otherwise.
401 * @param integer|array|string  $p_access_level Parameter representing access level threshold, may be:
402 *                                              - integer: for a simple threshold
403 *                                              - array: for an array threshold
404 *                                              - string: for a threshold option which will be evaluated
405 *                                                 for each project context
406 * @param array                 $p_project_ids  Array of project ids to check access against, default to null
407 *                                               to use all user accessible projects
408 * @param integer|null          $p_user_id      Integer representing user id, defaults to null to use current user.
409 * @return boolean              True if user has the specified access level for any of the projects
410 * @access public
411 */
412function access_has_any_project_level( $p_access_level, array $p_project_ids = null, $p_user_id = null ) {
413	# We only need 1 matching project to return positive
414	$t_matches = access_project_array_filter( $p_access_level, $p_project_ids, $p_user_id, 1 );
415	return !empty( $t_matches );
416}
417
418/**
419 * Check if the user has the specified access level in any of the given projects
420 * Refer to access_has_any_project_level() for detailed parameter information
421 * @param integer|array|string $p_access_level  Parameter representing access level threshold
422 * @param array $p_project_ids                  Array of project ids to check access against
423 * @param integer|null $p_user_id
424 */
425function access_ensure_any_project_level( $p_access_level, array $p_project_ids = null, $p_user_id = null ) {
426	if( !access_has_any_project_level( $p_access_level, $p_project_ids, $p_user_id ) ) {
427		access_denied();
428	}
429}
430
431/**
432 * Check if the user has the specified access level for the given project
433 * and deny access to the page if not
434 * @see access_has_project_level
435 * @param integer      $p_access_level Integer representing access level.
436 * @param integer|null $p_project_id   Integer representing project id to check access against, defaults to null to use current project.
437 * @param integer|null $p_user_id      Integer representing user identifier, defaults to null to use current user.
438 * @access public
439 * @return void
440 */
441function access_ensure_project_level( $p_access_level, $p_project_id = null, $p_user_id = null ) {
442	if( !access_has_project_level( $p_access_level, $p_project_id, $p_user_id ) ) {
443		access_denied();
444	}
445}
446
447/**
448 * Check whether the user has the specified access level for any project project
449 *
450 * Warning: this function may mislead into incorrect validations. Usually you want to
451 * check that a user meets a threshold for any project, but that threshold may be configured
452 * differently for each project, and the user may also have different access levels in each
453 * project due to private projects assignment.
454 * In that scenario, $p_access_level can't be a static threshold, but a "threshold identifier"
455 * instead, that must be evaluated for each project.
456 * Function "access_has_any_project_level()" provides that functionality, also covers the basic
457 * usage of this function.
458 * For such reasons, this function has been deprecated.
459 *
460 * @param integer      $p_access_level Integer representing access level.
461 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
462 * @return boolean whether user has access level specified
463 * @access public
464 * @deprecated	access_has_any_project_level() should be used in preference to this function (since verrsion 2.6)
465 */
466function access_has_any_project( $p_access_level, $p_user_id = null ) {
467	error_parameters( __FUNCTION__ . '()', 'access_has_any_project_level()' );
468	trigger_error( ERROR_DEPRECATED_SUPERSEDED, DEPRECATED );
469
470	# Short circuit the check in this case
471	if( NOBODY == $p_access_level ) {
472		return false;
473	}
474
475	if( null === $p_user_id ) {
476		$p_user_id = auth_get_current_user_id();
477	}
478
479	$t_projects = project_get_all_rows();
480	foreach( $t_projects as $t_project ) {
481		if( access_has_project_level( $p_access_level, $t_project['id'], $p_user_id ) ) {
482			return true;
483		}
484	}
485
486	return false;
487}
488
489/**
490 * Check the current user's access against the given value and return true
491 * if the user's access is equal to or higher, false otherwise.
492 * This function looks up the bug's project and performs an access check
493 * against that project
494 * @param integer      $p_access_level Integer representing access level.
495 * @param integer      $p_bug_id       Integer representing bug id to check access against.
496 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
497 * @return boolean whether user has access level specified
498 * @access public
499 */
500function access_has_bug_level( $p_access_level, $p_bug_id, $p_user_id = null ) {
501	if( $p_user_id === null ) {
502		$p_user_id = auth_get_current_user_id();
503	}
504
505	# Deal with not logged in silently in this case
506	# @@@ we may be able to remove this and just error
507	#     and once we default to anon login, we can remove it for sure
508	if( empty( $p_user_id ) && !auth_is_user_authenticated() ) {
509		return false;
510	}
511
512	# Check the requested access level, shortcut to fail if not satisfied
513	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
514	$t_access_level = access_get_project_level( $t_project_id, $p_user_id );
515	if( !access_compare_level( $t_access_level, $p_access_level ) ){
516		return false;
517	}
518
519	# If the level is met, we still need to verify that user has access to the issue
520
521	# Check if the bug is private
522	$t_bug_is_user_reporter = bug_is_user_reporter( $p_bug_id, $p_user_id );
523	if( !$t_bug_is_user_reporter && bug_get_field( $p_bug_id, 'view_state' ) == VS_PRIVATE ) {
524		$t_private_bug_threshold = config_get( 'private_bug_threshold', null, $p_user_id, $t_project_id );
525		if( !access_compare_level( $t_access_level, $t_private_bug_threshold ) ) {
526			return false;
527		}
528	}
529
530	# Check special limits
531	# Limited view means this user can only view the issues they reported, is handling, or monitoring
532	if( access_has_limited_view( $t_project_id, $p_user_id ) ) {
533		$t_allowed = $t_bug_is_user_reporter;
534		if( !$t_allowed ) {
535			$t_allowed = bug_is_user_handler( $p_bug_id, $p_user_id );
536		}
537		if( !$t_allowed ) {
538			$t_allowed = user_is_monitoring_bug( $p_user_id, $p_bug_id );
539		}
540		if( !$t_allowed ) {
541			return false;
542		}
543	}
544
545	return true;
546}
547
548/**
549 * Filter the provided array of user ids to those who has the specified access level for the
550 * specified bug.
551 * @param  int $p_access_level   The access level.
552 * @param  int $p_bug_id         The bug id.
553 * @param  array $p_user_ids     The array of user ids.
554 * @return array filtered array of user ids.
555 */
556function access_has_bug_level_filter( $p_access_level, $p_bug_id, $p_user_ids ) {
557	$t_users_ids_with_access = array();
558	foreach( $p_user_ids as $t_user_id ) {
559		if( access_has_bug_level( $p_access_level, $p_bug_id, $t_user_id ) ) {
560			$t_users_ids_with_access[] = $t_user_id;
561		}
562	}
563
564	return $t_users_ids_with_access;
565}
566
567/**
568 * Check if the user has the specified access level for the given bug
569 * and deny access to the page if not
570 * @see access_has_bug_level
571 * @param integer      $p_access_level Integer representing access level.
572 * @param integer      $p_bug_id       Integer representing bug id to check access against.
573 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
574 * @return void
575 * @access public
576 */
577function access_ensure_bug_level( $p_access_level, $p_bug_id, $p_user_id = null ) {
578	if( !access_has_bug_level( $p_access_level, $p_bug_id, $p_user_id ) ) {
579		access_denied();
580	}
581}
582
583/**
584 * Check the current user's access against the given value and return true
585 * if the user's access is equal to or higher, false otherwise.
586 * This function looks up the bugnote's bug and performs an access check
587 * against that bug
588 * @param integer      $p_access_level Integer representing access level.
589 * @param integer      $p_bugnote_id   Integer representing bugnote id to check access against.
590 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
591 * @return boolean whether user has access level specified
592 * @access public
593 */
594function access_has_bugnote_level( $p_access_level, $p_bugnote_id, $p_user_id = null ) {
595	if( null === $p_user_id ) {
596		$p_user_id = auth_get_current_user_id();
597	}
598
599	$t_bug_id = bugnote_get_field( $p_bugnote_id, 'bug_id' );
600	$t_project_id = bug_get_field( $t_bug_id, 'project_id' );
601
602	# If the bug is private and the user is not the reporter, then the
603	# the user must also have higher access than private_bug_threshold
604	if( bugnote_get_field( $p_bugnote_id, 'view_state' ) == VS_PRIVATE && !bugnote_is_user_reporter( $p_bugnote_id, $p_user_id ) ) {
605		$t_private_bugnote_threshold = config_get( 'private_bugnote_threshold', null, $p_user_id, $t_project_id );
606		$p_access_level = max( $p_access_level, $t_private_bugnote_threshold );
607	}
608
609	return access_has_bug_level( $p_access_level, $t_bug_id, $p_user_id );
610}
611
612/**
613 * Filter the provided array of user ids to those who has the specified access level for the
614 * specified bugnote.
615 * @param  int $p_access_level   The access level.
616 * @param  int $p_bugnote_id     The bugnote id.
617 * @param  array $p_user_ids     The array of user ids.
618 * @return array filtered array of user ids.
619 */
620function access_has_bugnote_level_filter( $p_access_level, $p_bugnote_id, $p_user_ids ) {
621	$t_users_ids_with_access = array();
622	foreach( $p_user_ids as $t_user_id ) {
623		if( access_has_bugnote_level( $p_access_level, $p_bugnote_id, $t_user_id ) ) {
624			$t_users_ids_with_access[] = $t_user_id;
625		}
626	}
627
628	return $t_users_ids_with_access;
629}
630
631/**
632 * Check if the user has the specified access level for the given bugnote
633 * and deny access to the page if not
634 * @see access_has_bugnote_level
635 * @param integer      $p_access_level Integer representing access level.
636 * @param integer      $p_bugnote_id   Integer representing bugnote id to check access against.
637 * @param integer|null $p_user_id      Integer representing user id, defaults to null to use current user.
638 * @access public
639 * @return void
640 */
641function access_ensure_bugnote_level( $p_access_level, $p_bugnote_id, $p_user_id = null ) {
642	if( !access_has_bugnote_level( $p_access_level, $p_bugnote_id, $p_user_id ) ) {
643		access_denied();
644	}
645}
646
647/**
648 * Check if the specified bug can be closed
649 * @param BugData      $p_bug     Bug to check access against.
650 * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user.
651 * @return boolean true if user can close the bug
652 * @access public
653 */
654function access_can_close_bug( BugData $p_bug, $p_user_id = null ) {
655	if( bug_is_closed( $p_bug->id ) ) {
656		# Can't close a bug that's already closed
657		return false;
658	}
659
660	if( null === $p_user_id ) {
661		$p_user_id = auth_get_current_user_id();
662	}
663
664	# If allow_reporter_close is enabled, then reporters can close their own bugs
665	# if they are in resolved status
666	if( ON == config_get( 'allow_reporter_close', null, null, $p_bug->project_id )
667		&& bug_is_user_reporter( $p_bug->id, $p_user_id )
668		&& bug_is_resolved( $p_bug->id )
669	) {
670		return true;
671	}
672
673	$t_closed_status = config_get( 'bug_closed_status_threshold', null, null, $p_bug->project_id );
674	$t_closed_status_threshold = access_get_status_threshold( $t_closed_status, $p_bug->project_id );
675	return access_has_bug_level( $t_closed_status_threshold, $p_bug->id, $p_user_id );
676}
677
678/**
679 * Make sure that the user can close the specified bug
680 * @see access_can_close_bug
681 * @param BugData      $p_bug     Bug to check access against.
682 * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user.
683 * @access public
684 * @return void
685 */
686function access_ensure_can_close_bug( BugData $p_bug, $p_user_id = null ) {
687	if( !access_can_close_bug( $p_bug, $p_user_id ) ) {
688		access_denied();
689	}
690}
691
692/**
693 * Check if the specified bug can be reopened
694 * @param BugData      $p_bug     Bug to check access against.
695 * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user.
696 * @return boolean whether user has access to reopen bugs
697 * @access public
698 */
699function access_can_reopen_bug( BugData $p_bug, $p_user_id = null ) {
700	if( !bug_is_resolved( $p_bug->id ) ) {
701		# Can't reopen a bug that's not resolved
702		return false;
703	}
704
705	if( $p_user_id === null ) {
706		$p_user_id = auth_get_current_user_id();
707	}
708
709	$t_reopen_status = config_get( 'bug_reopen_status', null, null, $p_bug->project_id );
710
711	# Reopen status must be reachable by workflow
712	if( !bug_check_workflow( $p_bug->status, $t_reopen_status ) ) {
713		return false;
714	}
715
716	# If allow_reporter_reopen is enabled, then reporters can always reopen
717	# their own bugs as long as their access level is reporter or above
718	if( ON == config_get( 'allow_reporter_reopen', null, null, $p_bug->project_id )
719		&& bug_is_user_reporter( $p_bug->id, $p_user_id )
720		&& access_has_project_level( config_get( 'report_bug_threshold', null, $p_user_id, $p_bug->project_id ), $p_bug->project_id, $p_user_id )
721	) {
722		return true;
723	}
724
725	# Other users's access level must allow them to reopen bugs
726	$t_reopen_bug_threshold = config_get( 'reopen_bug_threshold', null, null, $p_bug->project_id );
727	if( access_has_bug_level( $t_reopen_bug_threshold, $p_bug->id, $p_user_id ) ) {
728
729		# User must be allowed to change status to reopen status
730		$t_reopen_status_threshold = access_get_status_threshold( $t_reopen_status, $p_bug->project_id );
731		return access_has_bug_level( $t_reopen_status_threshold, $p_bug->id, $p_user_id );
732	}
733
734	return false;
735}
736
737/**
738 * Make sure that the user can reopen the specified bug.
739 * Calls access_denied if user has no access to terminate script
740 * @see access_can_reopen_bug
741 * @param BugData      $p_bug     Bug to check access against.
742 * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user.
743 * @access public
744 * @return void
745 */
746function access_ensure_can_reopen_bug( BugData $p_bug, $p_user_id = null ) {
747	if( !access_can_reopen_bug( $p_bug, $p_user_id ) ) {
748		access_denied();
749	}
750}
751
752/**
753 * get the user's access level specific to this project.
754 * return false (0) if the user has no access override here
755 * @param integer $p_user_id    Integer representing user id.
756 * @param integer $p_project_id Integer representing project id.
757 * @return boolean|integer returns false (if no access) or an integer representing level of access
758 * @access public
759 */
760function access_get_local_level( $p_user_id, $p_project_id ) {
761	global $g_cache_access_matrix, $g_cache_access_matrix_project_ids;
762
763	$p_project_id = (int)$p_project_id;
764	$p_user_id = (int)$p_user_id;
765
766	if( in_array( $p_project_id, $g_cache_access_matrix_project_ids ) ) {
767		if( isset( $g_cache_access_matrix[$p_user_id][$p_project_id] ) ) {
768			return $g_cache_access_matrix[$p_user_id][$p_project_id];
769		} else {
770			return false;
771		}
772	}
773
774	$t_project_level = access_cache_matrix_user( $p_user_id );
775
776	if( isset( $t_project_level[$p_project_id] ) ) {
777		return $t_project_level[$p_project_id];
778	} else {
779		return false;
780	}
781}
782
783/**
784 * get the access level required to change the issue to the new status
785 * If there is no specific differentiated access level, use the
786 * generic update_bug_status_threshold.
787 * @param integer $p_status     Status.
788 * @param integer $p_project_id Default value ALL_PROJECTS.
789 * @return integer integer representing user level e.g. DEVELOPER
790 * @access public
791 */
792function access_get_status_threshold( $p_status, $p_project_id = ALL_PROJECTS ) {
793	$t_thresh_array = config_get( 'set_status_threshold', null, null, $p_project_id );
794	if( isset( $t_thresh_array[(int)$p_status] ) ) {
795		return (int)$t_thresh_array[(int)$p_status];
796	} else {
797		if( $p_status == config_get( 'bug_submit_status', null, null, $p_project_id ) ) {
798			return config_get( 'report_bug_threshold', null, null, $p_project_id );
799		} else {
800			return config_get( 'update_bug_status_threshold', null, null, $p_project_id );
801		}
802	}
803}
804
805/**
806 * Given a access level, return the appropriate string for it
807 * @param integer $p_access_level
808 * @return string
809 */
810function access_level_get_string( $p_access_level ) {
811	if( $p_access_level > ANYBODY ) {
812		$t_access_level_string = get_enum_element( 'access_levels', $p_access_level );
813	} else {
814		$t_access_level_string = lang_get( 'no_access' );
815	}
816	return $t_access_level_string;
817}
818
819/**
820 * Return the minimum access level, as integer, that matches the threshold.
821 * $p_threshold may be a single value, or an array. If it is a single
822 * value, returns that number. If it is an array, return the value of the
823 * smallest element
824 * @param integer|array $p_threshold         Access threshold
825 * @return integer		Integer value for an access level.
826 */
827function access_threshold_min_level( $p_threshold ) {
828	if( is_array( $p_threshold ) ) {
829		if( empty( $p_threshold ) ) {
830			return NOBODY;
831		} else {
832			sort( $p_threshold );
833			return( reset( $p_threshold ) );
834		}
835	} else {
836		return $p_threshold;
837	}
838
839}
840
841/**
842 * Checks if the user can view the handler for the bug.
843 * @param BugData      $p_bug     Bug to check access against.
844 * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user.
845 * @return boolean whether user can view the handler user.
846 */
847function access_can_see_handler_for_bug( BugData $p_bug, $p_user_id = null ) {
848	if( null === $p_user_id ) {
849		$t_user_id = auth_get_current_user_id();
850	} else {
851		$t_user_id = $p_user_id;
852	}
853
854	# handler can be viewed if allowed by access level, OR the user himself is the handler
855	$t_can_view_handler =
856		( $p_bug->handler_id == $t_user_id )
857		|| access_has_bug_level(
858			config_get( 'view_handler_threshold', null, $t_user_id, $p_bug->project_id ),
859			$p_bug->id );
860
861	return $t_can_view_handler;
862}
863
864/**
865 * Parse access level reference array parsed from json.
866 *
867 * @param array $p_access The access level
868 * @return integer The access level
869 * @throws ClientException Access level is invalid or not specified.
870 */
871function access_parse_array( array $p_access ) {
872	$t_access_levels_enum = config_get( 'access_levels_enum_string' );
873	$t_access_level = false;
874
875	if( isset( $p_access['id'] ) ) {
876		$t_access_level = (int)$p_access['id'];
877
878		# Make sure the provided id is valid
879		if( !MantisEnum::hasValue( $t_access_levels_enum, $t_access_level ) ) {
880			$t_access_level = false;
881		}
882	}
883
884	if( isset( $p_access['name'] ) ) {
885		$t_access_level = MantisEnum::getValue( $t_access_levels_enum, $p_access['name'] );
886	}
887
888	if( $t_access_level === false ) {
889		throw new ClientException(
890			'Invalid access level',
891			ERROR_INVALID_FIELD_VALUE,
892			array( 'access_level' ) );
893	}
894
895	return $t_access_level;
896}
897
898/**
899 * Returns true if the user has limited view to issues in the specified project.
900 *
901 * @param integer $p_project_id   Project id, or null for current project
902 * @param integer $p_user_id      User id, or null for current user
903 * @return boolean	Whether limited view applies
904 *
905 * @see $g_limit_view_unless_threshold
906 * @see $g_limit_reporters
907 */
908function access_has_limited_view( $p_project_id = null, $p_user_id = null ) {
909	$t_user_id = ( null === $p_user_id ) ? auth_get_current_user_id() : $p_user_id;
910	$t_project_id = ( null === $p_project_id ) ? helper_get_current_project() : $p_project_id;
911
912	# Old 'limit_reporters' option was previously only supported for ALL_PROJECTS,
913	# Use this option if set, otherwise, check the new option for "unlimited view" threshold
914	$t_old_limit_reporters = config_get( 'limit_reporters', null, $t_user_id, ALL_PROJECTS );
915	$t_threshold_can_view = NOBODY;
916	if( ON != $t_old_limit_reporters ) {
917		$t_threshold_can_view = config_get( 'limit_view_unless_threshold', null, $t_user_id, $t_project_id );
918	} else {
919		# If old 'limit_reporters' option is enabled, use that setting
920		# Note that the effective threshold can vary for each project, based on
921		# the reporting threshold configuration.
922		# To improve performance, esp. when processing for several projects, we
923		# build a static array holding that threshold for each project
924		static $s_thresholds = array();
925		if( !isset( $s_thresholds[$t_project_id] ) ) {
926			$t_report_bug_threshold = config_get( 'report_bug_threshold', null, $t_user_id, $t_project_id );
927			if( empty( $t_report_bug_threshold ) ) {
928				$s_thresholds[$t_project_id] = NOBODY;
929			} else {
930				$s_thresholds[$t_project_id] = access_threshold_min_level( $t_report_bug_threshold ) + 1;
931			}
932		}
933		$t_threshold_can_view = $s_thresholds[$t_project_id];
934	}
935
936	$t_project_level = access_get_project_level( $p_project_id, $p_user_id );
937	return !access_compare_level( $t_project_level, $t_threshold_can_view );
938}
939
940/**
941 * Return true if user is allowed to view bug revisions.
942 *
943 * User must have $g_bug_revision_view_threshold or be the bug's reporter.
944 *
945 * @param int $p_bug_id
946 * @param int $p_user_id
947 *
948 * @return bool
949 */
950function access_can_view_bug_revisions( $p_bug_id, $p_user_id = null ) {
951	if( !bug_exists( $p_bug_id ) ) {
952		return false;
953	}
954	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
955	$t_user_id = null === $p_user_id ? auth_get_current_user_id() : $p_user_id;
956
957	$t_has_access = access_has_bug_level(
958		config_get( 'bug_revision_view_threshold', null, $t_user_id, $t_project_id ),
959		$p_bug_id,
960		$t_user_id
961	);
962
963	return $t_has_access || bug_is_user_reporter( $p_bug_id, $t_user_id );
964}
965
966/**
967 * Return true if user is allowed to view bugnote revisions.
968 *
969 * User must have $g_bug_revision_view_threshold or be the bugnote's reporter.
970 *
971 * @param int $p_bugnote_id
972 * @param int $p_user_id
973 *
974 * @return bool
975 */
976function access_can_view_bugnote_revisions( $p_bugnote_id, $p_user_id = null ) {
977	if( !bugnote_exists( $p_bugnote_id ) ) {
978		return false;
979	}
980	$t_bug_id = bugnote_get_field( $p_bugnote_id, 'bug_id' );
981	$t_project_id = bug_get_field( $t_bug_id, 'project_id' );
982	$t_user_id = null === $p_user_id ? auth_get_current_user_id() : $p_user_id;
983
984	$t_has_access = access_has_bugnote_level(
985		config_get( 'bug_revision_view_threshold', null, $t_user_id, $t_project_id ),
986		$p_bugnote_id,
987		$t_user_id
988	);
989
990
991	return $t_has_access || bugnote_is_user_reporter( $p_bugnote_id, $t_user_id );
992}
993