PluginProbe
Groups – Memberships and Access Control / trunk
Groups – Memberships and Access Control vtrunk
4.6.0 4.5.0 4.4.0 4.3.0 trunk 1.0.0-beta-1 1.0.0-beta-2 1.0.0-beta-3 1.0.0-beta-3b 1.0.0-beta-3c 1.0.0-beta-3d 1.1.4 1.1.5 1.10.0 1.10.1 1.10.2 1.10.3 1.11.0 1.11.1 1.11.2 1.11.3 1.12.0 1.13.0 1.13.1 1.2.0 All 129 releases
groups / lib / access / class-groups-post-access.php

class-groups-post-access.php in Groups – Memberships and Access Control trunk, at lib/access/class-groups-post-access.php

1,340 lines 43.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class-groups-post-access.php
4 *
5 * Copyright (c) "kento" Karim Rahimpur www.itthinx.com
6 *
7 * This code is released under the GNU General Public License.
8 * See COPYRIGHT.txt and LICENSE.txt.
9 *
10 * This code is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * This header and all notices must be kept intact.
16 *
17 * @author Karim Rahimpur
18 * @package groups
19 * @since groups 1.0.0
20 */
21
22 if ( !defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Post access restrictions.
28 */
29 class Groups_Post_Access {
30
31 /**
32 * @var string
33 */
34 const POSTMETA_PREFIX = 'groups-';
35
36 /**
37 * @var string
38 */
39 const READ = 'read';
40
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 */
68 const READ_POST_CAPABILITIES = 'read_post_capabilities';
69
70 /**
71 * @var string
72 */
73 const POST_TYPES = 'post_types';
74
75 /**
76 * @since 2.20.0
77 *
78 * @var \WP_Block block for which to filter
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 */
94 public static function activate() {
95 }
96
97 /**
98 * Sets up filters to restrict access.
99 */
100 public static function init() {
101 // post access
102 add_filter( 'posts_where', array( __CLASS__, 'posts_where' ), 10, 2 );
103 add_filter( 'get_pages', array( __CLASS__, 'get_pages' ), 1 );
104 if ( apply_filters( 'groups_filter_the_posts', false ) ) {
105 add_filter( 'the_posts', array( __CLASS__, 'the_posts' ), 1, 2 );
106 }
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 );
110 // content access
111 add_filter( 'get_the_excerpt', array( __CLASS__, 'get_the_excerpt' ), 1 );
112 add_filter( 'the_content', array( __CLASS__, 'the_content' ), 1 );
113 // edit & delete post
114 add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 10, 4 );
115
116 // These could be interesting to add later ...
117 // add_filter( "plugin_row_meta", array( __CLASS__, "plugin_row_meta" ), 1 );
118 // add_filter( "posts_join_paged", array( __CLASS__, "posts_join_paged" ), 1 );
119 // add_filter( "posts_where_paged", array( __CLASS__, "posts_where_paged" ), 1 );
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 );
154 }
155
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 /**
189 * Restrict access to edit or delete posts based on the post's access restrictions.
190 *
191 * @param array $caps
192 * @param string $cap
193 * @param int $user_id
194 * @param array $args
195 *
196 * @return array
197 */
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 );
203 if ( isset( $args[0] ) ) {
204 if ( strpos( $cap, 'edit_' ) === 0 || strpos( $cap, 'delete_' ) === 0 ) {
205 if ( $post_type = get_post_type( $args[0] ) ) {
206
207 $edit_post_type = 'edit_' . $post_type;
208 $delete_post_type = 'delete_' . $post_type;
209 if ( $post_type_object = get_post_type_object( $post_type ) ) {
210 if ( !isset( $post_type_object->capabilities ) ) {
211 $post_type_object->capabilities = array(); // @phpstan-ignore property.notFound
212 }
213 $caps_object = get_post_type_capabilities( $post_type_object );
214 if ( isset( $caps_object->edit_post ) ) {
215 $edit_post_type = $caps_object->edit_post;
216 }
217 if ( isset( $caps_object->delete_post ) ) {
218 $delete_post_type = $caps_object->delete_post;
219 }
220 }
221
222 if (
223 $cap === $edit_post_type ||
224 $cap === $delete_post_type ||
225 $cap === 'edit_post' ||
226 $cap === 'delete_post'
227 ) {
228 $post_id = null;
229 if ( is_numeric( $args[0] ) ) {
230 $post_id = $args[0];
231 } else if ( $args[0] instanceof WP_Post ) {
232 $post_id = $args[0]->ID;
233 }
234 if ( $post_id ) {
235 if ( !self::user_can_read_post( $post_id, $user_id ) ) {
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 }
249 }
250 }
251 }
252 }
253 }
254 }
255 add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 10, 4 );
256 return $caps;
257 }
258
259 /**
260 * Filters out posts that the user should not be able to access.
261 *
262 * @param string $where current where conditions
263 * @param WP_Query $query current query
264 *
265 * @return string modified $where
266 */
267 public static function posts_where( $where, $query ) {
268
269 global $wpdb;
270
271 if ( apply_filters( 'groups_post_access_posts_where_apply', true, $where, $query ) ) {
272
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 ) {
332 return $where;
333 }
334 }
335
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;
341 }
342 }
343 if ( count( $post_types ) == 0 ) {
344 return $where;
345 }
346 $post_types_in = "'" . implode( "','", array_map( 'esc_sql', $post_types ) ) . "'";
347
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 );
398 }
399
400 return apply_filters( 'groups_post_access_posts_where', $where, $query );
401 }
402
403 /**
404 * Filter pages by access capability.
405 *
406 * @param array $pages
407 *
408 * @return array
409 */
410 public static function get_pages( $pages ) {
411 $result = array();
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 }
418 }
419 } else {
420 $result = $pages;
421 }
422 return $result;
423 }
424
425 /**
426 * Filter posts by access capability.
427 *
428 * @param array $posts list of posts
429 * @param WP_Query $query
430 *
431 * @return array
432 */
433 public static function the_posts( $posts, &$query ) {
434 $result = array();
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 }
441 }
442 } else {
443 $result = $posts;
444 }
445 return $result;
446 }
447
448 /**
449 * Filter menu items by access capability.
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 *
455 * @param array $items
456 * @param mixed $menu
457 * @param array $args
458 *
459 * @return array
460 */
461 public static function wp_get_nav_menu_items( $items = null, $menu = null, $args = null ) {
462 $result = array();
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 }
474 }
475 } else {
476 $result = $items;
477 }
478 return $result;
479 }
480
481 /**
482 * Filter excerpt by access capability.
483 *
484 * @param string $output
485 *
486 * @return string $output if access granted, otherwise ''
487 */
488 public static function get_the_excerpt( $output ) {
489 global $post;
490 $result = '';
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
498 $result = $output;
499 }
500 } else {
501 $result = $output;
502 }
503 return $result;
504 }
505
506 /**
507 * Filter content by access capability.
508 *
509 * @param string $output
510 *
511 * @return string $output if access granted, otherwise ''
512 */
513 public static function the_content( $output ) {
514 global $post;
515 $result = '';
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
523 $result = $output;
524 }
525 } else {
526 $result = $output;
527 }
528 return $result;
529 }
530
531 /**
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 *
638 * (*) Revisions : As of Groups 1.3.13 and at WordPress 3.6.1, as
639 * add_post_meta stores postmeta for the revision's parent, we retrieve
640 * the parent's post ID if it applies and check against that to see if
641 * that capability is already present. This is to avoid duplicating
642 * the already existing postmeta entry (which ocurred in previous
643 * versions).
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
648 */
649 public static function create( $map ) {
650
651 $result = false;
652
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 );
663 }
664
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 ) ) {
669 if ( $revision_parent_id = wp_is_post_revision( $post_id ) ) {
670 $post_id = $revision_parent_id;
671 }
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 );
675 }
676 }
677 }
678 return $result;
679 }
680
681 /**
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
688 */
689 public static function read( $post_id, $map = array() ) {
690
691 $result = false;
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 }
712 }
713 return $result;
714 }
715
716 /**
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 *
721 * @param array $map
722 *
723 * @return array of group ids, false on failure
724 */
725 public static function update( $map ) {
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;
755 }
756
757 /**
758 * Removes access restrictions from a post.
759 *
760 * @param int $post_id
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
764 */
765 public static function delete( $post_id, $map = array() ) {
766
767 $result = false;
768
769 $groups_read = isset( $map['groups_read'] ) ? $map['groups_read'] : null;
770
771 if ( !empty( $post_id ) ) {
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 }
782 } else {
783 $result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ );
784 }
785 }
786 return $result;
787 }
788
789 /**
790 * Returns a list of capabilities that grant access to the post.
791 *
792 * @deprecated
793 *
794 * @param int $post_id
795 *
796 * @return array of string, capabilities
797 */
798 public static function get_read_post_capabilities( $post_id ) {
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 );
807 }
808
809 /**
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 *
833 * @param int $post_id post id
834 * @param int $user_id user id or null for current user
835 *
836 * @return boolean true if user can read the post
837 */
838 public static function user_can_read_post( $post_id, $user_id = null ) {
839
840 $result = false;
841
842 if ( !empty( $post_id ) ) {
843
844 if ( $user_id === null ) {
845 $user_id = get_current_user_id();
846 }
847
848 $cached = Groups_Cache::get( self::CAN_READ_POST . '_' . $user_id . '_' . $post_id, self::CACHE_GROUP );
849
850 if ( $cached !== null ) {
851 $result = $cached->get_value();
852 unset( $cached );
853 } else {
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 ) ) {
861 $result = true;
862 }
863 }
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 }
875 }
876 $result = apply_filters( 'groups_post_access_user_can_read_post', $result, $post_id, $user_id );
877 Groups_Cache::set( self::CAN_READ_POST . '_' . $user_id . '_' . $post_id, $result, self::CACHE_GROUP );
878 }
879 }
880 return $result;
881 }
882
883 /**
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
888 */
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 }
893 }
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 }
1338 }
1339 Groups_Post_Access::init();
1340