diff --git a/bug_view_inc.php b/bug_view_inc.php index caf8af6ac8..3bdb99a8e6 100644 --- a/bug_view_inc.php +++ b/bug_view_inc.php @@ -176,7 +176,7 @@ $t_show_last_updated = in_array( 'last_updated', $t_fields ); $t_last_updated = $t_show_last_updated ? date( config_get( 'normal_date_format' ), $t_bug->last_updated ) : ''; -$t_show_tags = in_array( 'tags', $t_fields ) && access_has_global_level( config_get( 'tag_view_threshold' ) ); +$t_show_tags = in_array( 'tags', $t_fields ) && access_has_bug_level( config_get( 'tag_view_threshold' ), $t_bug_id ); $t_bug_overdue = bug_is_overdue( $f_bug_id ); diff --git a/core/access_api.php b/core/access_api.php index d48a149a5a..acd650c3a8 100644 --- a/core/access_api.php +++ b/core/access_api.php @@ -334,10 +334,11 @@ function access_has_project_level( $p_access_level, $p_project_id = null, $p_use return access_compare_level( $t_access_level, $p_access_level ); } - /** - * Check the current user's access against the given value, in each of the provided projects, - * and return true if the user's access is equal to or higher in any of the projects, false otherwise. + * Filters an array of project ids, based on an access level, returning an array + * containing only those projects which meet said access level. + * An optional limit for the number of results is provided as a shortcut for access checks. + * * @param integer|array|string $p_access_level Parameter representing access level threshold, may be: * - integer: for a simple threshold * - array: for an array threshold @@ -346,13 +347,13 @@ function access_has_project_level( $p_access_level, $p_project_id = null, $p_use * @param array $p_project_ids Array of project ids to check access against, default to null * to use all user accesible projects * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user. - * @return boolean whether user has access level specified - * @access public + * @param integer $p_limit Maximum number of results, default is 0 for all results + * @return array The filtered array of project ids */ -function access_has_any_project_level( $p_access_level, array $p_project_ids = null, $p_user_id = null ) { +function access_project_array_filter( $p_access_level, array $p_project_ids = null, $p_user_id = null, $p_limit = 0 ) { # Short circuit the check in this case if( NOBODY == $p_access_level ) { - return false; + return array(); } if( null === $p_user_id ) { @@ -371,19 +372,42 @@ function access_has_any_project_level( $p_access_level, array $p_project_ids = n } $t_check_level = $p_access_level; - $t_has_access = false; + $t_filtered_projects = array(); foreach( $p_project_ids as $t_project_id ) { # If a config string is provided, evaluate for each project if( is_string( $p_access_level ) ) { $t_check_level = config_get( $p_access_level, $t_default, $p_user_id, $t_project_id ); } if( access_has_project_level( $t_check_level, $t_project_id, $p_user_id ) ) { - $t_has_access = true; - break; + $t_filtered_projects[] = $t_project_id; + # Shortcut if the result limit has been reached + if( --$p_limit == 0 ) { + break; + } } } - return $t_has_access; + return $t_filtered_projects; +} + +/** + * Check the current user's access against the given value, in each of the provided projects, + * and return true if the user's access is equal to or higher in any of the projects, false otherwise. + * @param integer|array|string $p_access_level Parameter representing access level threshold, may be: + * - integer: for a simple threshold + * - array: for an array threshold + * - string: for a threshold option which will be evaluated + * for each project context + * @param array $p_project_ids Array of project ids to check access against, default to null + * to use all user accesible projects + * @param integer|null $p_user_id Integer representing user id, defaults to null to use current user. + * @return boolean True if user has the specified access level for any of the projects + * @access public + */ +function access_has_any_project_level( $p_access_level, array $p_project_ids = null, $p_user_id = null ) { + # We only need 1 matching project to return positive + $t_matches = access_project_array_filter( $p_access_level, $p_project_ids, $p_user_id, 1 ); + return !empty( $t_matches ); } /** diff --git a/core/filter_api.php b/core/filter_api.php index 21aaf40848..06252dd8cf 100644 --- a/core/filter_api.php +++ b/core/filter_api.php @@ -1400,90 +1400,27 @@ function filter_get_bug_rows_query_clauses( array $p_filter, $p_project_id = nul ' JOIN {project} ON {project}.id = {bug}.project_id', ); - # normalize the project filtering into an array $t_project_ids - if( FILTER_VIEW_TYPE_SIMPLE == $t_view_type ) { - log_event( LOG_FILTERING, 'Simple Filter' ); - $t_project_ids = array( - $t_project_id, - ); - $t_include_sub_projects = true; - } else { - log_event( LOG_FILTERING, 'Advanced Filter' ); - if( !is_array( $t_filter[FILTER_PROPERTY_PROJECT_ID] ) ) { - $t_project_ids = array( - (int)$t_filter[FILTER_PROPERTY_PROJECT_ID], - ); - } else { - $t_project_ids = array_map( 'intval', $t_filter[FILTER_PROPERTY_PROJECT_ID] ); - } - - $t_include_sub_projects = (( count( $t_project_ids ) == 1 ) && ( ( $t_project_ids[0] == META_FILTER_CURRENT ) || ( $t_project_ids[0] == ALL_PROJECTS ) ) ); - } - - log_event( LOG_FILTERING, 'project_ids = @P' . implode( ', @P', $t_project_ids ) ); - log_event( LOG_FILTERING, 'include sub-projects = ' . ( $t_include_sub_projects ? '1' : '0' ) ); - - # if the array has ALL_PROJECTS, then reset the array to only contain ALL_PROJECTS. - # replace META_FILTER_CURRENT with the actually current project id. - $t_all_projects_found = false; - $t_new_project_ids = array(); - foreach( $t_project_ids as $t_pid ) { - if( $t_pid == META_FILTER_CURRENT ) { - $t_pid = $t_project_id; - } - - if( $t_pid == ALL_PROJECTS ) { - $t_all_projects_found = true; - log_event( LOG_FILTERING, 'all projects selected' ); - break; - } - - # filter out inaccessible projects. - if( !project_exists( $t_pid ) || !access_has_project_level( config_get( 'view_bug_threshold', null, $t_user_id, $t_pid ), $t_pid, $t_user_id ) ) { - log_event( LOG_FILTERING, 'Invalid or inaccessible project: ' . $t_pid ); - continue; - } - - $t_new_project_ids[] = $t_pid; - } + $t_included_project_ids = filter_get_included_projects( $t_filter, $t_project_id, $t_user_id ); + $t_all_accesible_projects = user_get_all_accessible_projects( $t_user_id ); + $t_project_diff = array_diff( $t_all_accesible_projects, $t_included_project_ids ); + # If the projects selected by the filter are the same as all accesible projects, + # we can assume that ALL_PROJECTS was used + $t_all_projects_found = empty( $t_project_diff ); $t_projects_query_required = true; - if( $t_all_projects_found ) { - if( user_is_administrator( $t_user_id ) ) { - log_event( LOG_FILTERING, 'all projects + administrator, hence no project filter.' ); - $t_projects_query_required = false; - } else { - $t_project_ids = user_get_accessible_projects( $t_user_id ); - } - } else { - $t_project_ids = $t_new_project_ids; + if( $t_all_projects_found && user_is_administrator( $t_user_id ) ) { + log_event( LOG_FILTERING, 'all projects + administrator, hence no project filter.' ); + $t_projects_query_required = false; } if( $t_projects_query_required ) { - # expand project ids to include sub-projects - if( $t_include_sub_projects ) { - $t_top_project_ids = $t_project_ids; - - foreach( $t_top_project_ids as $t_pid ) { - log_event( LOG_FILTERING, 'Getting sub-projects for project id @P' . $t_pid ); - $t_subproject_ids = user_get_all_accessible_subprojects( $t_user_id, $t_pid ); - if( !$t_subproject_ids ) { - continue; - } - $t_project_ids = array_merge( $t_project_ids, $t_subproject_ids ); - } - - $t_project_ids = array_unique( $t_project_ids ); - } # if no projects are accessible, then return an empty array. - if( count( $t_project_ids ) == 0 ) { + if( count( $t_included_project_ids ) == 0 ) { log_event( LOG_FILTERING, 'no accessible projects' ); return array(); } - log_event( LOG_FILTERING, 'project_ids after including sub-projects = @P' . implode( ', @P', $t_project_ids ) ); - # this array is to be populated with project ids for which we only want to show public issues. This is due to the limited # access of the current user. $t_public_only_project_ids = array(); @@ -1493,8 +1430,9 @@ function filter_get_bug_rows_query_clauses( array $p_filter, $p_project_id = nul $t_limited_projects = array(); # make sure the project rows are cached, as they will be used to check access levels. - project_cache_array_rows( $t_project_ids ); - foreach( $t_project_ids as $t_pid ) { + project_cache_array_rows( $t_included_project_ids ); + + foreach( $t_included_project_ids as $t_pid ) { # limit reporters to visible projects if( ( ON === $t_limit_reporters ) && ( !access_has_project_level( access_threshold_min_level( config_get( 'report_bug_threshold', null, $t_user_id, $t_pid ) ) + 1, $t_pid, $t_user_id ) ) ) { array_push( $t_limited_projects, '({bug}.project_id=' . $t_pid . ' AND ({bug}.reporter_id=' . $t_user_id . ') )' ); @@ -2061,61 +1999,89 @@ function filter_get_bug_rows_query_clauses( array $p_filter, $p_project_id = nul # tags $c_tag_string = trim( $t_filter[FILTER_PROPERTY_TAG_STRING] ); - $c_tag_select = trim( $t_filter[FILTER_PROPERTY_TAG_SELECT] ); - if( is_blank( $c_tag_string ) && !is_blank( $c_tag_select ) && $c_tag_select != 0 && tag_exists( $c_tag_select ) ) { - $t_tag = tag_get( $c_tag_select ); - $c_tag_string = $t_tag['name']; - } + $c_tag_select = (int)$t_filter[FILTER_PROPERTY_TAG_SELECT]; - if( !is_blank( $c_tag_string ) ) { + if( !is_blank( $c_tag_string ) || $c_tag_select > 0 ) { $t_tags = tag_parse_filters( $c_tag_string ); - if( count( $t_tags ) ) { - - $t_tags_all = array(); - $t_tags_any = array(); - $t_tags_none = array(); - - foreach( $t_tags as $t_tag_row ) { - switch( $t_tag_row['filter'] ) { - case 1: - $t_tags_all[] = $t_tag_row; - break; - case 0: - $t_tags_any[] = $t_tag_row; - break; - case -1: - $t_tags_none[] = $t_tag_row; - break; + if( count( $t_tags ) || $c_tag_select > 0 ) { + + $t_projects_can_view_tags = access_project_array_filter( 'tag_view_threshold', $t_included_project_ids, $t_user_id ); + if( !empty( $t_projects_can_view_tags ) ) { + $t_diff = array_diff( $t_included_project_ids, $t_projects_can_view_tags ); + # If tags can't be viewed in all included project, a filter must be used + if( empty( $t_diff ) ) { + $t_tag_projects_clause = ''; + } else { + $t_tag_projects_clause = ' AND {bug}.project_id IN (' . implode( ',', $t_projects_can_view_tags ) . ')'; } - } - if( 0 < $t_filter[FILTER_PROPERTY_TAG_SELECT] && tag_exists( $t_filter[FILTER_PROPERTY_TAG_SELECT] ) ) { - $t_tags_any[] = tag_get( $t_filter[FILTER_PROPERTY_TAG_SELECT] ); - } + $t_tags_all = array(); + $t_tags_any = array(); + $t_tags_none = array(); - if( count( $t_tags_all ) ) { - $t_clauses = array(); - foreach( $t_tags_all as $t_tag_row ) { - array_push( $t_clauses, '{bug}.id IN ( SELECT bug_id FROM {bug_tag} WHERE {bug_tag}.tag_id = ' . $t_tag_row['id'] . ')' ); + foreach( $t_tags as $t_tag_row ) { + switch( $t_tag_row['filter'] ) { + case 1: + $t_tags_all[] = $t_tag_row; + break; + case 0: + $t_tags_any[] = $t_tag_row; + break; + case -1: + $t_tags_none[] = $t_tag_row; + break; + } } - array_push( $t_where_clauses, '(' . implode( ' AND ', $t_clauses ) . ')' ); - } - if( count( $t_tags_any ) ) { - $t_clauses = array(); - foreach( $t_tags_any as $t_tag_row ) { - array_push( $t_clauses, '{bug_tag}.tag_id = ' . $t_tag_row['id'] ); + # Add the tag id to the array, from filter field "tag_select" + if( 0 < $c_tag_select && tag_exists( $c_tag_select ) ) { + $t_tags_any[] = tag_get( $c_tag_select ); } - array_push( $t_where_clauses, '{bug}.id IN ( SELECT bug_id FROM {bug_tag} WHERE ( ' . implode( ' OR ', $t_clauses ) . ') )' ); - } - if( count( $t_tags_none ) ) { - $t_clauses = array(); - foreach( $t_tags_none as $t_tag_row ) { - array_push( $t_clauses, '{bug_tag}.tag_id = ' . $t_tag_row['id'] ); + $t_tag_counter = 0; + if( count( $t_tags_all ) ) { + foreach( $t_tags_all as $t_tag_row ) { + $t_tag_alias = 'bug_tag_alias_' . ++$t_tag_counter; + array_push( $t_join_clauses, + 'JOIN {bug_tag} ' . $t_tag_alias . ' ON ' . $t_tag_alias . '.bug_id = {bug}.id' + . ' AND ' . $t_tag_alias . '.tag_id=' . (int)$t_tag_row['id'] + . $t_tag_projects_clause + ); + } + } + + if( count( $t_tags_any ) ) { + $t_tag_alias = 'bug_tag_alias_' . ++$t_tag_counter; + $t_tag_ids = array(); + foreach( $t_tags_any as $t_tag_row ) { + $t_tag_ids[] = (int)$t_tag_row['id']; + } + array_push( $t_join_clauses, + 'LEFT OUTER JOIN {bug_tag} ' . $t_tag_alias . ' ON ' . $t_tag_alias . '.bug_id = {bug}.id' + . ' AND ' . $t_tag_alias . '.tag_id IN (' . implode( ',', $t_tag_ids ) . ')' + . $t_tag_projects_clause + ); + + # If the isn't a non-outer join, check that at least one of the tags has been matched by the outer join + if( !count( $t_tags_all ) ) { + array_push( $t_where_clauses, $t_tag_alias . '.tag_id IS NOT NULL' ); + } + } + + if( count( $t_tags_none ) ) { + $t_tag_alias = 'bug_tag_alias_' . ++$t_tag_counter; + $t_tag_ids = array(); + foreach( $t_tags_none as $t_tag_row ) { + $t_tag_ids[] = (int)$t_tag_row['id']; + } + array_push( $t_join_clauses, + 'LEFT OUTER JOIN {bug_tag} ' . $t_tag_alias . ' ON ' . $t_tag_alias . '.bug_id = {bug}.id' + . ' AND ' . $t_tag_alias . '.tag_id IN (' . implode( ',', $t_tag_ids ) . ')' + . $t_tag_projects_clause + ); + array_push( $t_where_clauses, $t_tag_alias . '.tag_id IS NULL' ); } - array_push( $t_where_clauses, '{bug}.id NOT IN ( SELECT bug_id FROM {bug_tag} WHERE ( ' . implode( ' OR ', $t_clauses ) . ') )' ); } } } @@ -3510,3 +3476,94 @@ function filter_print_view_type_toggle( $p_url, $p_view_type ) { ); echo ''; } + +/** + * Returns an array of project ids which are included in the filter. + * This array includes all individual projects/subprojects that are in the search scope. + * @param array $p_filter Filter array + * @param integer $p_project_id Project id to use in filtering, if applicable by filter type + * @param integer $p_user_id User id to use as current user when filtering + * @return array + */ +function filter_get_included_projects( array $p_filter, $p_project_id = null, $p_user_id = null ) { + if( null === $p_project_id ) { + $t_project_id = helper_get_current_project(); + } else { + $t_project_id = $p_project_id; + } + if( !$p_user_id ) { + $t_user_id = auth_get_current_user_id(); + } else { + $t_user_id = $p_user_id; + } + + $t_view_type = $p_filter['_view_type']; + # normalize the project filtering into an array $t_project_ids + if( FILTER_VIEW_TYPE_SIMPLE == $t_view_type ) { + log_event( LOG_FILTERING, 'Simple Filter' ); + $t_project_ids = array( $t_project_id ); + $t_include_sub_projects = true; + } else { + log_event( LOG_FILTERING, 'Advanced Filter' ); + $t_project_ids = $p_filter[FILTER_PROPERTY_PROJECT_ID]; + $t_include_sub_projects = (( count( $t_project_ids ) == 1 ) && ( ( $t_project_ids[0] == META_FILTER_CURRENT ) || ( $t_project_ids[0] == ALL_PROJECTS ) ) ); + } + + log_event( LOG_FILTERING, 'project_ids = @P' . implode( ', @P', $t_project_ids ) ); + log_event( LOG_FILTERING, 'include sub-projects = ' . ( $t_include_sub_projects ? '1' : '0' ) ); + + # if the array has ALL_PROJECTS, then reset the array to only contain ALL_PROJECTS. + # replace META_FILTER_CURRENT with the actual current project id. + + $t_all_projects_found = false; + $t_new_project_ids = array(); + foreach( $t_project_ids as $t_pid ) { + if( $t_pid == META_FILTER_CURRENT ) { + $t_pid = $t_project_id; + } + + if( $t_pid == ALL_PROJECTS ) { + $t_all_projects_found = true; + log_event( LOG_FILTERING, 'all projects selected' ); + break; + } + + # filter out inaccessible projects. + if( !project_exists( $t_pid ) || !access_has_project_level( config_get( 'view_bug_threshold', null, $t_user_id, $t_pid ), $t_pid, $t_user_id ) ) { + log_event( LOG_FILTERING, 'Invalid or inaccessible project: ' . $t_pid ); + continue; + } + + $t_new_project_ids[] = $t_pid; + } + + if( $t_all_projects_found ) { + $t_project_ids = user_get_accessible_projects( $t_user_id ); + } else { + $t_project_ids = $t_new_project_ids; + } + + # expand project ids to include sub-projects + if( $t_include_sub_projects ) { + $t_top_project_ids = $t_project_ids; + + foreach( $t_top_project_ids as $t_pid ) { + log_event( LOG_FILTERING, 'Getting sub-projects for project id @P' . $t_pid ); + $t_subproject_ids = user_get_all_accessible_subprojects( $t_user_id, $t_pid ); + if( !$t_subproject_ids ) { + continue; + } + $t_project_ids = array_merge( $t_project_ids, $t_subproject_ids ); + } + + $t_project_ids = array_unique( $t_project_ids ); + } + + if( count( $t_project_ids ) ) { + log_event( LOG_FILTERING, 'project_ids after including sub-projects = @P' . implode( ', @P', $t_project_ids ) ); + } else { + log_event( LOG_FILTERING, 'no accessible projects' ); + } + + return $t_project_ids; +} \ No newline at end of file diff --git a/core/filter_form_api.php b/core/filter_form_api.php index 96ed9d73ac..e9d772309e 100644 --- a/core/filter_form_api.php +++ b/core/filter_form_api.php @@ -84,7 +84,7 @@ * if the option is disabled, returns the current value and a hidden input for that value. * @param array $p_filter Filter array * @param string $p_filter_target Filter field name - * @param boolean $p_show_inputs Whether to return a visible form input or a text value. + * @param boolean $p_show_inputs True to return a visible form input or false for a text value. * @return string The html content for the field requested */ function filter_form_get_input( array $p_filter, $p_filter_target, $p_show_inputs = true ) { @@ -1584,7 +1584,7 @@ function print_filter_values_tag_string( array $p_filter ) { */ function print_filter_tag_string( array $p_filter = null ) { global $g_filter; - if( !access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { + if( !access_has_project_level( config_get( 'tag_view_threshold' ) ) ) { return; } if( null === $p_filter ) { @@ -2595,7 +2595,7 @@ function filter_form_draw_inputs( $p_filter, $p_for_screen = true, $p_static = f null /* class */, 'relationship_type_filter_target' /* content id */ )); - if( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { + if( access_has_project_level( config_get( 'tag_view_threshold' ) ) ) { $t_row3->add_item( new TableFieldsItem( $get_field_header( 'tag_string_filter', lang_get( 'tags' ) ), filter_form_get_input( $t_filter, 'tag_string', $t_show_inputs ), diff --git a/core/tag_api.php b/core/tag_api.php index c430182b7d..68df7f3925 100644 --- a/core/tag_api.php +++ b/core/tag_api.php @@ -979,45 +979,46 @@ function tag_stats_attached( $p_tag_id ) { function tag_stats_related( $p_tag_id, $p_limit = 5 ) { $c_user_id = auth_get_current_user_id(); - db_param_push(); - $t_query = 'SELECT * FROM {bug_tag} - WHERE tag_id != ' . db_param(); # 1st Param - - $t_subquery = 'SELECT b.id FROM {bug} b - LEFT JOIN {project_user_list} p - ON p.project_id=b.project_id AND p.user_id=' . db_param() . # 2nd Param - ' JOIN {user} u - ON u.id=' . db_param() . # 3rd Param - ' JOIN {bug_tag} t - ON t.bug_id=b.id - WHERE ( p.access_level>b.view_state OR u.access_level>b.view_state ) - AND t.tag_id=' . db_param(); # 4th Param - - $t_query .= ' AND bug_id IN ( ' . $t_subquery . ' ) '; - - $t_result = db_query( $t_query, array( $p_tag_id, $c_user_id, $c_user_id, $p_tag_id ) ); - - $t_tag_counts = array(); - while( $t_row = db_fetch_array( $t_result ) ) { - if( !isset( $t_tag_counts[$t_row['tag_id']] ) ) { - $t_tag_counts[$t_row['tag_id']] = 1; - } else { - $t_tag_counts[$t_row['tag_id']]++; - } + # Use a filter to get all visible issues for this tag id + $t_filter = array( + FILTER_PROPERTY_HIDE_STATUS => array( META_FILTER_NONE ), + FILTER_PROPERTY_TAG_SELECT => $p_tag_id, + FILTER_PROPERTY_PROJECT_ID => array( ALL_PROJECTS ), + '_view_type' => FILTER_VIEW_TYPE_ADVANCED, + ); + $t_filter = filter_ensure_valid_filter( $t_filter ); + + # Note: filter_get_bug_rows_query_clauses() calls db_param_push(); + $t_query_clauses = filter_get_bug_rows_query_clauses( $t_filter, null, null, null ); + # if the query can't be formed, there are no results + if( empty( $t_query_clauses ) ) { + # reset the db_param stack that was initialized by "filter_get_bug_rows_query_clauses()" + db_param_pop(); + return array(); } + $t_select_string = 'SELECT {bug}.id '; + $t_from_string = ' FROM ' . implode( ', ', $t_query_clauses['from'] ); + $t_join_string = count( $t_query_clauses['join'] ) > 0 ? implode( ' ', $t_query_clauses['join'] ) : ' '; + $t_where_string = ' WHERE '. implode( ' AND ', $t_query_clauses['project_where'] ); + if( count( $t_query_clauses['where'] ) > 0 ) { + $t_where_string .= ' AND ( ' . implode( $t_query_clauses['operator'], $t_query_clauses['where'] ) . ' ) '; + } + $t_filter_in = ' ( ' . $t_select_string . $t_from_string . $t_join_string . $t_where_string . ' )'; + $t_params = $t_query_clauses['where_values']; + + $t_query = 'SELECT tag_id, COUNT(1) AS tag_count FROM {bug_tag}' + . ' WHERE bug_id IN ' . $t_filter_in + . ' AND tag_id <> ' . db_param() + . ' GROUP BY tag_id ORDER BY COUNT(1) DESC'; - arsort( $t_tag_counts ); + $t_params[] = (int)$p_tag_id; + $t_result = db_query( $t_query, $t_params, $p_limit ); $t_tags = array(); - $i = 1; - foreach( $t_tag_counts as $t_tag_id => $t_count ) { - $t_tag_row = tag_get( $t_tag_id ); - $t_tag_row['count'] = $t_count; + while( $t_row = db_fetch_array( $t_result ) ) { + $t_tag_row = tag_get( $t_row['tag_id'] ); + $t_tag_row['count'] = (int)$t_row['tag_count']; $t_tags[] = $t_tag_row; - $i++; - if( $i > $p_limit ) { - break; - } } return $t_tags;