| @@ -24,40 +24,75 @@ | ||
| 24 | 24 | } |
| 25 | 25 | |
| 26 | 26 | /** |
| 27 | 27 | * Post access restrictions. |
| 28 | - * | |
| 29 | - * @todo when wp_count_posts() provides reasonable filters, use them so that | |
| 30 | - * the post counts displayed on top are in line with the actual posts that | |
| 31 | - * are displayed in the table; same for wp_count_attachments() | |
| 32 | - * @see http://core.trac.wordpress.org/ticket/16603 | |
| 33 | - * | |
| 34 | 28 | */ |
| 35 | 29 | class Groups_Post_Access { |
| 36 | 30 | |
| 31 | + /** | |
| 32 | + * @var string | |
| 33 | + */ | |
| 37 | 34 | const POSTMETA_PREFIX = 'groups-'; |
| 38 | 35 | |
| 39 | - const CACHE_GROUP = 'groups'; | |
| 40 | - const CAN_READ_POST = 'can_read_post'; | |
| 36 | + /** | |
| 37 | + * @var string | |
| 38 | + */ | |
| 39 | + const READ = 'read'; | |
| 41 | 40 | |
| 42 | - const READ_POST_CAPABILITY = "groups_read_post"; | |
| 43 | - const READ_POST_CAPABILITY_NAME = "Read Post"; | |
| 41 | + /** | |
| 42 | + * @var string | |
| 43 | + */ | |
| 44 | + const CACHE_GROUP = 'groups'; | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * | |
| 48 | + * @var string | |
| 49 | + */ | |
| 50 | + const CAN_READ_POST = 'can_read_post'; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * @deprecated | |
| 54 | + * @var string | |
| 55 | + */ | |
| 56 | + const READ_POST_CAPABILITY = 'groups_read_post'; | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * @deprecated | |
| 60 | + * @var string | |
| 61 | + */ | |
| 62 | + const READ_POST_CAPABILITY_NAME = 'Read Post'; | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * @deprecated | |
| 66 | + * @var string | |
| 67 | + */ | |
| 44 | 68 | const READ_POST_CAPABILITIES = 'read_post_capabilities'; |
| 69 | + | |
| 70 | + /** | |
| 71 | + * @var string | |
| 72 | + */ | |
| 45 | 73 | const POST_TYPES = 'post_types'; |
| 46 | 74 | |
| 47 | 75 | /** |
| 48 | - * Create needed capabilities on plugin activation. | |
| 49 | - * Must be called explicitly or hooked into activation. | |
| 76 | + * @since 2.20.0 | |
| 77 | + * | |
| 78 | + * @var \WP_Block block for which to filter | |
| 50 | 79 | */ |
| 80 | + private static $filter_get_terms_block = null; | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * @since 2.20.0 | |
| 84 | + * | |
| 85 | + * @var array widget for which to filter | |
| 86 | + */ | |
| 87 | + private static $filter_get_terms_widget = null; | |
| 88 | + | |
| 89 | + /** | |
| 90 | + * Work done on activation, currently does nothing. | |
| 91 | + * | |
| 92 | + * @see Groups_Controller::activate() | |
| 93 | + */ | |
| 51 | 94 | public static function activate() { |
| 52 | - if ( !Groups_Capability::read_by_capability( self::READ_POST_CAPABILITY ) ) { | |
| 53 | - Groups_Capability::create( array( "capability" => self::READ_POST_CAPABILITY ) ); | |
| 54 | - // default read caps | |
| 55 | - Groups_Options::update_option( Groups_Post_Access::READ_POST_CAPABILITIES, array( Groups_Post_Access::READ_POST_CAPABILITY ) ); | |
| 56 | - // for translation | |
| 57 | - // @see self::READ_POST_CAPABILITY_NAME | |
| 58 | - __( "Read Post", GROUPS_PLUGIN_DOMAIN ); | |
| 59 | - } | |
| 60 | 95 | } |
| 61 | 96 | |
| 62 | 97 | /** |
| 63 | 98 | * Sets up filters to restrict access. |
| @@ -64,35 +99,108 @@ | ||
| 64 | 99 | */ |
| 65 | 100 | public static function init() { |
| 66 | 101 | // post access |
| 67 | 102 | add_filter( 'posts_where', array( __CLASS__, 'posts_where' ), 10, 2 ); |
| 68 | - add_filter( 'get_pages', array( __CLASS__, "get_pages" ), 1 ); | |
| 103 | + add_filter( 'get_pages', array( __CLASS__, 'get_pages' ), 1 ); | |
| 69 | 104 | if ( apply_filters( 'groups_filter_the_posts', false ) ) { |
| 70 | - add_filter( 'the_posts', array( __CLASS__, "the_posts" ), 1, 2 ); | |
| 105 | + add_filter( 'the_posts', array( __CLASS__, 'the_posts' ), 1, 2 ); | |
| 71 | 106 | } |
| 72 | - add_filter( 'wp_get_nav_menu_items', array( __CLASS__, "wp_get_nav_menu_items" ), 1, 3 ); | |
| 107 | + // If we had a get_post filter https://core.trac.wordpress.org/ticket/12955 | |
| 108 | + // add_filter( 'get_post', ... ); | |
| 109 | + add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'wp_get_nav_menu_items' ), 1, 3 ); | |
| 73 | 110 | // content access |
| 74 | - add_filter( "get_the_excerpt", array( __CLASS__, "get_the_excerpt" ), 1 ); | |
| 75 | - add_filter( "the_content", array( __CLASS__, "the_content" ), 1 ); | |
| 111 | + add_filter( 'get_the_excerpt', array( __CLASS__, 'get_the_excerpt' ), 1 ); | |
| 112 | + add_filter( 'the_content', array( __CLASS__, 'the_content' ), 1 ); | |
| 76 | 113 | // edit & delete post |
| 77 | 114 | add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 10, 4 ); |
| 78 | - // @todo these could be interesting to add later ... | |
| 115 | + | |
| 116 | + // These could be interesting to add later ... | |
| 79 | 117 | // add_filter( "plugin_row_meta", array( __CLASS__, "plugin_row_meta" ), 1 ); |
| 80 | 118 | // add_filter( "posts_join_paged", array( __CLASS__, "posts_join_paged" ), 1 ); |
| 81 | 119 | // add_filter( "posts_where_paged", array( __CLASS__, "posts_where_paged" ), 1 ); |
| 82 | - add_action( 'groups_deleted_capability_capability', array( __CLASS__, 'groups_deleted_capability_capability' ) ); | |
| 120 | + | |
| 121 | + add_action( 'groups_deleted_group', array( __CLASS__, 'groups_deleted_group' ) ); | |
| 122 | + add_filter( 'wp_count_posts', array( __CLASS__, 'wp_count_posts' ), 10, 3 ); | |
| 123 | + | |
| 124 | + // Enable the filter and implement below if needed to correct attachment counts ... | |
| 125 | + // add_filter( 'wp_count_attachments', array( __CLASS__, 'wp_count_attachments' ), 10, 2 ); | |
| 126 | + | |
| 127 | + // REST API | |
| 128 | + $post_types = self::get_handles_post_types(); | |
| 129 | + if ( !empty( $post_types ) ) { | |
| 130 | + foreach ( $post_types as $post_type => $handles ) { | |
| 131 | + if ( $handles ) { | |
| 132 | + add_filter( "rest_prepare_{$post_type}", array( __CLASS__, 'rest_prepare_post' ), 10, 3 ); | |
| 133 | + } | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + // adjacent posts | |
| 138 | + add_filter( 'get_previous_post_where', array( __CLASS__, 'get_previous_post_where' ), 10, 5 ); | |
| 139 | + add_filter( 'get_next_post_where', array( __CLASS__, 'get_next_post_where' ), 10, 5 ); | |
| 140 | + add_action( 'save_post', array( __CLASS__, 'save_post' ), PHP_INT_MAX ); | |
| 141 | + add_filter( 'attachment_fields_to_save', array( __CLASS__, 'attachment_fields_to_save' ), PHP_INT_MAX, 2 ); | |
| 142 | + | |
| 143 | + // @since 2.20.0 | |
| 144 | + add_filter( 'render_block', array( __CLASS__, 'render_block' ), 10, 3 ); | |
| 145 | + // @since 2.20.0 | |
| 146 | + add_filter( 'block_core_navigation_render_inner_blocks', array( __CLASS__, 'block_core_navigation_render_inner_blocks' ) ); | |
| 147 | + | |
| 148 | + // @since 2.20.0 adds our get_terms filter for Categories blocks: | |
| 149 | + add_filter( 'pre_render_block', array( __CLASS__, 'pre_render_block' ), 10, 3 ); | |
| 150 | + // @since 2.20.0 adds our get_terms filter for Categories widgets rendered as list: | |
| 151 | + add_filter( 'widget_categories_args', array( __CLASS__, 'widget_categories_args' ), 10, 2 ); | |
| 152 | + // @since 2.20.0 adds our get_terms filter for Categories widgets rendered as dropdown: | |
| 153 | + add_filter( 'widget_categories_dropdown_args', array( __CLASS__, 'widget_categories_dropdown_args' ), 10, 2 ); | |
| 83 | 154 | } |
| 84 | 155 | |
| 85 | 156 | /** |
| 157 | + * Replicates the response for invalid post IDs when unauthorized access to a post is requested. | |
| 158 | + * There is no filter in WP_REST_Posts_Controller::get_post() nor in get_post() that we could use (WP 4.8). | |
| 159 | + * | |
| 160 | + * REST API Handbook https://developer.wordpress.org/rest-api/ | |
| 161 | + * | |
| 162 | + * For development tests: | |
| 163 | + * | |
| 164 | + * 1. Install https://github.com/WP-API/Basic-Auth | |
| 165 | + * 2. Protect post 1 with group "Test". | |
| 166 | + * 3. Test access denied: $ curl http://example.com/wp-json/wp/v2/posts/1 | |
| 167 | + * 4. Test access granted $ curl --user username:password https://example.com/wp-json/wp/v2/posts/1 | |
| 168 | + * | |
| 169 | + * On #4 username:password are cleartext, username must belong to group "Test". | |
| 170 | + * | |
| 171 | + * @param array $response | |
| 172 | + * @param WP_Post $post | |
| 173 | + * @param string $request | |
| 174 | + * | |
| 175 | + * @return string[]|number[][] | |
| 176 | + */ | |
| 177 | + public static function rest_prepare_post( $response, $post, $request ) { | |
| 178 | + if ( isset( $post->ID ) && !self::user_can_read_post( $post->ID ) ) { | |
| 179 | + $response = array( | |
| 180 | + 'code' => 'rest_post_invalid_id', | |
| 181 | + 'message' => __( 'Invalid post ID.' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain | |
| 182 | + 'data' => array( 'status' => 404 ) | |
| 183 | + ); | |
| 184 | + } | |
| 185 | + return $response; | |
| 186 | + } | |
| 187 | + | |
| 188 | + /** | |
| 86 | 189 | * Restrict access to edit or delete posts based on the post's access restrictions. |
| 87 | - * | |
| 190 | + * | |
| 88 | 191 | * @param array $caps |
| 89 | 192 | * @param string $cap |
| 90 | 193 | * @param int $user_id |
| 91 | 194 | * @param array $args |
| 195 | + * | |
| 92 | 196 | * @return array |
| 93 | 197 | */ |
| 94 | 198 | public static function map_meta_cap( $caps, $cap, $user_id, $args ) { |
| 199 | + // get user object before removing filter | |
| 200 | + $user = function_exists( 'get_current_user_id' ) ? new Groups_User( get_current_user_id() ) : null; | |
| 201 | + // @since 4.0.0 avoid potential infinite recursion | |
| 202 | + remove_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 10 ); | |
| 95 | 203 | if ( isset( $args[0] ) ) { |
| 96 | 204 | if ( strpos( $cap, 'edit_' ) === 0 || strpos( $cap, 'delete_' ) === 0 ) { |
| 97 | 205 | if ( $post_type = get_post_type( $args[0] ) ) { |
| 98 | 206 | |
| @@ -99,9 +207,9 @@ | ||
| 99 | 207 | $edit_post_type = 'edit_' . $post_type; |
| 100 | 208 | $delete_post_type = 'delete_' . $post_type; |
| 101 | 209 | if ( $post_type_object = get_post_type_object( $post_type ) ) { |
| 102 | 210 | if ( !isset( $post_type_object->capabilities ) ) { |
| 103 | - $post_type_object->capabilities = array(); | |
| 211 | + $post_type_object->capabilities = array(); // @phpstan-ignore property.notFound | |
| 104 | 212 | } |
| 105 | 213 | $caps_object = get_post_type_capabilities( $post_type_object ); |
| 106 | 214 | if ( isset( $caps_object->edit_post ) ) { |
| 107 | 215 | $edit_post_type = $caps_object->edit_post; |
| @@ -110,18 +218,35 @@ | ||
| 110 | 218 | $delete_post_type = $caps_object->delete_post; |
| 111 | 219 | } |
| 112 | 220 | } |
| 113 | 221 | |
| 114 | - if ( $cap === $edit_post_type || $cap === $delete_post_type ) { | |
| 222 | + if ( | |
| 223 | + $cap === $edit_post_type || | |
| 224 | + $cap === $delete_post_type || | |
| 225 | + $cap === 'edit_post' || | |
| 226 | + $cap === 'delete_post' | |
| 227 | + ) { | |
| 115 | 228 | $post_id = null; |
| 116 | 229 | if ( is_numeric( $args[0] ) ) { |
| 117 | - $post_id = $args[0]; | |
| 230 | + $post_id = $args[0]; | |
| 118 | 231 | } else if ( $args[0] instanceof WP_Post ) { |
| 119 | - $post_id = $post->ID; | |
| 232 | + $post_id = $args[0]->ID; | |
| 120 | 233 | } |
| 121 | 234 | if ( $post_id ) { |
| 122 | 235 | if ( !self::user_can_read_post( $post_id, $user_id ) ) { |
| 123 | 236 | $caps[] = 'do_not_allow'; |
| 237 | + } else { | |
| 238 | + // post with access restrictions requires user to have access restriction rights | |
| 239 | + if ( $post_type = get_post_type( $post_id ) ) { | |
| 240 | + if ( self::handles_post_type( $post_type ) ) { | |
| 241 | + $group_ids = self::get_read_group_ids( $post_id ); | |
| 242 | + if ( !empty( $group_ids ) ) { | |
| 243 | + if ( $user === null || !$user->can( GROUPS_RESTRICT_ACCESS ) ) { | |
| 244 | + $caps[] = 'do_not_allow'; | |
| 245 | + } | |
| 246 | + } | |
| 247 | + } | |
| 248 | + } | |
| 124 | 249 | } |
| 125 | 250 | } |
| 126 | 251 | } |
| 127 | 252 | } |
| @@ -126,90 +251,174 @@ | ||
| 126 | 251 | } |
| 127 | 252 | } |
| 128 | 253 | } |
| 129 | 254 | } |
| 255 | + add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 10, 4 ); | |
| 130 | 256 | return $caps; |
| 131 | 257 | } |
| 132 | 258 | |
| 133 | 259 | /** |
| 134 | 260 | * Filters out posts that the user should not be able to access. |
| 135 | - * | |
| 261 | + * | |
| 136 | 262 | * @param string $where current where conditions |
| 137 | 263 | * @param WP_Query $query current query |
| 264 | + * | |
| 138 | 265 | * @return string modified $where |
| 139 | 266 | */ |
| 140 | - public static function posts_where( $where, &$query ) { | |
| 267 | + public static function posts_where( $where, $query ) { | |
| 141 | 268 | |
| 142 | 269 | global $wpdb; |
| 143 | 270 | |
| 144 | - $user_id = get_current_user_id(); | |
| 271 | + if ( apply_filters( 'groups_post_access_posts_where_apply', true, $where, $query ) ) { | |
| 145 | 272 | |
| 146 | - // this only applies to logged in users | |
| 147 | - if ( $user_id ) { | |
| 148 | - // if administrators can override access, don't filter | |
| 149 | - if ( get_option( GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE, GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE_DEFAULT ) ) { | |
| 150 | - if ( user_can( $user_id, 'administrator' ) ) { | |
| 273 | + $user_id = get_current_user_id(); | |
| 274 | + | |
| 275 | + // this only applies to logged in users | |
| 276 | + if ( _groups_admin_override() ) { | |
| 277 | + return $where; | |
| 278 | + } | |
| 279 | + | |
| 280 | + // Groups admins see everything | |
| 281 | + if ( Groups_User::current_user_can( GROUPS_ADMINISTER_GROUPS ) ) { | |
| 282 | + return $where; | |
| 283 | + } | |
| 284 | + | |
| 285 | + if ( !apply_filters( 'groups_post_access_posts_where_filter_all', false ) ) { | |
| 286 | + $filter = true; | |
| 287 | + $post_types = apply_filters( | |
| 288 | + 'groups_post_access_posts_where_query_get_post_types', | |
| 289 | + $query->get( 'post_type', null ), | |
| 290 | + $where, | |
| 291 | + $query | |
| 292 | + ); | |
| 293 | + | |
| 294 | + // If post_types is empty and we have a term page (AKA taxonomy archive), | |
| 295 | + // try to retrieve the post type from the taxonomy: | |
| 296 | + if ( empty( $post_types ) ) { | |
| 297 | + if ( $query->is_tax() ) { | |
| 298 | + $queried_object = $query->get_queried_object(); | |
| 299 | + if ( $queried_object instanceof WP_Term ) { | |
| 300 | + $taxonomy = get_taxonomy( $queried_object->taxonomy ); | |
| 301 | + if ( $taxonomy !== false ) { | |
| 302 | + if ( property_exists( $taxonomy, 'object_type' ) ) { // object_type property since WP 4.7.0 | |
| 303 | + $post_types = $taxonomy->object_type; | |
| 304 | + } | |
| 305 | + } | |
| 306 | + } | |
| 307 | + } | |
| 308 | + } | |
| 309 | + | |
| 310 | + if ( 'any' == $post_types ) { | |
| 311 | + // we need to filter in this case as it affects any post type | |
| 312 | + } else if ( !empty( $post_types ) && is_array( $post_types ) ) { | |
| 313 | + // if there is at least one post type we handle, we need to filter | |
| 314 | + $handled = 0; | |
| 315 | + $handles_post_types = self::get_handles_post_types(); | |
| 316 | + foreach ( $post_types as $post_type ) { | |
| 317 | + if ( !isset( $handles_post_types[$post_type] ) || $handles_post_types[$post_type] ) { | |
| 318 | + $handled++; | |
| 319 | + } | |
| 320 | + } | |
| 321 | + $filter = $handled > 0; | |
| 322 | + } else if ( !empty( $post_types ) && is_string( $post_types ) ) { | |
| 323 | + $filter = self::handles_post_type( $post_types ); | |
| 324 | + } else if ( $query->is_attachment ) { | |
| 325 | + $filter = self::handles_post_type( 'attachment' ); | |
| 326 | + } else if ( $query->is_page ) { | |
| 327 | + $filter = self::handles_post_type( 'page' ); | |
| 328 | + } else { | |
| 329 | + $filter = self::handles_post_type( 'post' ); | |
| 330 | + } | |
| 331 | + if ( !$filter ) { | |
| 151 | 332 | return $where; |
| 152 | 333 | } |
| 153 | 334 | } |
| 154 | - } | |
| 155 | 335 | |
| 156 | - // 1. Get all the capabilities that the user has, including those that are inherited: | |
| 157 | - $caps = array(); | |
| 158 | - if ( $user = new Groups_User( $user_id ) ) { | |
| 159 | - $capabilities = $user->capabilities_deep; | |
| 160 | - if ( is_array( $capabilities ) ) { | |
| 161 | - foreach ( $capabilities as $capability ) { | |
| 162 | - $caps[] = "'". $capability . "'"; | |
| 336 | + $handles_post_types = Groups_Post_Access::get_handles_post_types(); | |
| 337 | + $post_types = array(); | |
| 338 | + foreach ( $handles_post_types as $post_type => $handles ) { | |
| 339 | + if ( $handles ) { | |
| 340 | + $post_types[] = $post_type; | |
| 163 | 341 | } |
| 164 | 342 | } |
| 165 | - } | |
| 343 | + if ( count( $post_types ) == 0 ) { | |
| 344 | + return $where; | |
| 345 | + } | |
| 346 | + $post_types_in = "'" . implode( "','", array_map( 'esc_sql', $post_types ) ) . "'"; | |
| 166 | 347 | |
| 167 | - if ( count( $caps ) > 0 ) { | |
| 168 | - $caps = implode( ',', $caps ); | |
| 169 | - } else { | |
| 170 | - $caps = '\'\''; | |
| 348 | + // 1. Get all the groups that the user belongs to, including those that are inherited: | |
| 349 | + $group_ids = array(); | |
| 350 | + if ( $user = new Groups_User( $user_id ) ) { | |
| 351 | + $group_ids_deep = $user->get_group_ids_deep(); | |
| 352 | + if ( is_array( $group_ids_deep ) ) { | |
| 353 | + $group_ids = $group_ids_deep; | |
| 354 | + } | |
| 355 | + } | |
| 356 | + if ( count( $group_ids ) > 0 ) { | |
| 357 | + $group_ids = implode( ',', array_map( 'intval', $group_ids ) ); | |
| 358 | + } else { | |
| 359 | + $group_ids = '0'; | |
| 360 | + } | |
| 361 | + | |
| 362 | + // 2. Filter the posts: | |
| 363 | + // This allows the user to access posts where the posts are not restricted or where | |
| 364 | + // the user belongs to ANY of the groups: | |
| 365 | + // $where .= sprintf( | |
| 366 | + // " AND {$wpdb->posts}.ID IN " . | |
| 367 | + // " ( " . | |
| 368 | + // " SELECT ID FROM $wpdb->posts WHERE post_type NOT IN (%s) OR ID NOT IN ( SELECT post_id FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = '%s' ) " . // posts of a type that is not handled or posts without access restriction | |
| 369 | + // " UNION ALL " . // we don't care about duplicates here, just make it quick | |
| 370 | + // " SELECT post_id AS ID FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = '%s' AND {$wpdb->postmeta}.meta_value IN (%s) " . // posts that require any group the user belongs to | |
| 371 | + // " ) ", | |
| 372 | + // $post_types_in, | |
| 373 | + // self::POSTMETA_PREFIX . self::READ, | |
| 374 | + // self::POSTMETA_PREFIX . self::READ, | |
| 375 | + // $group_ids | |
| 376 | + // ); | |
| 377 | + // New faster version - Exclude any post IDs from: | |
| 378 | + // posts restricted to groups that the user does not belong to MINUS posts restricted to groups to which the user belongs | |
| 379 | + $groups_table = _groups_get_tablename( 'group' ); | |
| 380 | + $where .= sprintf( | |
| 381 | + " AND {$wpdb->posts}.ID NOT IN ( " . | |
| 382 | + "SELECT ID FROM $wpdb->posts WHERE " . | |
| 383 | + "post_type IN (%s) AND " . | |
| 384 | + "ID IN ( " . | |
| 385 | + "SELECT post_id FROM $wpdb->postmeta pm WHERE " . | |
| 386 | + "pm.meta_key = '%s' AND " . | |
| 387 | + "pm.meta_value NOT IN (%s) AND " . | |
| 388 | + "pm.meta_value IN ( SELECT group_id FROM $groups_table ) AND " . // @since 2.18.0 also check for group ID value integrity | |
| 389 | + "post_id NOT IN ( SELECT post_id FROM $wpdb->postmeta pm WHERE pm.meta_key = '%s' AND pm.meta_value IN (%s) ) " . | |
| 390 | + ") " . | |
| 391 | + ") ", | |
| 392 | + $post_types_in, | |
| 393 | + esc_sql( self::POSTMETA_PREFIX . self::READ ), | |
| 394 | + $group_ids, | |
| 395 | + esc_sql( self::POSTMETA_PREFIX . self::READ ), | |
| 396 | + $group_ids | |
| 397 | + ); | |
| 171 | 398 | } |
| 172 | 399 | |
| 173 | - // 2. Filter the posts that require a capability that the user doesn't | |
| 174 | - // have, or in other words: exclude posts that the user must NOT access: | |
| 175 | - | |
| 176 | - // The following is not correct in that it requires the user to have ALL capabilities: | |
| 177 | -// $where .= sprintf( | |
| 178 | -// " AND {$wpdb->posts}.ID NOT IN (SELECT DISTINCT ID FROM $wpdb->posts LEFT JOIN $wpdb->postmeta on {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id WHERE {$wpdb->postmeta}.meta_key = '%s' AND {$wpdb->postmeta}.meta_value NOT IN (%s) ) ", | |
| 179 | -// self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, | |
| 180 | -// $caps | |
| 181 | -// ); | |
| 182 | - | |
| 183 | - // This allows the user to access posts where the posts are not restricted or where | |
| 184 | - // the user has ANY of the capabilities: | |
| 185 | - $where .= sprintf( | |
| 186 | - " AND {$wpdb->posts}.ID IN " . | |
| 187 | - " ( " . | |
| 188 | - " SELECT ID FROM $wpdb->posts WHERE ID NOT IN ( SELECT post_id FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = '%s' ) " . // posts without access restriction | |
| 189 | - " UNION ALL " . // we don't care about duplicates here, just make it quick | |
| 190 | - " SELECT post_id AS ID FROM $wpdb->postmeta WHERE {$wpdb->postmeta}.meta_key = '%s' AND {$wpdb->postmeta}.meta_value IN (%s) " . // posts that require any capability the user has | |
| 191 | - " ) ", | |
| 192 | - self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, | |
| 193 | - self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, | |
| 194 | - $caps | |
| 195 | - ); | |
| 196 | - | |
| 197 | - return $where; | |
| 400 | + return apply_filters( 'groups_post_access_posts_where', $where, $query ); | |
| 198 | 401 | } |
| 199 | 402 | |
| 200 | 403 | /** |
| 201 | 404 | * Filter pages by access capability. |
| 202 | - * | |
| 405 | + * | |
| 203 | 406 | * @param array $pages |
| 407 | + * | |
| 408 | + * @return array | |
| 204 | 409 | */ |
| 205 | 410 | public static function get_pages( $pages ) { |
| 206 | 411 | $result = array(); |
| 207 | - $user_id = get_current_user_id(); | |
| 208 | - foreach ( $pages as $page ) { | |
| 209 | - if ( self::user_can_read_post( $page->ID, $user_id ) ) { | |
| 210 | - $result[] = $page; | |
| 412 | + if ( apply_filters( 'groups_post_access_get_pages_apply', true, $pages ) ) { | |
| 413 | + $user_id = get_current_user_id(); | |
| 414 | + foreach ( $pages as $page ) { | |
| 415 | + if ( self::user_can_read_post( $page->ID, $user_id ) ) { | |
| 416 | + $result[] = $page; | |
| 417 | + } | |
| 211 | 418 | } |
| 419 | + } else { | |
| 420 | + $result = $pages; | |
| 212 | 421 | } |
| 213 | 422 | return $result; |
| 214 | 423 | } |
| 215 | 424 | |
| @@ -214,19 +423,25 @@ | ||
| 214 | 423 | } |
| 215 | 424 | |
| 216 | 425 | /** |
| 217 | 426 | * Filter posts by access capability. |
| 218 | - * | |
| 427 | + * | |
| 219 | 428 | * @param array $posts list of posts |
| 220 | 429 | * @param WP_Query $query |
| 430 | + * | |
| 431 | + * @return array | |
| 221 | 432 | */ |
| 222 | 433 | public static function the_posts( $posts, &$query ) { |
| 223 | 434 | $result = array(); |
| 224 | - $user_id = get_current_user_id(); | |
| 225 | - foreach ( $posts as $post ) { | |
| 226 | - if ( self::user_can_read_post( $post->ID, $user_id ) ) { | |
| 227 | - $result[] = $post; | |
| 435 | + if ( apply_filters( 'groups_post_access_the_posts_apply', true, $posts, $query ) ) { | |
| 436 | + $user_id = get_current_user_id(); | |
| 437 | + foreach ( $posts as $post ) { | |
| 438 | + if ( self::user_can_read_post( $post->ID, $user_id ) ) { | |
| 439 | + $result[] = $post; | |
| 440 | + } | |
| 228 | 441 | } |
| 442 | + } else { | |
| 443 | + $result = $posts; | |
| 229 | 444 | } |
| 230 | 445 | return $result; |
| 231 | 446 | } |
| 232 | 447 | |
| @@ -231,24 +446,35 @@ | ||
| 231 | 446 | } |
| 232 | 447 | |
| 233 | 448 | /** |
| 234 | 449 | * Filter menu items by access capability. |
| 235 | - * | |
| 236 | - * @todo admin section: this won't inhibit the items being offered to be added, although when they're added they won't show up in the menu | |
| 237 | - * | |
| 450 | + * | |
| 451 | + * Notes for the admin section: | |
| 452 | + * 1. With themes that provide sidebars and widgets, protected items are visible and can be chosen to be added to a menu. But once the menu is saved, those items do not appear, providing a somewhat confusing user experience. | |
| 453 | + * 2. With themes that support full site editing (like Twenty Twenty-Four which gets rid of sidebars and widgets), protected items are not offered to whom is editing a Navigation block. However, protected items that were added by someone who could access them, will be visible. | |
| 454 | + * | |
| 238 | 455 | * @param array $items |
| 239 | 456 | * @param mixed $menu |
| 240 | 457 | * @param array $args |
| 458 | + * | |
| 459 | + * @return array | |
| 241 | 460 | */ |
| 242 | 461 | public static function wp_get_nav_menu_items( $items = null, $menu = null, $args = null ) { |
| 243 | 462 | $result = array(); |
| 244 | - $user_id = get_current_user_id(); | |
| 245 | - foreach ( $items as $item ) { | |
| 246 | - // @todo might want to check $item->object and $item->type first, | |
| 247 | - // for example these are 'page' and 'post_type' for a page | |
| 248 | - if ( self::user_can_read_post( $item->object_id, $user_id ) ) { | |
| 249 | - $result[] = $item; | |
| 463 | + if ( apply_filters( 'groups_post_access_wp_get_nav_menu_items_apply', true, $items, $menu, $args ) ) { | |
| 464 | + $user_id = get_current_user_id(); | |
| 465 | + foreach ( $items as $item ) { | |
| 466 | + // Check whether the menu item is for some post type, otherwise it's for something else which we don't control. | |
| 467 | + if ( is_object( $item ) && isset( $item->type ) && $item->type === 'post_type' ) { | |
| 468 | + if ( self::user_can_read_post( $item->object_id, $user_id ) ) { | |
| 469 | + $result[] = $item; | |
| 470 | + } | |
| 471 | + } else { | |
| 472 | + $result[] = $item; | |
| 473 | + } | |
| 250 | 474 | } |
| 475 | + } else { | |
| 476 | + $result = $items; | |
| 251 | 477 | } |
| 252 | 478 | return $result; |
| 253 | 479 | } |
| 254 | 480 | |
| @@ -253,21 +479,26 @@ | ||
| 253 | 479 | } |
| 254 | 480 | |
| 255 | 481 | /** |
| 256 | 482 | * Filter excerpt by access capability. |
| 257 | - * | |
| 483 | + * | |
| 258 | 484 | * @param string $output |
| 259 | - * @return $output if access granted, otherwise '' | |
| 485 | + * | |
| 486 | + * @return string $output if access granted, otherwise '' | |
| 260 | 487 | */ |
| 261 | 488 | public static function get_the_excerpt( $output ) { |
| 262 | 489 | global $post; |
| 263 | 490 | $result = ''; |
| 264 | - if ( isset( $post->ID ) ) { | |
| 265 | - if ( self::user_can_read_post( $post->ID ) ) { | |
| 491 | + if ( apply_filters( 'groups_post_access_get_the_excerpt_apply', true, $output ) ) { | |
| 492 | + if ( isset( $post->ID ) ) { | |
| 493 | + if ( self::user_can_read_post( $post->ID ) ) { | |
| 494 | + $result = $output; | |
| 495 | + } | |
| 496 | + } else { | |
| 497 | + // not a post, don't interfere | |
| 266 | 498 | $result = $output; |
| 267 | 499 | } |
| 268 | 500 | } else { |
| 269 | - // not a post, don't interfere | |
| 270 | 501 | $result = $output; |
| 271 | 502 | } |
| 272 | 503 | return $result; |
| 273 | 504 | } |
| @@ -275,19 +506,24 @@ | ||
| 275 | 506 | /** |
| 276 | 507 | * Filter content by access capability. |
| 277 | 508 | * |
| 278 | 509 | * @param string $output |
| 279 | - * @return $output if access granted, otherwise '' | |
| 510 | + * | |
| 511 | + * @return string $output if access granted, otherwise '' | |
| 280 | 512 | */ |
| 281 | 513 | public static function the_content( $output ) { |
| 282 | 514 | global $post; |
| 283 | 515 | $result = ''; |
| 284 | - if ( isset( $post->ID ) ) { | |
| 285 | - if ( self::user_can_read_post( $post->ID ) ) { | |
| 516 | + if ( apply_filters( 'groups_post_access_the_content_apply', true, $output ) ) { | |
| 517 | + if ( isset( $post->ID ) ) { | |
| 518 | + if ( self::user_can_read_post( $post->ID ) ) { | |
| 519 | + $result = $output; | |
| 520 | + } | |
| 521 | + } else { | |
| 522 | + // not a post, don't interfere | |
| 286 | 523 | $result = $output; |
| 287 | 524 | } |
| 288 | 525 | } else { |
| 289 | - // not a post, don't interfere | |
| 290 | 526 | $result = $output; |
| 291 | 527 | } |
| 292 | 528 | return $result; |
| 293 | 529 | } |
| @@ -292,15 +528,114 @@ | ||
| 292 | 528 | return $result; |
| 293 | 529 | } |
| 294 | 530 | |
| 295 | 531 | /** |
| 296 | - * Adds an access capability requirement. | |
| 297 | - * | |
| 298 | - * $map must contain 'post_id' (*) | |
| 299 | - * | |
| 300 | - * For now this only should be used to add the READ_POST_CAPABILITY which | |
| 301 | - * it does automatically. Nothing else is checked for granting access. | |
| 302 | - * | |
| 532 | + * Hooked on the get_{$adjacent}_post_where filter to remove restricted posts. | |
| 533 | + * | |
| 534 | + * @param string $where | |
| 535 | + * @param boolean $in_same_term | |
| 536 | + * @param array $excluded_terms | |
| 537 | + * @param string $taxonomy | |
| 538 | + * @param WP_Post $post | |
| 539 | + * | |
| 540 | + * @return string $where modified if appropriate | |
| 541 | + */ | |
| 542 | + public static function get_previous_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { | |
| 543 | + return self::get_next_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ); | |
| 544 | + } | |
| 545 | + | |
| 546 | + /** | |
| 547 | + * Hooked on the get_{$adjacent}_post_where filter to remove restricted posts. | |
| 548 | + * | |
| 549 | + * @param string $where | |
| 550 | + * @param boolean $in_same_term | |
| 551 | + * @param array $excluded_terms | |
| 552 | + * @param string $taxonomy | |
| 553 | + * @param WP_Post $post | |
| 554 | + * | |
| 555 | + * @return string $where modified if appropriate | |
| 556 | + */ | |
| 557 | + public static function get_next_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { | |
| 558 | + if ( | |
| 559 | + !empty( $post ) && // @phpstan-ignore empty.variable | |
| 560 | + self::handles_post_type( $post->post_type ?? '' ) | |
| 561 | + ) { | |
| 562 | + // @since 4.0.0 cached values are purged by Groups_Cache_Robot when flushing the cache group for a post type | |
| 563 | + $cache_group = self::get_post_type_cache_group( $post->post_type ); | |
| 564 | + | |
| 565 | + $post_ids = array( -1 ); | |
| 566 | + $cached = Groups_Cache::get_ext( 'eligible_post_ids', $cache_group ); | |
| 567 | + if ( $cached === null ) { | |
| 568 | + // run it through get_posts with suppress_filters set to false so that our posts_where filter is applied and assures only accessible posts are seen | |
| 569 | + $post_ids = get_posts( array( 'post_type' => $post->post_type, 'numberposts' => -1, 'suppress_filters' => false, 'fields' => 'ids' ) ); | |
| 570 | + if ( is_array( $post_ids ) && count( $post_ids ) > 0 ) { | |
| 571 | + foreach ( $post_ids as $i => $post_id ) { | |
| 572 | + $post_ids[$i] = intval( $post_id ); | |
| 573 | + } | |
| 574 | + } else { | |
| 575 | + $post_ids = array( -1 ); | |
| 576 | + } | |
| 577 | + Groups_Cache::set_ext( 'eligible_post_ids', $post_ids, $cache_group ); | |
| 578 | + } else { | |
| 579 | + $post_ids = $cached->get_value(); | |
| 580 | + } | |
| 581 | + | |
| 582 | + if ( is_array( $post_ids ) && count( $post_ids ) > 0 ) { | |
| 583 | + $condition = ' p.ID IN (' . implode( ',', $post_ids ) . ') '; | |
| 584 | + if ( !empty( $where ) ) { | |
| 585 | + $where .= ' AND ' . $condition; | |
| 586 | + } else { | |
| 587 | + $where = ' WHERE ' . $condition; | |
| 588 | + } | |
| 589 | + } | |
| 590 | + } | |
| 591 | + return $where; | |
| 592 | + } | |
| 593 | + | |
| 594 | + /** | |
| 595 | + * Clears cached eligible post IDs. | |
| 596 | + * | |
| 597 | + * @since 2.17.0 | |
| 598 | + * | |
| 599 | + * @param int $post_id | |
| 600 | + */ | |
| 601 | + public static function save_post( $post_id ) { | |
| 602 | + if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) ) { | |
| 603 | + } else { | |
| 604 | + $post_type = get_post_type( $post_id ); | |
| 605 | + if ( self::handles_post_type( $post_type ) ) { | |
| 606 | + } | |
| 607 | + } | |
| 608 | + } | |
| 609 | + | |
| 610 | + /** | |
| 611 | + * Clear cached eligible post IDs for the 'attachment' post type (the save_post action is not triggered for those). | |
| 612 | + * | |
| 613 | + * @since 2.17.0 | |
| 614 | + * | |
| 615 | + * @param array $post | |
| 616 | + * @param array $attachment | |
| 617 | + * | |
| 618 | + * @return array | |
| 619 | + */ | |
| 620 | + public static function attachment_fields_to_save( $post, $attachment ) { | |
| 621 | + if ( self::handles_post_type( 'attachment' ) ) { | |
| 622 | + $post_id = null; | |
| 623 | + if ( isset( $post['ID'] ) ) { | |
| 624 | + $post_id = $post['ID']; | |
| 625 | + } else if ( isset( $post['post_ID'] ) ) { | |
| 626 | + $post_id = $post['post_ID']; | |
| 627 | + } | |
| 628 | + if ( $post_id !== null ) { | |
| 629 | + self::save_post( $post_id ); | |
| 630 | + } | |
| 631 | + } | |
| 632 | + return $post; | |
| 633 | + } | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * Adds an access requirement based on post_id and group_id. | |
| 637 | + * | |
| 303 | 638 | * (*) Revisions : As of Groups 1.3.13 and at WordPress 3.6.1, as |
| 304 | 639 | * add_post_meta stores postmeta for the revision's parent, we retrieve |
| 305 | 640 | * the parent's post ID if it applies and check against that to see if |
| 306 | 641 | * that capability is already present. This is to avoid duplicating |
| @@ -305,27 +640,39 @@ | ||
| 305 | 640 | * the parent's post ID if it applies and check against that to see if |
| 306 | 641 | * that capability is already present. This is to avoid duplicating |
| 307 | 642 | * the already existing postmeta entry (which ocurred in previous |
| 308 | 643 | * versions). |
| 309 | - * | |
| 310 | - * @param array $map | |
| 311 | - * @return true if the capability could be added to the post, otherwis false | |
| 644 | + * | |
| 645 | + * @param array $map must contain 'post_id' (*) and 'group_id' | |
| 646 | + * | |
| 647 | + * @return true if the access requirement could be added to the post, otherwise false | |
| 312 | 648 | */ |
| 313 | 649 | public static function create( $map ) { |
| 314 | - extract( $map ); | |
| 650 | + | |
| 315 | 651 | $result = false; |
| 316 | 652 | |
| 317 | - if ( !isset( $capability ) ) { | |
| 318 | - $capability = self::READ_POST_CAPABILITY; | |
| 653 | + $capability = isset( $map['capability'] ) ? $map['capability'] : null; | |
| 654 | + $post_id = isset( $map['post_id'] ) ? $map['post_id'] : null; | |
| 655 | + $group_id = isset( $map['group_id'] ) ? $map['group_id'] : null; | |
| 656 | + | |
| 657 | + if ( $capability !== null ) { | |
| 658 | + _doing_it_wrong( | |
| 659 | + __CLASS__ . '::' . __METHOD__, | |
| 660 | + __( 'You should use Groups_Post_Access_Legacy::create() to pass a capability restriction instead.', 'groups' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 661 | + '2.0.0' | |
| 662 | + ); | |
| 319 | 663 | } |
| 320 | 664 | |
| 321 | - if ( !empty( $post_id ) && !empty( $capability) ) { | |
| 322 | - if ( Groups_Capability::read_by_capability( $capability ) ) { | |
| 665 | + if ( !empty( $post_id ) && !empty( $group_id ) ) { | |
| 666 | + $post_id = Groups_Utility::id( $post_id ); | |
| 667 | + $group_id = Groups_Utility::id( $group_id ); | |
| 668 | + if ( Groups_Group::read( $group_id ) ) { | |
| 323 | 669 | if ( $revision_parent_id = wp_is_post_revision( $post_id ) ) { |
| 324 | 670 | $post_id = $revision_parent_id; |
| 325 | 671 | } |
| 326 | - if ( !in_array( $capability, get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY ) ) ) { | |
| 327 | - $result = add_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, $capability ); | |
| 672 | + $stored_group_ids = self::get_read_group_ids( $post_id ); | |
| 673 | + if ( !in_array( $group_id, $stored_group_ids ) ) { | |
| 674 | + $result = add_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ, $group_id ); | |
| 328 | 675 | } |
| 329 | 676 | } |
| 330 | 677 | } |
| 331 | 678 | return $result; |
| @@ -331,50 +678,110 @@ | ||
| 331 | 678 | return $result; |
| 332 | 679 | } |
| 333 | 680 | |
| 334 | 681 | /** |
| 335 | - * Returns true if the post requires the given capability to grant access. | |
| 336 | - * | |
| 337 | - * Currently only READ_POST_CAPABILITY should be used, this is also taken | |
| 338 | - * as the default. | |
| 339 | - * | |
| 340 | - * @param int $post_id | |
| 341 | - * @param string $capability capability label | |
| 342 | - * @return true if the capability is required, otherwise false | |
| 682 | + * Returns true if the post requires the user to be a member of the given group(s) to grant access. | |
| 683 | + * | |
| 684 | + * @param int $post_id ID of the post | |
| 685 | + * @param array $map should provide one or more group IDs via 'groups_read' | |
| 686 | + * | |
| 687 | + * @return boolean true if the group(s) is required, otherwise false | |
| 343 | 688 | */ |
| 344 | - public static function read( $post_id, $capability = self::READ_POST_CAPABILITY ) { | |
| 689 | + public static function read( $post_id, $map = array() ) { | |
| 690 | + | |
| 345 | 691 | $result = false; |
| 346 | - $caps = get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY ); | |
| 347 | - if ( $caps ) { | |
| 348 | - $result = in_array( $capability, $caps ); | |
| 692 | + | |
| 693 | + $groups_read = isset( $map['groups_read'] ) ? $map['groups_read'] : null; | |
| 694 | + | |
| 695 | + if ( !empty( $post_id ) ) { | |
| 696 | + if ( $groups_read !== null ) { | |
| 697 | + if ( empty( $groups_read ) ) { | |
| 698 | + $groups_read = array(); | |
| 699 | + } else if ( !is_array( $groups_read ) ) { | |
| 700 | + $groups_read = array( $groups_read ); | |
| 701 | + } | |
| 702 | + $group_ids = self::get_read_group_ids( $post_id ); | |
| 703 | + if ( $group_ids ) { | |
| 704 | + foreach ( $groups_read as $group_id ) { | |
| 705 | + $result = in_array( $group_id, $group_ids ); | |
| 706 | + if ( !$result ) { | |
| 707 | + break; | |
| 708 | + } | |
| 709 | + } | |
| 710 | + } | |
| 711 | + } | |
| 349 | 712 | } |
| 350 | 713 | return $result; |
| 351 | 714 | } |
| 352 | 715 | |
| 353 | 716 | /** |
| 354 | - * Currently does nothing, always returns false. | |
| 355 | - * | |
| 717 | + * Update the post access restrictions. | |
| 718 | + * | |
| 719 | + * $map must provide 'post_id' (int) indicating the post's ID and 'groups_read' (int|array of int) holding group IDs that restrict read access. | |
| 720 | + * | |
| 356 | 721 | * @param array $map |
| 357 | - * @return false | |
| 722 | + * | |
| 723 | + * @return array of group ids, false on failure | |
| 358 | 724 | */ |
| 359 | 725 | public static function update( $map ) { |
| 360 | - return false; | |
| 726 | + | |
| 727 | + $result = false; | |
| 728 | + | |
| 729 | + $post_id = isset( $map['post_id'] ) ? $map['post_id'] : null; | |
| 730 | + $groups_read = isset( $map['groups_read'] ) ? $map['groups_read'] : null; | |
| 731 | + | |
| 732 | + if ( !empty( $post_id ) ) { | |
| 733 | + if ( empty( $groups_read ) ) { | |
| 734 | + $groups_read = array(); | |
| 735 | + } else if ( !is_array( $groups_read ) ) { | |
| 736 | + $groups_read = array( $groups_read ); | |
| 737 | + } | |
| 738 | + $groups_read = array_map( array( 'Groups_Utility', 'id' ), $groups_read ); | |
| 739 | + $current_groups_read = self::get_read_group_ids( $post_id ); | |
| 740 | + $current_groups_read = array_map( array( 'Groups_Utility', 'id' ), $current_groups_read ); | |
| 741 | + foreach ( $groups_read as $group_id ) { | |
| 742 | + if ( !in_array( $group_id, $current_groups_read ) ) { | |
| 743 | + add_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ, $group_id ); | |
| 744 | + } | |
| 745 | + } | |
| 746 | + foreach ( $current_groups_read as $group_id ) { | |
| 747 | + if ( !in_array( $group_id, $groups_read ) ) { | |
| 748 | + delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ, $group_id ); | |
| 749 | + } | |
| 750 | + } | |
| 751 | + $stored_group_ids = self::get_read_group_ids( $post_id ); | |
| 752 | + $result = array_map( array( 'Groups_Utility', 'id' ), $stored_group_ids ); | |
| 753 | + } | |
| 754 | + return $result; | |
| 361 | 755 | } |
| 362 | 756 | |
| 363 | 757 | /** |
| 364 | - * Removes a capability requirement from a post. | |
| 365 | - * | |
| 758 | + * Removes access restrictions from a post. | |
| 759 | + * | |
| 366 | 760 | * @param int $post_id |
| 367 | - * @param string $capability defaults to groups_read_post, removes all if null is given | |
| 368 | - * @return true on success, otherwise false | |
| 761 | + * @param array $map must provide 'groups_read' holding group IDs to remove from restricting access to the post; if empty, all access restrictions will be removed | |
| 762 | + * | |
| 763 | + * @return boolean true on success, otherwise false | |
| 369 | 764 | */ |
| 370 | - public static function delete( $post_id, $capability = self::READ_POST_CAPABILITY ) { | |
| 765 | + public static function delete( $post_id, $map = array() ) { | |
| 766 | + | |
| 371 | 767 | $result = false; |
| 768 | + | |
| 769 | + $groups_read = isset( $map['groups_read'] ) ? $map['groups_read'] : null; | |
| 770 | + | |
| 372 | 771 | if ( !empty( $post_id ) ) { |
| 373 | - if ( !empty( $capability ) ) { | |
| 374 | - $result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, $capability ); | |
| 772 | + if ( empty( $groups_read ) ) { | |
| 773 | + $groups_read = array(); | |
| 774 | + } else if ( !is_array( $groups_read ) ) { | |
| 775 | + $groups_read = array( $groups_read ); | |
| 776 | + } | |
| 777 | + $groups_read = array_map( array( 'Groups_Utility', 'id' ), $groups_read ); | |
| 778 | + if ( !empty( $groups_read ) ) { | |
| 779 | + foreach ( $groups_read as $group_id ) { | |
| 780 | + $result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ, $group_id ); | |
| 781 | + } | |
| 375 | 782 | } else { |
| 376 | - $result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY ); | |
| 783 | + $result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ ); | |
| 377 | 784 | } |
| 378 | 785 | } |
| 379 | 786 | return $result; |
| 380 | 787 | } |
| @@ -380,45 +787,92 @@ | ||
| 380 | 787 | } |
| 381 | 788 | |
| 382 | 789 | /** |
| 383 | 790 | * Returns a list of capabilities that grant access to the post. |
| 384 | - * | |
| 791 | + * | |
| 792 | + * @deprecated | |
| 793 | + * | |
| 385 | 794 | * @param int $post_id |
| 795 | + * | |
| 386 | 796 | * @return array of string, capabilities |
| 387 | 797 | */ |
| 388 | 798 | public static function get_read_post_capabilities( $post_id ) { |
| 389 | - return get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY ); | |
| 799 | + _doing_it_wrong( | |
| 800 | + __CLASS__ . '::' . __METHOD__, | |
| 801 | + __( 'This method is deprecated. You should use Groups_Post_Access_Legacy::get_read_post_capabilities() to retrieve the capabilities instead.', 'groups' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 802 | + '2.0.0' | |
| 803 | + ); | |
| 804 | + | |
| 805 | + require_once GROUPS_LEGACY_LIB . '/access/class-groups-post-access-legacy.php'; | |
| 806 | + return Groups_Post_Access_Legacy::get_read_post_capabilities( $post_id ); | |
| 390 | 807 | } |
| 391 | 808 | |
| 392 | 809 | /** |
| 393 | - * Returns true if the user has any of the capabilities that grant access to the post. | |
| 394 | - * | |
| 810 | + * Returns a list of group IDs that grant read access to the post. | |
| 811 | + * | |
| 812 | + * @param int $post_id | |
| 813 | + * | |
| 814 | + * @return array of int, group IDs | |
| 815 | + */ | |
| 816 | + public static function get_read_group_ids( $post_id ) { | |
| 817 | + $result = array(); | |
| 818 | + $group_ids = get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ ); | |
| 819 | + if ( is_array( $group_ids ) ) { | |
| 820 | + foreach ( $group_ids as $group_id ) { | |
| 821 | + // @since 2.18.0 discard invalid group IDs | |
| 822 | + if ( !empty( $group_id ) && Groups_Group::exists( $group_id ) ) { | |
| 823 | + $result[] = intval( $group_id ); | |
| 824 | + } | |
| 825 | + } | |
| 826 | + } | |
| 827 | + return $result; | |
| 828 | + } | |
| 829 | + | |
| 830 | + /** | |
| 831 | + * Returns true if the user belongs to any of the groups that grant access to the post. | |
| 832 | + * | |
| 395 | 833 | * @param int $post_id post id |
| 396 | - * @param int $user_id user id or null for current user | |
| 834 | + * @param int $user_id user id or null for current user | |
| 835 | + * | |
| 397 | 836 | * @return boolean true if user can read the post |
| 398 | 837 | */ |
| 399 | 838 | public static function user_can_read_post( $post_id, $user_id = null ) { |
| 839 | + | |
| 400 | 840 | $result = false; |
| 841 | + | |
| 401 | 842 | if ( !empty( $post_id ) ) { |
| 843 | + | |
| 402 | 844 | if ( $user_id === null ) { |
| 403 | 845 | $user_id = get_current_user_id(); |
| 404 | 846 | } |
| 847 | + | |
| 405 | 848 | $cached = Groups_Cache::get( self::CAN_READ_POST . '_' . $user_id . '_' . $post_id, self::CACHE_GROUP ); |
| 849 | + | |
| 406 | 850 | if ( $cached !== null ) { |
| 407 | - $result = $cached->value; | |
| 408 | - unset( $cached ) ; | |
| 851 | + $result = $cached->get_value(); | |
| 852 | + unset( $cached ); | |
| 409 | 853 | } else { |
| 410 | - $groups_user = new Groups_User( $user_id ); | |
| 411 | - $read_caps = self::get_read_post_capabilities( $post_id ); | |
| 412 | - if ( !empty( $read_caps ) ) { | |
| 413 | - foreach( $read_caps as $read_cap ) { | |
| 414 | - if ( $groups_user->can( $read_cap ) ) { | |
| 854 | + // admin override and Groups admins see everything | |
| 855 | + if ( _groups_admin_override() || Groups_User::current_user_can( GROUPS_ADMINISTER_GROUPS ) ) { | |
| 856 | + $result = true; | |
| 857 | + } else { | |
| 858 | + // can read if post type is not handled | |
| 859 | + if ( $post_type = get_post_type( $post_id ) ) { | |
| 860 | + if ( !self::handles_post_type( $post_type ) ) { | |
| 415 | 861 | $result = true; |
| 416 | - break; | |
| 417 | 862 | } |
| 418 | 863 | } |
| 419 | - } else { | |
| 420 | - $result = true; | |
| 864 | + // check if the user can read | |
| 865 | + if ( !$result ) { | |
| 866 | + $groups_user = new Groups_User( $user_id ); | |
| 867 | + $group_ids = self::get_read_group_ids( $post_id ); | |
| 868 | + if ( empty( $group_ids ) ) { | |
| 869 | + $result = true; | |
| 870 | + } else { | |
| 871 | + $ids = array_intersect( $groups_user->get_group_ids_deep(), $group_ids ); | |
| 872 | + $result = !empty( $ids ); | |
| 873 | + } | |
| 874 | + } | |
| 421 | 875 | } |
| 422 | 876 | $result = apply_filters( 'groups_post_access_user_can_read_post', $result, $post_id, $user_id ); |
| 423 | 877 | Groups_Cache::set( self::CAN_READ_POST . '_' . $user_id . '_' . $post_id, $result, self::CACHE_GROUP ); |
| 424 | 878 | } |
| @@ -426,15 +880,460 @@ | ||
| 426 | 880 | return $result; |
| 427 | 881 | } |
| 428 | 882 | |
| 429 | 883 | /** |
| 430 | - * Hooks into groups_deleted_capability_capability to remove existing access | |
| 431 | - * restrictions based on the deleted capability. | |
| 432 | - * | |
| 433 | - * @param string $name of the deleted capability | |
| 884 | + * Hooks into groups_deleted_group to remove existing access restrictions | |
| 885 | + * based on the deleted group. | |
| 886 | + * | |
| 887 | + * @param int $group_id the ID of the deleted group | |
| 434 | 888 | */ |
| 435 | - public static function groups_deleted_capability_capability( $capability ) { | |
| 436 | - delete_metadata( 'post', null, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, $capability, true ); | |
| 889 | + public static function groups_deleted_group( $group_id ) { | |
| 890 | + if ( $group_id ) { | |
| 891 | + delete_metadata( 'post', null, self::POSTMETA_PREFIX . self::READ, $group_id, true ); | |
| 892 | + } | |
| 437 | 893 | } |
| 438 | 894 | |
| 895 | + /** | |
| 896 | + * Hooked on wp_count_posts to correct the post counts. | |
| 897 | + * | |
| 898 | + * Note: at WP 4.7.4 through WP_Posts_List_Table::prepare_items() which obtains $post_status | |
| 899 | + * independent of the post type, we will come here for any post status, so don't be surprised | |
| 900 | + * to see this executed e.g. on post type 'post' with e.g. 'wc-completed' post status. | |
| 901 | + * | |
| 902 | + * @param object $counts An object containing the current post_type's post counts by status. | |
| 903 | + * @param string $type the post type | |
| 904 | + * @param string $perm The permission to determine if the posts are 'readable' by the current user. | |
| 905 | + * | |
| 906 | + * @return object | |
| 907 | + */ | |
| 908 | + public static function wp_count_posts( $counts, $type, $perm ) { | |
| 909 | + // @since 3.3.1 remove temporarily to avoid potential infinite recursion https://github.com/itthinx/groups/pull/160 | |
| 910 | + remove_filter( 'wp_count_posts', array( __CLASS__, 'wp_count_posts' ), 10 ); | |
| 911 | + if ( !empty( $type ) && is_string( $type ) && self::handles_post_type( $type ) ) { | |
| 912 | + // @since 4.0.0 counts in cache are purged by Groups_Cache_Robot when flushing the cache group for a post type | |
| 913 | + $cache_group = self::get_post_type_cache_group( $type ); | |
| 914 | + $cached = Groups_Cache::get_ext( 'count_posts', $cache_group ); | |
| 915 | + if ( $cached !== null ) { | |
| 916 | + $counts = $cached->get_value(); | |
| 917 | + } else { | |
| 918 | + foreach ( $counts as $post_status => $count ) { | |
| 919 | + $query_args = array( | |
| 920 | + 'fields' => 'ids', | |
| 921 | + 'post_type' => $type, | |
| 922 | + 'post_status' => $post_status, | |
| 923 | + 'numberposts' => -1, // all | |
| 924 | + 'suppress_filters' => false, // don't suppress filters as we need to get restrictions taken into account | |
| 925 | + 'orderby' => 'none', // Important! Don't waste time here. | |
| 926 | + 'no_found_rows' => true, // performance, omit unnecessary SQL_CALC_FOUND_ROWS in query here | |
| 927 | + 'nopaging' => true // no paging is needed, get all corresponding posts | |
| 928 | + ); | |
| 929 | + // WooCommerce Orders | |
| 930 | + if ( function_exists( 'wc_get_order_statuses' ) && ( $type == 'shop_order' ) ) { | |
| 931 | + $wc_order_statuses = array_keys( wc_get_order_statuses() ); | |
| 932 | + if ( !in_array( $post_status, $wc_order_statuses ) ) { | |
| 933 | + // Skip getting the post count for this status as it's | |
| 934 | + // not a valid order status and WC would raise a PHP Notice. | |
| 935 | + continue; | |
| 936 | + } | |
| 937 | + } | |
| 938 | + // WooCommerce Subscriptions | |
| 939 | + if ( function_exists( 'wcs_get_subscription_statuses' ) && ( $type == 'shop_subscription' ) ) { | |
| 940 | + $wc_subscription_statuses = array_keys( wcs_get_subscription_statuses() ); | |
| 941 | + if ( !in_array( $post_status, $wc_subscription_statuses ) ) { | |
| 942 | + // Skip as it's not a valid subscription status | |
| 943 | + continue; | |
| 944 | + } | |
| 945 | + } | |
| 946 | + do_action( 'groups_post_access_wp_count_posts_before_query', $query_args, $counts, $type, $perm ); | |
| 947 | + $posts = get_posts( $query_args ); | |
| 948 | + do_action( 'groups_post_access_wp_count_posts_after_query', $query_args, $counts, $type, $perm ); | |
| 949 | + $count = count( $posts ); | |
| 950 | + unset( $posts ); | |
| 951 | + $counts->$post_status = $count; | |
| 952 | + } | |
| 953 | + Groups_Cache::set_ext( 'count_posts', $counts, $cache_group ); | |
| 954 | + } | |
| 955 | + } | |
| 956 | + // @since 3.3.1 reestablish filter for next use | |
| 957 | + add_filter( 'wp_count_posts', array( __CLASS__, 'wp_count_posts' ), 10, 3 ); | |
| 958 | + return $counts; | |
| 959 | + } | |
| 960 | + | |
| 961 | + /** | |
| 962 | + * Would be hooked on wp_count_attachments to correct the counts but it's not actually | |
| 963 | + * being used in the current media library. | |
| 964 | + * | |
| 965 | + * @param object $counts An object containing the attachment counts by mime type. | |
| 966 | + * @param string $mime_type The mime type pattern used to filter the attachments counted. | |
| 967 | + * | |
| 968 | + * @return object | |
| 969 | + */ | |
| 970 | + public static function wp_count_attachments( $counts, $mime_type ) { | |
| 971 | + return $counts; | |
| 972 | + } | |
| 973 | + | |
| 974 | + /** | |
| 975 | + * Returns true if we are supposed to handle the post type, otherwise false. | |
| 976 | + * | |
| 977 | + * @param string $post_type | |
| 978 | + * | |
| 979 | + * @return boolean | |
| 980 | + */ | |
| 981 | + public static function handles_post_type( $post_type ) { | |
| 982 | + $post_types = self::get_handles_post_types(); | |
| 983 | + return isset( $post_types[$post_type] ) && $post_types[$post_type]; | |
| 984 | + } | |
| 985 | + | |
| 986 | + /** | |
| 987 | + * Returns an array of post types indicating for each whether we handle it (true) or not. | |
| 988 | + * The array is indexed by the post type names. | |
| 989 | + * | |
| 990 | + * @return array indexed by post type names, indicating the value true if we handle it, otherwise false | |
| 991 | + */ | |
| 992 | + public static function get_handles_post_types() { | |
| 993 | + $result = array(); | |
| 994 | + $post_types_option = Groups_Options::get_option( self::POST_TYPES, array() ); | |
| 995 | + $post_types = get_post_types( array(), 'objects' ); | |
| 996 | + foreach ( $post_types as $post_type => $object ) { | |
| 997 | + $public = isset( $object->public ) ? $object->public : false; | |
| 998 | + // $exclude_from_search = isset( $object->exclude_from_search ) ? $object->exclude_from_search : false; | |
| 999 | + // $publicly_queryable = isset( $object->publicly_queryable ) ? $object->publicly_queryable : false; | |
| 1000 | + // $show_ui = isset( $object->show_ui ) ? $object->show_ui : false; | |
| 1001 | + // $show_in_nav_menus = isset( $object->show_in_nav_menus ) ? $object->show_in_nav_menus : false; | |
| 1002 | + | |
| 1003 | + // by default, handle any post type whose public attribute is true | |
| 1004 | + $managed = | |
| 1005 | + $public && ( !isset( $post_types_option[$post_type] ) || !isset( $post_types_option[$post_type]['add_meta_box'] ) ) || | |
| 1006 | + isset( $post_types_option[$post_type] ) && isset( $post_types_option[$post_type]['add_meta_box'] ) && $post_types_option[$post_type]['add_meta_box']; | |
| 1007 | + $result[$post_type] = $managed; | |
| 1008 | + } | |
| 1009 | + return $result; | |
| 1010 | + } | |
| 1011 | + | |
| 1012 | + /** | |
| 1013 | + * Set which post types we should handle. | |
| 1014 | + * | |
| 1015 | + * @param array $post_types of post type names mapped to booleans, indicating to handle or not a post type | |
| 1016 | + */ | |
| 1017 | + public static function set_handles_post_types( $post_types ) { | |
| 1018 | + $post_types_option = Groups_Options::get_option( self::POST_TYPES, array() ); | |
| 1019 | + $available_post_types = get_post_types(); | |
| 1020 | + foreach ( $available_post_types as $post_type ) { | |
| 1021 | + $post_types_option[$post_type]['add_meta_box'] = isset( $post_types[$post_type] ) && $post_types[$post_type]; | |
| 1022 | + } | |
| 1023 | + Groups_Options::update_option( self::POST_TYPES, $post_types_option ); | |
| 1024 | + } | |
| 1025 | + | |
| 1026 | + /** | |
| 1027 | + * Filter the block content of core/navigation-link and core/navigation-submenu blocks. | |
| 1028 | + * | |
| 1029 | + * This is necessary as these blocks would render their content although the corresponding post is protected. | |
| 1030 | + * | |
| 1031 | + * @since 2.20.0 | |
| 1032 | + * | |
| 1033 | + * @param string $block_content | |
| 1034 | + * @param array $parsed_block | |
| 1035 | + * @param \WP_Block $block | |
| 1036 | + * | |
| 1037 | + * @return string | |
| 1038 | + */ | |
| 1039 | + public static function render_block( $block_content, $parsed_block, $block ) { | |
| 1040 | + if ( !is_admin() ) { | |
| 1041 | + /** | |
| 1042 | + * Whether to process this block. | |
| 1043 | + * | |
| 1044 | + * @param boolean $filter whether to filter the block | |
| 1045 | + * @param string $block_content the block's content | |
| 1046 | + * @param array $parsed_block the parsed block | |
| 1047 | + * @param \WP_Block $block the block | |
| 1048 | + * | |
| 1049 | + * @return boolean whether to filter | |
| 1050 | + */ | |
| 1051 | + if ( apply_filters( 'groups_post_access_filter_render_block', true, $block_content, $parsed_block, $block ) ) { | |
| 1052 | + $is_valid = true; | |
| 1053 | + if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { | |
| 1054 | + if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { | |
| 1055 | + $post_id = $block->attributes['id']; | |
| 1056 | + if ( !self::user_can_read_post( $post_id ) ) { | |
| 1057 | + $is_valid = false; | |
| 1058 | + } | |
| 1059 | + } | |
| 1060 | + } | |
| 1061 | + if ( !$is_valid ) { | |
| 1062 | + $block_content = ''; | |
| 1063 | + } | |
| 1064 | + } | |
| 1065 | + } | |
| 1066 | + return $block_content; | |
| 1067 | + } | |
| 1068 | + | |
| 1069 | + /** | |
| 1070 | + * Short-circuits render_block() and WP_Block->render(). | |
| 1071 | + * | |
| 1072 | + * For core/navigation-link and core/navigation-submenu blocks: | |
| 1073 | + * - This will be overridden for dynamic blocks, so the render_block filter is necessary instead. | |
| 1074 | + * | |
| 1075 | + * For core/categories blocks: | |
| 1076 | + * - We use this hook to add our get_terms filter. | |
| 1077 | + * | |
| 1078 | + * @see Groups_Post_Access::render_block() | |
| 1079 | + * | |
| 1080 | + * @since 2.20.0 | |
| 1081 | + * | |
| 1082 | + * @param string|null $pre_render | |
| 1083 | + * @param array $parsed_block | |
| 1084 | + * @param \WP_Block|null $parent_block | |
| 1085 | + * | |
| 1086 | + * @return string|null | |
| 1087 | + */ | |
| 1088 | + public static function pre_render_block( $pre_render, $parsed_block, $parent_block ) { | |
| 1089 | + if ( !is_admin() ) { | |
| 1090 | + /** | |
| 1091 | + * Whether to process this block pre rendering. | |
| 1092 | + * | |
| 1093 | + * @param boolean $filter whether to filter | |
| 1094 | + * @param string $pre_render block content | |
| 1095 | + * @param array $parsed_block the parsed block | |
| 1096 | + * @param \WP_Block $parent_block the parent block | |
| 1097 | + * | |
| 1098 | + * @return boolean whether to filter | |
| 1099 | + */ | |
| 1100 | + if ( apply_filters( 'groups_post_access_filter_pre_render_block', true, $pre_render, $parsed_block, $parent_block ) ) { | |
| 1101 | + $block = new \WP_Block( $parsed_block ); | |
| 1102 | + // $is_valid = true; | |
| 1103 | + // if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { | |
| 1104 | + // if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { | |
| 1105 | + // $post_id = $block->attributes['id']; | |
| 1106 | + // if ( !self::user_can_read_post( $post_id ) ) { | |
| 1107 | + // $is_valid = false; | |
| 1108 | + // } | |
| 1109 | + // } | |
| 1110 | + // } | |
| 1111 | + // if ( !$is_valid ) { | |
| 1112 | + // $pre_render = ''; | |
| 1113 | + // } | |
| 1114 | + | |
| 1115 | + // Detect a categories block and add our filter to update the counts | |
| 1116 | + if ( 'core/categories' === $block->name ) { | |
| 1117 | + self::$filter_get_terms_block = $block; | |
| 1118 | + add_filter( 'get_terms', array( __CLASS__, 'get_terms' ), 10, 4 ); | |
| 1119 | + } | |
| 1120 | + } | |
| 1121 | + } | |
| 1122 | + return $pre_render; | |
| 1123 | + } | |
| 1124 | + | |
| 1125 | + /** | |
| 1126 | + * Filter inner navigation blocks. | |
| 1127 | + * | |
| 1128 | + * This will not handle the inner blocks of core/navigation-submenu blocks, so the filter on render_block implemented here in this class is necessary. | |
| 1129 | + * | |
| 1130 | + * @since 2.20.0 | |
| 1131 | + * | |
| 1132 | + * @param \WP_Block_List $inner_blocks | |
| 1133 | + * | |
| 1134 | + * @return \WP_Block_List | |
| 1135 | + */ | |
| 1136 | + public static function block_core_navigation_render_inner_blocks( $inner_blocks ) { | |
| 1137 | + if ( !is_admin() ) { | |
| 1138 | + /** | |
| 1139 | + * Whether to filter the inner blocks. | |
| 1140 | + * | |
| 1141 | + * @param boolean $filter whether to filter | |
| 1142 | + * @param \WP_Block_List $inner_blocks the inner blocks | |
| 1143 | + * | |
| 1144 | + * @return \WP_Block_List the inner blocks | |
| 1145 | + */ | |
| 1146 | + if ( apply_filters( 'groups_post_access_filter_block_core_navigation_render_inner_blocks', true, $inner_blocks ) ) { | |
| 1147 | + $valid_inner_blocks = array(); | |
| 1148 | + /** | |
| 1149 | + * @var \WP_Block[] $blocks | |
| 1150 | + */ | |
| 1151 | + $blocks = iterator_to_array( $inner_blocks ); | |
| 1152 | + foreach ( $blocks as $block ) { | |
| 1153 | + $is_valid = true; | |
| 1154 | + // @see block_core_navigation_from_block_get_post_ids( $block ) | |
| 1155 | + if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { | |
| 1156 | + if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { | |
| 1157 | + $post_id = $block->attributes['id']; | |
| 1158 | + if ( !self::user_can_read_post( $post_id ) ) { | |
| 1159 | + $is_valid = false; | |
| 1160 | + } | |
| 1161 | + } | |
| 1162 | + } | |
| 1163 | + if ( $is_valid ) { | |
| 1164 | + $valid_inner_blocks[] = $block; | |
| 1165 | + } | |
| 1166 | + } | |
| 1167 | + $inner_blocks = new WP_Block_List( $valid_inner_blocks ); | |
| 1168 | + } | |
| 1169 | + } | |
| 1170 | + return $inner_blocks; | |
| 1171 | + } | |
| 1172 | + | |
| 1173 | + /** | |
| 1174 | + * Hooks into the filter to activate our get_terms filter. | |
| 1175 | + * | |
| 1176 | + * @since 2.20.0 | |
| 1177 | + * | |
| 1178 | + * @param array $cat_args | |
| 1179 | + * @param array $instance | |
| 1180 | + * | |
| 1181 | + * @return array | |
| 1182 | + */ | |
| 1183 | + public static function widget_categories_args( $cat_args, $instance ) { | |
| 1184 | + if ( !is_admin() ) { | |
| 1185 | + /** | |
| 1186 | + * Whether to filter get_terms. | |
| 1187 | + * | |
| 1188 | + * @param boolean $filter whether to filter | |
| 1189 | + * @param array $cat_args category parameters | |
| 1190 | + * @param array $instance instance details | |
| 1191 | + * | |
| 1192 | + * @return array | |
| 1193 | + */ | |
| 1194 | + if ( apply_filters( 'groups_post_access_filter_widget_categories_args', true, $cat_args, $instance ) ) { | |
| 1195 | + self::$filter_get_terms_widget = $instance; | |
| 1196 | + add_filter( 'get_terms', array( __CLASS__, 'get_terms' ), 10, 4 ); | |
| 1197 | + } | |
| 1198 | + } | |
| 1199 | + return $cat_args; | |
| 1200 | + } | |
| 1201 | + | |
| 1202 | + /** | |
| 1203 | + * Hooks into the filter to activate our get_terms filter. | |
| 1204 | + * | |
| 1205 | + * @since 2.20.0 | |
| 1206 | + * | |
| 1207 | + * @param array $cat_args | |
| 1208 | + * @param array $instance | |
| 1209 | + * | |
| 1210 | + * @return array | |
| 1211 | + */ | |
| 1212 | + public static function widget_categories_dropdown_args( $cat_args, $instance ) { | |
| 1213 | + if ( !is_admin() ) { | |
| 1214 | + /** | |
| 1215 | + * Whether to filter get_terms. | |
| 1216 | + * | |
| 1217 | + * @param boolean $filter whether to filter | |
| 1218 | + * @param array $cat_args category parameters | |
| 1219 | + * @param array $instance instance details | |
| 1220 | + * | |
| 1221 | + * @return array | |
| 1222 | + */ | |
| 1223 | + if ( apply_filters( 'groups_post_access_filter_widget_categories_dropdown_args', true, $cat_args, $instance ) ) { | |
| 1224 | + self::$filter_get_terms_widget = $instance; | |
| 1225 | + add_filter( 'get_terms', array( __CLASS__, 'get_terms' ), 10, 4 ); | |
| 1226 | + } | |
| 1227 | + } | |
| 1228 | + return $cat_args; | |
| 1229 | + } | |
| 1230 | + | |
| 1231 | + /** | |
| 1232 | + * Filter get_terms to adjust counts. | |
| 1233 | + * | |
| 1234 | + * @since 2.20.0 | |
| 1235 | + * | |
| 1236 | + * @param array $terms | |
| 1237 | + * @param array|null $taxonomies | |
| 1238 | + * @param array $query_vars | |
| 1239 | + * @param \WP_Term_Query $term_query | |
| 1240 | + * | |
| 1241 | + * @return array | |
| 1242 | + */ | |
| 1243 | + public static function get_terms( $terms, $taxonomies, $query_vars, $term_query ) { | |
| 1244 | + | |
| 1245 | + // act only once per add_filter we do in the specific cases we cover | |
| 1246 | + remove_filter( 'get_terms', array( __CLASS__, 'get_terms' ), 10 ); | |
| 1247 | + | |
| 1248 | + if ( !is_admin() ) { | |
| 1249 | + /** | |
| 1250 | + * Whether to filter get_terms. | |
| 1251 | + * | |
| 1252 | + * @param boolean $filter whether to filter | |
| 1253 | + * @param array $terms | |
| 1254 | + * @param array|null $taxonomies | |
| 1255 | + * @param array $query_vars | |
| 1256 | + * @param \WP_Term_Query $term_query | |
| 1257 | + * | |
| 1258 | + * @return array | |
| 1259 | + */ | |
| 1260 | + if ( apply_filters( 'groups_post_access_filter_get_terms', true, $terms, $taxonomies, $query_vars, $term_query ) ) { | |
| 1261 | + foreach ( $terms as $term ) { | |
| 1262 | + if ( is_object( $term ) && property_exists( $term, 'term_id' ) ) { | |
| 1263 | + $query_args = array( | |
| 1264 | + 'cat' => $term->term_id, // this category term | |
| 1265 | + 'fields' => 'ids', | |
| 1266 | + 'post_type' => 'post', | |
| 1267 | + 'post_status' => 'publish', | |
| 1268 | + 'numberposts' => -1, // all | |
| 1269 | + 'suppress_filters' => false, // apply restrictions | |
| 1270 | + 'orderby' => 'none', // performance | |
| 1271 | + 'no_found_rows' => true, // performance | |
| 1272 | + 'nopaging' => true // all | |
| 1273 | + ); | |
| 1274 | + $post_ids = get_posts( $query_args ); | |
| 1275 | + $term->count = count( $post_ids ); | |
| 1276 | + } | |
| 1277 | + } | |
| 1278 | + _pad_term_counts( $terms, $taxonomies[0] ); | |
| 1279 | + $remove_empty = false; | |
| 1280 | + if ( self::$filter_get_terms_block !== null ) { | |
| 1281 | + if ( | |
| 1282 | + is_object( self::$filter_get_terms_block ) && | |
| 1283 | + // will mislead returning false because of dynamic properties : property_exists( self::$filter_get_terms_block, 'attributes' ) && | |
| 1284 | + is_array( self::$filter_get_terms_block->attributes ) && | |
| 1285 | + array_key_exists( 'showEmpty', self::$filter_get_terms_block->attributes ) && | |
| 1286 | + !self::$filter_get_terms_block->attributes['showEmpty'] | |
| 1287 | + ) { | |
| 1288 | + $remove_empty = true; | |
| 1289 | + } | |
| 1290 | + } | |
| 1291 | + if ( self::$filter_get_terms_widget !== null ) { | |
| 1292 | + // There is no option to hide empty categories for the widget, so we always remove them. | |
| 1293 | + // Otherwise the condition would be like ... | |
| 1294 | + // if ( is_array( self::$filter_get_terms_widget ) && array_key_exists( 'show_empty', self::$filter_get_terms_widget ) && !self::$filter_get_terms_widget['show_empty'] ) | |
| 1295 | + // There seems to be a bug in WP_Widget_Categories with showing the counts. Even though the "Show post counts" option was checked, the post counts are not shown. Tested with WP 6.4.2 and Classic Widgets 0.3. | |
| 1296 | + $remove_empty = true; | |
| 1297 | + } | |
| 1298 | + if ( $remove_empty ) { | |
| 1299 | + $_terms = array(); | |
| 1300 | + foreach ( $terms as $term ) { | |
| 1301 | + if ( is_object( $term ) && property_exists( $term, 'count' ) ) { | |
| 1302 | + if ( $term->count > 0 ) { | |
| 1303 | + $_terms[] = $term; | |
| 1304 | + } | |
| 1305 | + } | |
| 1306 | + } | |
| 1307 | + $terms = $_terms; | |
| 1308 | + } | |
| 1309 | + } | |
| 1310 | + } | |
| 1311 | + | |
| 1312 | + // void context | |
| 1313 | + self::$filter_get_terms_block = null; | |
| 1314 | + self::$filter_get_terms_widget = null; | |
| 1315 | + | |
| 1316 | + return $terms; | |
| 1317 | + } | |
| 1318 | + | |
| 1319 | + /** | |
| 1320 | + * Cache group for post type. | |
| 1321 | + * | |
| 1322 | + * @since 4.0.0 | |
| 1323 | + * | |
| 1324 | + * @param string|WP_Post_Type $post_type | |
| 1325 | + * | |
| 1326 | + * @return string | |
| 1327 | + */ | |
| 1328 | + public static function get_post_type_cache_group( $post_type ) { | |
| 1329 | + if ( $post_type instanceof WP_Post_Type ) { | |
| 1330 | + $post_type = $post_type->name; | |
| 1331 | + } | |
| 1332 | + $post_type = is_string( $post_type ) ? trim( sanitize_key( $post_type ) ) : 'void'; | |
| 1333 | + if ( strlen( $post_type ) === 0 ) { | |
| 1334 | + $post_type = 'void'; | |
| 1335 | + } | |
| 1336 | + return self::CACHE_GROUP . '_pt_' . $post_type; | |
| 1337 | + } | |
| 439 | 1338 | } |
| 440 | 1339 | Groups_Post_Access::init(); |