PluginProbe
Groups – Memberships and Access Control / 4.7.0
Groups – Memberships and Access Control v4.7.0
4.7.1 4.7.0 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 All 131 releases
← All changes | lib/access/class-groups-post-access.php +1067 -168 1.11.34.7.0 View file →
@@ -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();