PluginProbe
Groups – Memberships and Access Control / 4.7.1
Groups – Memberships and Access Control v4.7.1
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/views/class-groups-shortcodes.php +921 -127 1.11.14.7.1 View file →
@@ -22,8 +22,10 @@
22 22 if ( !defined( 'ABSPATH' ) ) {
23 23 exit;
24 24 }
25 25
26 +// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.WP.AlternativeFunctions.rand_rand, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
27 +
26 28 /**
27 29 * Shortcode handlers
28 30 */
29 31 class Groups_Shortcodes {
@@ -28,8 +30,51 @@
28 30 */
29 31 class Groups_Shortcodes {
30 32
31 33 /**
34 + * Maximum amount of time to accept hashes for join and leave requests.
35 + *
36 + * @var int
37 + */
38 + const MAX_TIME_DELTA = 3600;
39 +
40 + /**
41 + * Hashed content map.
42 + *
43 + * @since 3.11.0
44 + *
45 + * @var array
46 + */
47 + private static $map = array();
48 +
49 + /**
50 + * During preprocessing.
51 + *
52 + * @since 3.11.0
53 + *
54 + * @var boolean
55 + */
56 + private static $preprocessing = false;
57 +
58 + /**
59 + * Shortcode queue.
60 + *
61 + * @since 4.7.0
62 + *
63 + * @var array
64 + */
65 + private static $shortcode_queue = array();
66 +
67 + /**
68 + * Widgets contents.
69 + *
70 + * @since 4.7.1
71 + *
72 + * @var array
73 + */
74 + private static $widgets_contents = array();
75 +
76 + /**
32 77 * Adds shortcodes.
33 78 */
34 79 public static function init() {
35 80 // login
@@ -40,18 +85,27 @@
40 85 add_shortcode( 'groups_group_info', array( __CLASS__, 'groups_group_info' ) );
41 86 // user groups
42 87 add_shortcode( 'groups_user_groups', array( __CLASS__, 'groups_user_groups' ) );
43 88 // groups
44 - add_shortcode( 'groups_groups', array( __CLASS__, 'groups_groups' ) );
89 + add_shortcode( 'groups_groups', array( __CLASS__, 'groups_groups' ) );
45 90 // join a group
46 - add_shortcode( 'groups_join', array( __CLASS__, 'groups_join' ) );
91 + add_shortcode( 'groups_join', array( __CLASS__, 'groups_join' ) );
47 92 // leave a group
48 - add_shortcode( 'groups_leave', array( __CLASS__, 'groups_leave' ) );
93 + add_shortcode( 'groups_leave', array( __CLASS__, 'groups_leave' ) );
94 + // @since 3.11.0 content preprocessing
95 + add_filter( 'pre_render_block', array( __CLASS__, 'pre_render_block' ), 0, 3 );
96 + // @since 3.11.0 map processing
97 + add_filter( 'render_block', array( __CLASS__, 'render_block' ), 0, 3 );
98 + // @since 4.7.0 shortcode queue ops
99 + add_filter( 'pre_do_shortcode_tag', array( __CLASS__, 'pre_do_shortcode_tag' ), PHP_INT_MAX, 4 );
100 + add_filter( 'do_shortcode_tag', array( __CLASS__, 'do_shortcode_tag' ), PHP_INT_MAX, 4 );
101 + // @since 4.7.1 shortcodes in widgets
102 + add_filter( 'widget_display_callback', array( __CLASS__, 'widget_display_callback' ), PHP_INT_MAX, 3 );
49 103 }
50 104
51 105 /**
52 106 * Renders the Groups login form.
53 - *
107 + *
54 108 * The user is redirected to the current page after login by default.
55 109 * The user can be redirected to a specific URL after login by
56 110 * indicating the <code>redirect</code> attribute.
57 111 *
@@ -56,23 +110,27 @@
56 110 * indicating the <code>redirect</code> attribute.
57 111 *
58 112 * @param array $atts
59 113 * @param string $content
114 + *
60 115 * @return string the rendered form or empty
61 116 */
62 117 public static function groups_login( $atts, $content = null ) {
63 - $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
64 - extract(
65 - shortcode_atts(
66 - array(
67 - 'redirect' => $current_url,
68 - 'show_logout' => 'no'
69 - ),
70 - $atts
71 - )
118 +
119 + if ( !self::validate( 'groups_login', $atts, $content ) ) {
120 + return '';
121 + }
122 +
123 + $current_url = groups_get_current_url();
124 + $atts = shortcode_atts(
125 + array(
126 + 'redirect' => $current_url,
127 + 'show_logout' => 'no'
128 + ),
129 + $atts
72 130 );
73 - $redirect = trim( $redirect );
74 - $show_logout = trim( strtolower( $show_logout ) );
131 + $redirect = isset( $atts['redirect'] ) ? trim( $atts['redirect'] ) : $current_url;
132 + $show_logout = isset( $atts['show_logout'] ) ? trim( strtolower( $atts['show_logout'] ) ) : 'no';
75 133 $output = '';
76 134 if ( !is_user_logged_in() ) {
77 135 $output .= wp_login_form(
78 136 array(
@@ -88,14 +146,14 @@
88 146 )
89 147 );
90 148 }
91 149 }
92 - return $output;
150 + return $output; // nosemgrep audit.php.wp.security.sqli.shortcode-attr, audit.php.wp.security.xss.shortcode-attr
93 151 }
94 152
95 153 /**
96 154 * Renders the Groups logout link.
97 - *
155 + *
98 156 * The link is rendered if the user is logged in.
99 157 * The user is redirected to the current page after logout by default.
100 158 * The user can be redirected to a specific URL after logout by
101 159 * indicating the <code>redirect</code> attribute.
@@ -101,25 +159,29 @@
101 159 * indicating the <code>redirect</code> attribute.
102 160 *
103 161 * @param array $atts
104 162 * @param string $content not used
163 + *
105 164 * @return string logout link, is empty if not logged in
106 165 */
107 166 public static function groups_logout( $atts, $content = null ) {
108 - $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
109 - extract(
110 - shortcode_atts(
111 - array(
112 - 'redirect' => $current_url
113 - ),
114 - $atts
115 - )
167 +
168 + if ( !self::validate( 'groups_logout', $atts, $content ) ) {
169 + return '';
170 + }
171 +
172 + $current_url = groups_get_current_url();
173 + $atts = shortcode_atts(
174 + array(
175 + 'redirect' => $current_url
176 + ),
177 + $atts
116 178 );
117 - $redirect = trim( $redirect );
179 + $redirect = isset( $atts['redirect'] ) ? trim( $atts['redirect'] ) : $current_url;
118 180 $output = '';
119 181 if ( is_user_logged_in() ) {
120 182 $output .= sprintf( '<a href="%s">', esc_url( wp_logout_url( $redirect ) ) );
121 - $output .= __( 'Log out', GROUPS_PLUGIN_DOMAIN );
183 + $output .= esc_html__( 'Log out', 'groups' );
122 184 $output .= '</a>';
123 185 }
124 186 return $output;
125 187 }
@@ -125,8 +187,9 @@
125 187 }
126 188
127 189 /**
128 190 * Renders information about a group.
191 + *
129 192 * Attributes:
130 193 * - "group" : group name or id
131 194 * - "show" : what to show, can be "name", "description", "count"
132 195 * - "format" :
@@ -131,21 +194,29 @@
131 194 * - "show" : what to show, can be "name", "description", "count"
132 195 * - "format" :
133 196 * - "single" : used with show="count", single form, defaults to '1'
134 197 * - "plural" : used with show="count", plural form, defaults to '%d', must contain %d to show number
135 - *
198 + *
136 199 * @param array $atts attributes
137 200 * @param string $content content to render
138 - * @return rendered information
201 + *
202 + * @return string rendered information
139 203 */
140 204 public static function groups_group_info( $atts, $content = null ) {
205 +
141 206 global $wpdb;
142 - $output = "";
207 +
208 + if ( !self::validate( 'groups_group_info', $atts, $content ) ) {
209 + return '';
210 + }
211 +
212 + $output = '';
143 213 $options = shortcode_atts(
144 214 array(
145 - 'group' => '',
146 - 'show' => '',
215 + 'group' => '',
216 + 'show' => '',
147 217 'format' => '',
218 + 'none' => '0',
148 219 'single' => '1',
149 220 'plural' => '%d'
150 221 ),
151 222 $atts
@@ -155,9 +226,9 @@
155 226 if ( !$current_group ) {
156 227 $current_group = Groups_Group::read_by_name( $group );
157 228 }
158 229 if ( $current_group ) {
159 - switch( $options['show'] ) {
230 + switch ( $options['show'] ) {
160 231 case 'name' :
161 232 $output .= wp_filter_nohtml_kses( $current_group->name );
162 233 break;
163 234 case 'description' :
@@ -163,11 +234,11 @@
163 234 case 'description' :
164 235 $output .= wp_filter_nohtml_kses( $current_group->description );
165 236 break;
166 237 case 'count' :
167 - $user_group_table = _groups_get_tablename( "user_group" );
238 + $user_group_table = _groups_get_tablename( 'user_group' );
168 239 $count = $wpdb->get_var( $wpdb->prepare(
169 - "SELECT COUNT(*) FROM $user_group_table WHERE group_id = %d",
240 + "SELECT COUNT(*) FROM $user_group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
170 241 Groups_Utility::id( $current_group->group_id )
171 242 ) );
172 243 if ( $count === null ) {
173 244 $count = 0;
@@ -173,33 +244,44 @@
173 244 $count = 0;
174 245 } else {
175 246 $count = intval( $count );
176 247 }
177 - $output .= _n( $options['single'], sprintf( $options['plural'], $count ), $count, GROUPS_PLUGIN_DOMAIN );
248 + switch ( $count ) {
249 + case 0:
250 + $output .= wp_kses_post( $options['none'] );
251 + break;
252 + case 1:
253 + $output .= wp_kses_post( $options['single'] );
254 + break;
255 + default:
256 + $output .= wp_kses_post( sprintf( $options['plural'], $count ) );
257 + }
178 258 break;
179 - // @todo experimental - could use pagination, sorting, link to profile, ...
180 259 case 'users' :
181 - $user_group_table = _groups_get_tablename( "user_group" );
260 + // Renders a basic user list, do not extend. For more detailed information,
261 + // create a separate shortcode that could use pagination, sorting, link to profile, ...
262 + $user_group_table = _groups_get_tablename( 'user_group' );
182 263 $users = $wpdb->get_results( $wpdb->prepare(
183 - "SELECT * FROM $wpdb->users LEFT JOIN $user_group_table ON $wpdb->users.ID = $user_group_table.user_id WHERE $user_group_table.group_id = %d",
264 + "SELECT * FROM $wpdb->users LEFT JOIN $user_group_table ON $wpdb->users.ID = $user_group_table.user_id WHERE $user_group_table.group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
184 265 Groups_Utility::id( $current_group->group_id )
185 266 ) );
186 267 if ( $users ) {
187 268 $output .= '<ul>';
188 - foreach( $users as $user ) {
189 - $output .= '<li>' . wp_filter_nohtml_kses( $user->user_login ) . '</li>';
269 + foreach ( $users as $user ) {
270 + $display_name = !empty( $user->display_name ) ? $user->display_name : $user->user_login;
271 + $output .= '<li>' . wp_filter_nohtml_kses( $display_name ) . '</li>';
190 272 }
191 273 $output .= '</ul>';
192 274 }
193 -
194 275 break;
195 276 }
196 277 }
197 - return $output;
278 + return $output; // nosemgrep audit.php.wp.security.sqli.shortcode-attr, audit.php.wp.security.xss.shortcode-attr
198 279 }
199 280
200 281 /**
201 282 * Renders the current or a specific user's groups.
283 + *
202 284 * Attributes:
203 285 * - "user_id" OR "user_login" OR "user_email" to identify the user, if none given assumes the current user
204 286 * - "format" : one of "list" "div" "ul" or "ol" - "list" and "ul" are equivalent
205 287 * - "list_class" : defaults to "groups"
@@ -205,15 +287,21 @@
205 287 * - "list_class" : defaults to "groups"
206 288 * - "item_class" : defaults to "name"
207 289 * - "order_by" : defaults to "name", also accepts "group_id"
208 290 * - "order" : default to "ASC", also accepts "asc", "desc" and "DESC"
209 - *
291 + *
210 292 * @param array $atts attributes
211 293 * @param string $content not used
212 - * @return rendered groups for current user
294 + *
295 + * @return string rendered groups for current user
213 296 */
214 297 public static function groups_user_groups( $atts, $content = null ) {
215 - $output = "";
298 +
299 + if ( !self::validate( 'groups_user_groups', $atts, $content ) ) {
300 + return '';
301 + }
302 +
303 + $output = '';
216 304 $options = shortcode_atts(
217 305 array(
218 306 'user_id' => null,
219 307 'user_login' => null,
@@ -246,15 +334,15 @@
246 334 $user_id = get_current_user_id();
247 335 }
248 336 if ( $user_id !== null ) {
249 337 $user = new Groups_User( $user_id );
250 - $groups = $user->groups;
338 + $groups = $user->get_groups();
251 339
252 340 if ( !empty( $groups ) ) {
253 - // group attr
341 + // group attr
254 342 if ( $options['group'] !== null ) {
255 343 $groups = array();
256 - $groups_incl = explode( ",", $options['group'] );
344 + $groups_incl = explode( ',', $options['group'] );
257 345 foreach ( $groups_incl as $group_incl ) {
258 346 $group = trim( $group_incl );
259 347 $current_group = Groups_Group::read( $group );
260 348 if ( !$current_group ) {
@@ -260,9 +348,9 @@
260 348 if ( !$current_group ) {
261 349 $current_group = Groups_Group::read_by_name( $group );
262 350 }
263 351 if ( $current_group ) {
264 - if ( Groups_User_Group::read( $user_id, $current_group->group_id ) ) {
352 + if ( Groups_User::user_is_member( $user_id, $current_group->group_id ) ) {
265 353 $groups[] = $current_group;
266 354 }
267 355 }
268 356 }
@@ -268,9 +356,9 @@
268 356 }
269 357 }
270 358 // exclude_group attr
271 359 if ( $options['exclude_group'] !== null ) {
272 - $groups_excl = explode( ",", $options['exclude_group'] );
360 + $groups_excl = explode( ',', $options['exclude_group'] );
273 361 foreach ( $groups_excl as $key => $group_excl ) {
274 362 $group = trim( $group_excl );
275 363 $current_group = Groups_Group::read( $group );
276 364 if ( !$current_group ) {
@@ -287,9 +375,9 @@
287 375 unset( $groups[$key] );
288 376 }
289 377 }
290 378 }
291 - switch( $options['order_by'] ) {
379 + switch ( $options['order_by'] ) {
292 380 case 'group_id' :
293 381 usort( $groups, array( __CLASS__, 'sort_id' ) );
294 382 break;
295 383 default :
@@ -294,9 +382,9 @@
294 382 break;
295 383 default :
296 384 usort( $groups, array( __CLASS__, 'sort_name' ) );
297 385 }
298 - switch( $options['order'] ) {
386 + switch ( $options['order'] ) {
299 387 case 'desc' :
300 388 case 'DESC' :
301 389 $groups = array_reverse( $groups );
302 390 break;
@@ -301,9 +389,9 @@
301 389 $groups = array_reverse( $groups );
302 390 break;
303 391 }
304 392
305 - switch( $options['format'] ) {
393 + switch ( $options['format'] ) {
306 394 case 'list' :
307 395 case 'ul' :
308 396 $output .= '<ul class="' . esc_attr( $options['list_class'] ) . '">';
309 397 break;
@@ -312,20 +400,24 @@
312 400 break;
313 401 default :
314 402 $output .= '<div class="' . esc_attr( $options['list_class'] ) . '">';
315 403 }
316 - foreach( $groups as $group ) {
317 - switch( $options['format'] ) {
404 + foreach ( $groups as $group ) {
405 + switch ( $options['format'] ) {
318 406 case 'list' :
319 407 case 'ul' :
320 408 case 'ol' :
321 - $output .= '<li class="' . esc_attr( $options['item_class'] ) . '">' . $group->name . '</li>';
409 + // @todo mixed assignments done above, unify to Groups_Group objects only
410 + $name = $group instanceof Groups_Group ? $group->get_name() : $group->name;
411 + $output .= '<li class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $name ) ) . '</li>';
322 412 break;
323 413 default :
324 - $output .= '<div class="' . esc_attr( $options['item_class'] ) . '">' . $group->name . '</div>';
414 + // @todo mixed assignments done above, unify to Groups_Group objects only
415 + $name = $group instanceof Groups_Group ? $group->get_name() : $group->name;
416 + $output .= '<div class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $name ) ) . '</div>';
325 417 }
326 418 }
327 - switch( $options['format'] ) {
419 + switch ( $options['format'] ) {
328 420 case 'list' :
329 421 case 'ul' :
330 422 $output .= '</ul>';
331 423 break;
@@ -344,27 +436,30 @@
344 436 * Group comparison by group_id.
345 437 *
346 438 * @param Groups_Group $a
347 439 * @param Groups_Group $b
440 + *
348 441 * @return int
349 442 */
350 443 public static function sort_id( $a, $b ) {
351 - return $a->group_id - $b->group_id;
444 + return $a->get_id() - $b->get_id();
352 445 }
353 446
354 447 /**
355 448 * Group comparison by name.
356 - *
449 + *
357 450 * @param Groups_Group $a
358 451 * @param Groups_Group $b
452 + *
359 453 * @return int
360 454 */
361 455 public static function sort_name( $a, $b ) {
362 - return strcmp( $a->name, $b->name );
456 + return strcmp( $a->get_name(), $b->get_name() );
363 457 }
364 458
365 459 /**
366 460 * Renders a list of the site's groups.
461 + *
367 462 * Attributes:
368 463 * - "format" : one of "list" "div" "ul" or "ol" - "list" and "ul" are equivalent
369 464 * - "list_class" : defaults to "groups"
370 465 * - "item_class" : defaults to "name"
@@ -372,13 +467,20 @@
372 467 * - "order" : default to "ASC", also accepts "asc", "desc" and "DESC"
373 468 *
374 469 * @param array $atts attributes
375 470 * @param string $content not used
376 - * @return rendered groups
471 + *
472 + * @return string rendered groups
377 473 */
378 474 public static function groups_groups( $atts, $content = null ) {
475 +
379 476 global $wpdb;
380 - $output = "";
477 +
478 + if ( !self::validate( 'groups_groups', $atts, $content ) ) {
479 + return '';
480 + }
481 +
482 + $output = '';
381 483 $options = shortcode_atts(
382 484 array(
383 485 'format' => 'list',
384 486 'list_class' => 'groups',
@@ -387,9 +489,9 @@
387 489 'order' => 'ASC'
388 490 ),
389 491 $atts
390 492 );
391 - switch( $options['order_by'] ) {
493 + switch ( $options['order_by'] ) {
392 494 case 'group_id' :
393 495 case 'name' :
394 496 $order_by = $options['order_by'];
395 497 break;
@@ -395,9 +497,9 @@
395 497 break;
396 498 default :
397 499 $order_by = 'name';
398 500 }
399 - switch( $options['order'] ) {
501 + switch ( $options['order'] ) {
400 502 case 'asc' :
401 503 case 'ASC' :
402 504 case 'desc' :
403 505 case 'DESC' :
@@ -405,13 +507,13 @@
405 507 break;
406 508 default :
407 509 $order = 'ASC';
408 510 }
409 - $group_table = _groups_get_tablename( "group" );
410 - if ( $groups = $wpdb->get_results(
411 - "SELECT group_id FROM $group_table ORDER BY $order_by $order"
412 - ) ) {
413 - switch( $options['format'] ) {
511 + $group_table = _groups_get_tablename( 'group' );
512 + // nosemgrep: audit.php.wp.security.sqli.shortcode-attr
513 + $groups = $wpdb->get_results( "SELECT group_id FROM $group_table ORDER BY $order_by $order" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
514 + if ( is_array( $groups ) && count( $groups ) > 0 ) {
515 + switch ( $options['format'] ) {
414 516 case 'list' :
415 517 case 'ul' :
416 518 $output .= '<ul class="' . esc_attr( $options['list_class'] ) . '">';
417 519 break;
@@ -420,21 +522,21 @@
420 522 break;
421 523 default :
422 524 $output .= '<div class="' . esc_attr( $options['list_class'] ) . '">';
423 525 }
424 - foreach( $groups as $group ) {
526 + foreach ( $groups as $group ) {
425 527 $group = new Groups_Group( $group->group_id );
426 - switch( $options['format'] ) {
528 + switch ( $options['format'] ) {
427 529 case 'list' :
428 530 case 'ul' :
429 531 case 'ol' :
430 - $output .= '<li class="' . esc_attr( $options['item_class'] ) . '">' . $group->name . '</li>';
532 + $output .= '<li class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $group->get_name() ) ) . '</li>';
431 533 break;
432 534 default :
433 - $output .= '<div class="' . esc_attr( $options['item_class'] ) . '">' . $group->name . '</div>';
535 + $output .= '<div class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $group->get_name() ) ) . '</div>';
434 536 }
435 537 }
436 - switch( $options['format'] ) {
538 + switch ( $options['format'] ) {
437 539 case 'list' :
438 540 case 'ul' :
439 541 $output .= '</ul>';
440 542 break;
@@ -449,159 +551,851 @@
449 551 }
450 552
451 553 /**
452 554 * Renders a form that lets a user join a group.
453 - * * Attributes:
555 + *
556 + * Attributes:
557 + *
454 558 * - "group" : (required) group name or id
455 - *
559 + * - "class" : (optional) container class to add
560 + * - "display_message" : (optional) whether to display the message
561 + * - "display_is_member" : (optional) whether to display the message that a user is a member
562 + * - "redirect" : (optional) whether to redirect after accepted submission
563 + * - "submit_class" : (optional) submit HTML element class to add
564 + * - "submit_text" : (optional) submit HTML element text to use
565 + *
456 566 * @param array $atts attributes
457 567 * @param string $content not used
568 + *
569 + * @return string
458 570 */
459 571 public static function groups_join( $atts, $content = null ) {
572 +
573 + global $groups_join_data_init, $post;
574 +
575 + if ( !self::validate( 'groups_join', $atts, $content ) ) {
576 + return '';
577 + }
578 +
460 579 $nonce_action = 'groups_action';
461 580 $nonce = 'nonce_join';
462 - $output = "";
581 + $output = '';
463 582
464 583 $options = shortcode_atts(
465 584 array(
585 + 'class' => '',
466 586 'group' => '',
467 587 'display_message' => true,
468 588 'display_is_member' => false,
469 - 'submit_text' => __( 'Join the %s group', GROUPS_PLUGIN_DOMAIN )
589 + 'redirect' => true,
590 + 'submit_class' => '',
591 + /* translators: group name */
592 + 'submit_text' => esc_html__( 'Join the %s group', 'groups' )
470 593 ),
471 594 $atts
472 595 );
473 - extract( $options );
474 596
475 - if ( $display_message === 'false' ) {
476 - $display_message = false;
597 + $display_message = is_string( $options['display_message'] ) ? strtolower( $options['display_message'] ) : $options['display_message'];
598 + $display_is_member = is_string( $options['display_is_member'] ) ? strtolower( $options['display_is_member'] ) : $options['display_is_member'];
599 + $redirect = is_string( $options['redirect'] ) ? strtolower( $options['redirect'] ) : $options['redirect'];
600 + $submit_text = $options['submit_text'];
601 + $display_message = in_array( $display_message, array( 'true', 'yes', true ) );
602 + $display_is_member = in_array( $display_is_member, array( 'true', 'yes', true ) );
603 +
604 + if ( !is_bool( $redirect ) ) {
605 + switch ( $redirect ) {
606 + case 'true':
607 + case 'yes':
608 + $redirect = true;
609 + break;
610 + case 'false':
611 + case 'no':
612 + $redirect = false;
613 + break;
614 + default:
615 + if ( is_string( $redirect ) ) {
616 + $redirect = trim( $redirect );
617 + if ( strlen( $redirect ) === 0 ) {
618 + $redirect = true;
619 + }
620 + } else {
621 + $redirect = true;
622 + }
623 + }
477 624 }
478 - if ( $display_is_member === 'true' ) {
479 - $display_is_member = true;
480 - }
481 625
626 + $class = trim( $options['class'] );
627 + $submit_class = trim( $options['submit_class'] );
482 628 $group = trim( $options['group'] );
483 629 $current_group = Groups_Group::read( $group );
484 630 if ( !$current_group ) {
485 631 $current_group = Groups_Group::read_by_name( $group );
486 632 }
633 + // bail out if no valid group
634 + if ( !$current_group ) {
635 + return '';
636 + }
637 +
638 + // @since 3.11.0 Restrict the functionality to authors with appropriate permission
639 + $author_can_restrict_group_ids = array();
640 + $author_id = isset( $post ) && !empty( $post->post_author ) ? $post->post_author : get_the_author_meta( 'ID' );
641 + $author_id = is_numeric( $author_id ) ? intval( $author_id ) : null;
642 + if ( $author_id !== null ) {
643 + $author = new Groups_User( $author_id );
644 + if ( $author->can( GROUPS_RESTRICT_ACCESS ) ) {
645 + if ( $author->can( GROUPS_ADMINISTER_GROUPS ) ) {
646 + $author_can_restrict_group_ids = Groups_Group::get_group_ids();
647 + } else {
648 + $author_can_restrict_group_ids = $author->get_group_ids_deep();
649 + }
650 + }
651 + }
652 + if ( !in_array( $current_group->group_id, $author_can_restrict_group_ids ) ) {
653 + return '';
654 + }
655 +
487 656 if ( $current_group ) {
488 657 if ( $user_id = get_current_user_id() ) {
658 + $joined = false;
489 659 $submitted = false;
490 660 $invalid_nonce = false;
491 - if ( !empty( $_POST['groups_action'] ) && $_POST['groups_action'] == 'join' ) {
661 + if ( groups_sanitize_post( 'groups_action' ) === 'join' ) {
492 662 $submitted = true;
493 - if ( !wp_verify_nonce( $_POST[$nonce], $nonce_action ) ) {
663 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
664 + if ( !groups_verify_post_nonce( $nonce, $nonce_action ) ) { // nosemgrep: scanner.php.wp.security.csrf.nonce-check-not-dying
494 665 $invalid_nonce = true;
495 666 }
496 667 }
497 668 if ( $submitted && !$invalid_nonce ) {
498 669 // add user to group
499 - if ( isset( $_POST['group_id'] ) ) {
500 - $join_group = Groups_Group::read( $_POST['group_id'] );
501 - Groups_User_Group::create(
502 - array(
503 - 'group_id' => $join_group->group_id,
504 - 'user_id' => $user_id
505 - )
506 - );
670 + if ( isset( $_POST['groups-join-data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
671 + $hash = trim( groups_sanitize_post( 'groups-join-data' ) );
672 + $groups_join_data = get_user_meta( $user_id, 'groups-join-data', true );
673 + if ( is_array( $groups_join_data ) && isset( $groups_join_data[$hash] ) ) {
674 + if ( isset( $groups_join_data[$hash]['group_id'] ) && isset( $groups_join_data[$hash]['time'] ) ) {
675 + $group_id = $groups_join_data[$hash]['group_id'];
676 + $dt = time() - $groups_join_data[$hash]['time'];
677 + if ( $dt < apply_filters( 'groups_join_submit_max_time_delta', self::MAX_TIME_DELTA ) ) {
678 + $joined = Groups_User_Group::create(
679 + array(
680 + 'group_id' => $group_id,
681 + 'user_id' => $user_id
682 + )
683 + );
684 + if ( $joined ) {
685 + /**
686 + * Whether to redirect after submit and successful addition to group.
687 + *
688 + * @since 3.6.0
689 + *
690 + * @param boolean|string $redirect true to redirect to current ULR, string to redirect to specific URL, false not to redirect
691 + * @param array $atts shortcode attributes
692 + * @param array $options evaluated shortcode options
693 + *
694 + * @return boolean|string
695 + */
696 + if ( apply_filters( 'groups_join_submit_redirect', $redirect, $atts, $options ) !== false ) {
697 + self::maybe_redirect( $redirect );
698 + }
699 + }
700 + }
701 + }
702 + }
507 703 }
508 704 }
509 - if ( !Groups_User_Group::read( $user_id, $current_group->group_id ) ) {
510 - $submit_text = sprintf( $options['submit_text'], wp_filter_nohtml_kses( $current_group->name ) );
511 - $output .= '<div class="groups-join">';
705 + if ( !Groups_User::user_is_member( $user_id, $current_group->group_id ) ) {
706 + if ( !isset( $groups_join_data_init ) ) {
707 + $groups_join_data_init = true;
708 + delete_user_meta( $user_id, 'groups-join-data' );
709 + }
710 + $data = array(
711 + 'user_id' => $user_id,
712 + 'group_id' => $current_group->group_id,
713 + 'time' => time(),
714 + 'salt' => rand( 0, PHP_INT_MAX )
715 + );
716 + $hash = hash( 'sha256', json_encode( $data ) );
717 + $groups_join_data = get_user_meta( $user_id, 'groups-join-data', true );
718 + if ( !is_array( $groups_join_data ) ) {
719 + $groups_join_data = array();
720 + }
721 + $groups_join_data[$hash] = $data;
722 + update_user_meta( $user_id, 'groups-join-data', $groups_join_data );
723 +
724 + $submit_text = sprintf( $options['submit_text'], stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
725 + $output .= sprintf(
726 + '<div class="groups-join%s">',
727 + strlen( $class ) > 0 ? ' ' . esc_attr( $class ) : '',
728 + );
512 729 $output .= '<form action="#" method="post">';
513 730 $output .= '<input type="hidden" name="groups_action" value="join" />';
514 - $output .= '<input type="hidden" name="group_id" value="' . esc_attr( $current_group->group_id ) . '" />';
515 - $output .= '<input type="submit" value="' . $submit_text . '" />';
516 - $output .= wp_nonce_field( $nonce_action, $nonce, true, false );
731 + $output .= '<input type="hidden" name="groups-join-data" value="' . esc_attr( $hash ) . '" />';
732 + $output .= sprintf(
733 + '<input class="groups-join-submit%s" type="submit" value="%s" />',
734 + strlen( $submit_class ) > 0 ? ' ' . esc_attr( $submit_class ) : '',
735 + esc_attr( $submit_text )
736 + );
737 + $output .= wp_nonce_field( $nonce_action, $nonce, true, false );
517 738 $output .= '</form>';
518 739 $output .= '</div>';
519 740 } else if ( $display_message ) {
520 - if ( $submitted && !$invalid_nonce && isset( $join_group ) && $join_group->group_id === $current_group->group_id ) {
741 + if ( $joined ) {
521 742 $output .= '<div class="groups-join joined">';
522 - $output .= sprintf( __( 'You have joined the %s group.', GROUPS_PLUGIN_DOMAIN ), wp_filter_nohtml_kses( $join_group->name ) );
743 + /* translators: group name */
744 + $output .= sprintf( esc_html__( 'You have joined the %s group.', 'groups' ), stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
523 745 $output .= '</div>';
524 - }
525 - else if ( $display_is_member && isset( $current_group ) && $current_group !== false ) {
746 + } else if ( $display_is_member && $current_group !== false ) {
526 747 $output .= '<div class="groups-join member">';
527 - $output .= sprintf( __( 'You are a member of the %s group.', GROUPS_PLUGIN_DOMAIN ), wp_filter_nohtml_kses( $current_group->name ) );
748 + /* translators: group name */
749 + $output .= sprintf( esc_html__( 'You are a member of the %s group.', 'groups' ), stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
528 750 $output .= '</div>';
529 751 }
530 752 }
531 753 }
532 754 }
755 +
756 + if ( self::$preprocessing ) {
757 + // surround content with hashmarks
758 + // <!-- groups:{hash} -->{content}<!-- /groups:{hash} -->
759 + $hash = md5( $output );
760 + $prefix = sprintf( '<!-- groups:%s -->', $hash );
761 + $suffix = sprintf( '<!-- /groups:%s -->', $hash );
762 + self::$map[$hash] = array(
763 + 'prefix' => $prefix,
764 + 'suffix' => $suffix,
765 + 'content' => $output
766 + );
767 +
768 + $output = sprintf(
769 + '%s%s%s',
770 + $prefix,
771 + $output,
772 + $suffix
773 + );
774 + }
775 +
533 776 return $output;
534 777 }
535 778
536 779 /**
537 780 * Renders a form that lets a user leave a group.
538 - * * Attributes:
781 + *
782 + * Attributes:
783 + *
539 784 * - "group" : (required) group name or id
785 + * - "class" : (optional) container class to add
786 + * - "display_message" : (optional) whether to display the message
787 + * - "redirect" : (optional) whether to redirect after accepted submission
788 + * - "submit_class" : (optional) submit HTML element class to add
789 + * - "submit_text" : (optional) submit HTML element text to use
540 790 *
541 791 * @param array $atts attributes
542 792 * @param string $content not used
793 + *
794 + * @return string
543 795 */
544 796 public static function groups_leave( $atts, $content = null ) {
797 +
798 + global $groups_leave_data_init, $post;
799 +
800 + if ( !self::validate( 'groups_leave', $atts, $content ) ) {
801 + return '';
802 + }
803 +
545 804 $nonce_action = 'groups_action';
546 805 $nonce = 'nonce_leave';
547 - $output = "";
806 + $output = '';
548 807
549 808 $options = shortcode_atts(
550 809 array(
810 + 'class' => '',
551 811 'group' => '',
552 812 'display_message' => true,
553 - 'submit_text' => __( 'Leave the %s group', GROUPS_PLUGIN_DOMAIN ),
813 + 'redirect' => true,
814 + 'submit_class' => '',
815 + /* translators: group name */
816 + 'submit_text' => esc_html__( 'Leave the %s group', 'groups' ),
554 817 ),
555 818 $atts
556 819 );
557 - extract( $options );
558 820
559 - if ( $display_message === 'false' ) {
560 - $display_message = false;
821 + $display_message = is_string( $options['display_message'] ) ? strtolower( $options['display_message'] ) : $options['display_message'];
822 + $redirect = is_string( $options['redirect'] ) ? strtolower( $options['redirect'] ) : $options['redirect'];
823 + $submit_text = $options['submit_text'];
824 +
825 + $display_message = in_array( $display_message, array( 'true', 'yes', true ) );
826 +
827 + if ( !is_bool( $redirect ) ) {
828 + switch ( $redirect ) {
829 + case 'true':
830 + case 'yes':
831 + $redirect = true;
832 + break;
833 + case 'false':
834 + case 'no':
835 + $redirect = false;
836 + break;
837 + default:
838 + if ( is_string( $redirect ) ) {
839 + $redirect = trim( $redirect );
840 + if ( strlen( $redirect ) === 0 ) {
841 + $redirect = true;
842 + }
843 + } else {
844 + $redirect = true;
845 + }
846 + }
561 847 }
562 848
849 + $class = trim( $options['class'] );
850 + $submit_class = trim( $options['submit_class'] );
563 851 $group = trim( $options['group'] );
564 852 $current_group = Groups_Group::read( $group );
565 853 if ( !$current_group ) {
566 854 $current_group = Groups_Group::read_by_name( $group );
567 855 }
856 + // bail out if no valid group
857 + if ( !$current_group ) {
858 + return '';
859 + }
860 +
861 + // @since 3.11.0 Restrict the functionality to authors with appropriate permission
862 + $author_can_restrict_group_ids = array();
863 + $author_id = isset( $post ) && !empty( $post->post_author ) ? $post->post_author : get_the_author_meta( 'ID' );
864 + $author_id = is_numeric( $author_id ) ? intval( $author_id ) : null;
865 + if ( $author_id !== null ) {
866 + $author = new Groups_User( $author_id );
867 + if ( $author->can( GROUPS_RESTRICT_ACCESS ) ) {
868 + if ( $author->can( GROUPS_ADMINISTER_GROUPS ) ) {
869 + $author_can_restrict_group_ids = Groups_Group::get_group_ids();
870 + } else {
871 + $author_can_restrict_group_ids = $author->get_group_ids_deep();
872 + }
873 + }
874 + }
875 + if ( !in_array( $current_group->group_id, $author_can_restrict_group_ids ) ) {
876 + return '';
877 + }
878 +
568 879 if ( $current_group ) {
569 880 if ( $user_id = get_current_user_id() ) {
881 + $left = false;
570 882 $submitted = false;
571 883 $invalid_nonce = false;
572 - if ( !empty( $_POST['groups_action'] ) && $_POST['groups_action'] == 'leave' ) {
884 + if ( groups_sanitize_post( 'groups_action' ) === 'leave' ) {
573 885 $submitted = true;
574 - if ( !wp_verify_nonce( $_POST[$nonce], $nonce_action ) ) {
886 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
887 + if ( !groups_verify_post_nonce( $nonce, $nonce_action ) ) { // nosemgrep: scanner.php.wp.security.csrf.nonce-check-not-dying
575 888 $invalid_nonce = true;
576 889 }
577 890 }
578 891 if ( $submitted && !$invalid_nonce ) {
579 892 // remove user from group
580 - if ( isset( $_POST['group_id'] ) ) {
581 - $leave_group = Groups_Group::read( $_POST['group_id'] );
582 - Groups_User_Group::delete( $user_id, $leave_group->group_id );
893 + if ( isset( $_POST['groups-leave-data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
894 + $hash = trim( groups_sanitize_post( 'groups-leave-data' ) );
895 + $groups_leave_data = get_user_meta( $user_id, 'groups-leave-data', true );
896 + if ( is_array( $groups_leave_data ) && isset( $groups_leave_data[$hash] ) ) {
897 + if ( isset( $groups_leave_data[$hash]['group_id'] ) && isset( $groups_leave_data[$hash]['time'] ) ) {
898 + $group_id = $groups_leave_data[$hash]['group_id'];
899 + $dt = time() - $groups_leave_data[$hash]['time'];
900 + if ( $dt < apply_filters( 'groups_leave_submit_max_time_delta', self::MAX_TIME_DELTA ) ) {
901 + $left = Groups_User_Group::delete( $user_id, $group_id );
902 + if ( $left ) {
903 + /**
904 + * Whether to redirect after acceptedsubmit and successful removal from group.
905 + *
906 + * @since 3.6.0
907 + *
908 + * @param boolean|string $redirect true to redirect to current ULR, string to redirect to specific URL, false not to redirect
909 + * @param array $atts shortcode attributes
910 + * @param array $options evaluated shortcode options
911 + *
912 + * @return boolean|string
913 + */
914 + if ( apply_filters( 'groups_leave_submit_redirect', $redirect, $atts, $options ) !== false ) {
915 + self::maybe_redirect( $redirect );
916 + }
917 + }
918 + }
919 + }
920 + }
583 921 }
584 922 }
585 - if ( Groups_User_Group::read( $user_id, $current_group->group_id ) ) {
586 - $submit_text = sprintf( $options['submit_text'], wp_filter_nohtml_kses( $current_group->name ) );
587 - $output .= '<div class="groups-join">';
923 + if ( Groups_User::user_is_member( $user_id, $current_group->group_id ) ) {
924 + if ( !isset( $groups_leave_data_init ) ) {
925 + $groups_leave_data_init = true;
926 + delete_user_meta( $user_id, 'groups-leave-data' );
927 + }
928 + $data = array(
929 + 'user_id' => $user_id,
930 + 'group_id' => $current_group->group_id,
931 + 'time' => time(),
932 + 'salt' => rand( 0, PHP_INT_MAX )
933 + );
934 + $hash = hash( 'sha256', json_encode( $data ) );
935 + $groups_leave_data = get_user_meta( $user_id, 'groups-leave-data', true );
936 + if ( !is_array( $groups_leave_data ) ) {
937 + $groups_leave_data = array();
938 + }
939 + $groups_leave_data[$hash] = $data;
940 + update_user_meta( $user_id, 'groups-leave-data', $groups_leave_data );
941 +
942 + $submit_text = sprintf( $options['submit_text'], stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
943 + $output .= sprintf(
944 + '<div class="groups-leave%s">',
945 + strlen( $class ) > 0 ? ' ' . esc_attr( $class ) : '',
946 + );
588 947 $output .= '<form action="#" method="post">';
589 948 $output .= '<input type="hidden" name="groups_action" value="leave" />';
590 - $output .= '<input type="hidden" name="group_id" value="' . esc_attr( $current_group->group_id ) . '" />';
591 - $output .= '<input type="submit" value="' . $submit_text . '" />';
592 - $output .= wp_nonce_field( $nonce_action, $nonce, true, false );
949 + $output .= '<input type="hidden" name="groups-leave-data" value="' . esc_attr( $hash ) . '" />';
950 + $output .= sprintf(
951 + '<input class="groups-leave-submit%s" type="submit" value="%s" />',
952 + strlen( $submit_class ) > 0 ? ' ' . esc_attr( $submit_class ) : '',
953 + esc_attr( $submit_text )
954 + );
955 + $output .= wp_nonce_field( $nonce_action, $nonce, true, false );
593 956 $output .= '</form>';
594 957 $output .= '</div>';
595 958 } else if ( $display_message ) {
596 - if ( $submitted && !$invalid_nonce && isset( $leave_group ) && $leave_group->group_id === $current_group->group_id ) {
597 - $output .= '<div class="groups-join left">';
598 - $output .= sprintf( __( 'You have left the %s group.', GROUPS_PLUGIN_DOMAIN ), wp_filter_nohtml_kses( $leave_group->name ) );
959 + if ( $left ) {
960 + $output .= '<div class="groups-leave left">';
961 + /* translators: group name */
962 + $output .= sprintf( esc_html__( 'You have left the %s group.', 'groups' ), stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
599 963 $output .= '</div>';
600 964 }
601 965 }
602 966 }
603 967 }
968 +
969 + if ( self::$preprocessing ) {
970 + // surround content with hashmarks
971 + // <!-- groups:{hash} -->{content}<!-- /groups:{hash} -->
972 + $hash = md5( $output );
973 + $prefix = sprintf( '<!-- groups:%s -->', $hash );
974 + $suffix = sprintf( '<!-- /groups:%s -->', $hash );
975 + self::$map[$hash] = array(
976 + 'prefix' => $prefix,
977 + 'suffix' => $suffix,
978 + 'content' => $output
979 + );
980 +
981 + $output = sprintf(
982 + '%s%s%s',
983 + $prefix,
984 + $output,
985 + $suffix
986 + );
987 + }
988 +
604 989 return $output;
605 990 }
991 +
992 + /**
993 + * Try to redirect.
994 + *
995 + * No redirect will happen if $redirect is false.
996 + *
997 + * A redirect to the current URL is attempted if $redirect is an empty string.
998 + *
999 + * Relative paths will try to redirect to the path off the home URL and other URL components present.
1000 + *
1001 + * @since 3.6.0
1002 + *
1003 + * @param boolean|string $redirect
1004 + */
1005 + private static function maybe_redirect( $redirect ) {
1006 +
1007 + // Don't redirect
1008 + if ( is_bool( $redirect ) && !$redirect ) {
1009 + return;
1010 + }
1011 +
1012 + // Use the current URL if no specific URL is provided
1013 + if ( is_string( $redirect ) && trim( $redirect ) !== '' ) {
1014 + $redirect_url = trim( $redirect );
1015 + } else {
1016 + $redirect_url = groups_get_current_url();
1017 + }
1018 +
1019 + // Try to handle a relative URL, determine missing parts
1020 + $parts = wp_parse_url( $redirect_url );
1021 + if ( !isset( $parts['scheme'] ) ) {
1022 + $parts['scheme'] = is_ssl() ? 'https' : 'http';
1023 + }
1024 + if ( !isset( $parts['host'] ) ) {
1025 + $parts['host'] = wp_parse_url( home_url(), PHP_URL_HOST );
1026 + }
1027 + if ( !isset( $parts['path'] ) ) {
1028 + $parts['path'] = wp_parse_url( home_url(), PHP_URL_PATH );
1029 + } else {
1030 + $home_path = wp_parse_url( home_url(), PHP_URL_PATH );
1031 + if ( strpos( $parts['path'], $home_path ) !== 0 ) {
1032 + $parts['path'] = trailingslashit( $home_path ) . ltrim( $parts['path'], '/\\' );
1033 + }
1034 + }
1035 + // Put the absolute URL together
1036 + $url = $parts['scheme'] . ':';
1037 + if ( !empty( $parts['user'] ) && !empty( $parts['password'] ) ) {
1038 + $url .= $parts['user'] . ':' . $parts['password'] . '@';
1039 + }
1040 + $url .= '//' . $parts['host'];
1041 + if ( !empty( $parts['path'] ) ) {
1042 + $url .= $parts['path'];
1043 + }
1044 + if ( !empty( $parts['query'] ) ) {
1045 + $url .= '?' . $parts['query'];
1046 + }
1047 + if ( !empty( $parts['fragment'] ) ) {
1048 + $url .= '#' . $parts['fragment'];
1049 + }
1050 + $redirect_url = $url;
1051 +
1052 + // validate the URL and restrict to allowed hosts, uses the allowed_redirect_hosts filter restricting to the domain of the current site
1053 + $redirect_url = wp_validate_redirect( $redirect_url ); // default fallback is ''
1054 + if ( is_string( $redirect_url ) ) {
1055 + $redirect_url = trim( $redirect_url );
1056 + }
1057 + if ( $redirect_url !== null && $redirect_url !== false && $redirect_url !== '' ) {
1058 + if ( wp_redirect( $redirect_url ) ) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
1059 + exit;
1060 + }
1061 + }
1062 + }
1063 +
1064 + /**
1065 + * Determine which blocks to preprocess.
1066 + *
1067 + * @since 3.11.0
1068 + *
1069 + * @return array
1070 + */
1071 + public static function get_preprocess_blocks() {
1072 + $blocks = apply_filters(
1073 + 'groups_shortcodes_preprocess_blocks',
1074 + array(
1075 + 'core/latest-posts'
1076 + )
1077 + );
1078 + if ( !is_array( $blocks ) ) {
1079 + $blocks = array();
1080 + }
1081 + return $blocks;
1082 + }
1083 +
1084 + /**
1085 + * Content preprocessing.
1086 + *
1087 + * @since 3.11.0
1088 + *
1089 + * @param string|null $pre_render
1090 + * @param array $parsed_block
1091 + * @param WP_Block|null $parent_block
1092 + *
1093 + * @return string|null
1094 + */
1095 + public static function pre_render_block( $pre_render, $parsed_block, $parent_block ) {
1096 + if ( in_array( $parsed_block['blockName'], self::get_preprocess_blocks() ) ) {
1097 + // start preprocessing
1098 + self::$preprocessing = true;
1099 + add_filter( 'the_posts', array( __CLASS__, 'preprocess_the_posts' ), 10, 2 );
1100 + }
1101 + return $pre_render;
1102 + }
1103 +
1104 + /**
1105 + * Map processing.
1106 + *
1107 + * @since 3.11.0
1108 + *
1109 + * @param string $block_content
1110 + * @param array $parsed_block
1111 + * @param WP_Block $block
1112 + *
1113 + * @return string
1114 + */
1115 + public static function render_block( $block_content, $parsed_block, $block ) {
1116 + // Remove hashmarks leaving the content within.
1117 + if ( in_array( $parsed_block['blockName'], self::get_preprocess_blocks() ) ) {
1118 + // stop preprocessing
1119 + remove_filter( 'the_posts', array( __CLASS__, 'preprocess_the_posts' ), 10 );
1120 + self::$preprocessing = false;
1121 + foreach ( self::$map as $hash => $data ) {
1122 + $prefix = $data['prefix'] ?? '';
1123 + $suffix = $data['suffix'] ?? '';
1124 + $content = $data['content'] ?? '';
1125 + $start = $prefix !== '' ? strpos( $block_content, $prefix ) : false;
1126 + $end = $suffix !== '' ? strpos( $block_content, $suffix ) : false;
1127 + if ( $start !== false && $end !== false ) {
1128 + $block_content = substr( $block_content, 0, $start ) . $content . substr( $block_content, $end + strlen( $suffix ) );
1129 + }
1130 + }
1131 + }
1132 + return $block_content;
1133 + }
1134 +
1135 + /**
1136 + * Preprocess posts.
1137 + *
1138 + * @since 3.11.0
1139 + *
1140 + * @param WP_Post[] $posts
1141 + * @param WP_Query $query
1142 + *
1143 + * @return WP_Post[]
1144 + */
1145 + public static function preprocess_the_posts( $posts, $query ) {
1146 + global $shortcode_tags, $post;
1147 + if ( !empty( $shortcode_tags ) ) {
1148 + // remember the global post object
1149 + $original_post = $post;
1150 + // remember the global registered shortcodes
1151 + $original_shortcode_tags = $shortcode_tags;
1152 + // limit processing to these shortcodes
1153 + $do_shortcode_tags = array();
1154 + if ( isset( $shortcode_tags['groups_join'] ) ) {
1155 + $do_shortcode_tags['groups_join'] = $shortcode_tags['groups_join'];
1156 + }
1157 + if ( isset( $shortcode_tags['groups_leave'] ) ) {
1158 + $do_shortcode_tags['groups_leave'] = $shortcode_tags['groups_leave'];
1159 + }
1160 + $shortcode_tags = $do_shortcode_tags;
1161 + // preprocess content for each post
1162 + $processed_posts = array();
1163 + while ( !empty( $posts ) ) {
1164 + // set the global $post to process within do_shortcode()
1165 + $post = array_shift( $posts );
1166 + $post->post_excerpt = do_shortcode( $post->post_excerpt );
1167 + $post->post_content = do_shortcode( $post->post_content );
1168 + array_push( $processed_posts, $post );
1169 + }
1170 + // modified posts to return
1171 + $posts = $processed_posts;
1172 + // restore the global registered shortcodes
1173 + $shortcode_tags = $original_shortcode_tags;
1174 + // restore the global post
1175 + $post = $original_post;
1176 + }
1177 + return $posts;
1178 + }
1179 +
1180 + /**
1181 + * Validate shortcode.
1182 + *
1183 + * @since 4.7.0
1184 + *
1185 + * @see do_shortcode()
1186 + * @see do_shortcode_tag()
1187 + *
1188 + * @param string $tag shortcode tag
1189 + * @param array $atts shortcode attributes
1190 + * @param string $content shortcode content
1191 + *
1192 + * @return boolean
1193 + */
1194 + public static function validate( $tag, $atts, $content ) {
1195 +
1196 + global $post;
1197 +
1198 + // allow direct calls to shortcode processing functions
1199 + if ( !self::is_processing( $tag, $atts ) ) {
1200 + return true;
1201 + }
1202 +
1203 + $valid =
1204 + isset( $post ) && !empty( $post->ID ) && ( !empty( $post->post_excerpt ) || !empty( $post->post_content ) ) ||
1205 + !empty( self::$widgets_contents );
1206 +
1207 + if ( $valid ) {
1208 + $valid = ! (
1209 + defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ||
1210 + defined( 'REST_REQUEST' ) && REST_REQUEST ||
1211 + defined( 'WP_CLI' ) && WP_CLI ||
1212 + defined( 'WPCOM_CLI_SCRIPT' ) && WPCOM_CLI_SCRIPT ||
1213 + is_admin() ||
1214 + is_feed() ||
1215 + wp_doing_ajax() || // supersedes DOING_AJAX
1216 + wp_is_json_request() ||
1217 + wp_is_jsonp_request()
1218 + );
1219 + }
1220 +
1221 + if ( $valid ) {
1222 + $valid = false;
1223 +
1224 + // @since 4.7.1 also consider the excerpt and widgets
1225 + $contents = ( $post->post_excerpt ?? '' ) . ( $post->post_content ?? '' );
1226 + if ( !empty( self::$widgets_contents ) ) {
1227 + $contents .= implode( ' ', self::$widgets_contents );
1228 + }
1229 + /**
1230 + * Allow to filter the contents considered for shortcode validation.
1231 + *
1232 + * @since 4.7.1
1233 + *
1234 + * @param string $contents contents considered
1235 + * @param string $tag shortcode tag
1236 + * @param array $atts shortcode attributes
1237 + * @param string $content shortcode content
1238 + *
1239 + * @return string
1240 + */
1241 + $contents = apply_filters( 'groups_shortcodes_validate_contents', $contents, $tag, $atts, $content );
1242 +
1243 + if ( !empty( $contents ) && has_shortcode( $contents, $tag ) ) {
1244 + $matches = null;
1245 + preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $contents, $matches );
1246 + if ( !empty( $matches[1] ) && is_array( $matches[1] ) && count( $matches[1] ) > 0 ) {
1247 + $tags = array_intersect( array( $tag ), $matches[1] );
1248 + if ( !empty( $tags ) ) {
1249 + // - does not support nested shortcodes
1250 + // - Groups shortcodes do not support nesting
1251 + $pattern = '@' . get_shortcode_regex( $tags ) . '@';
1252 + $m = null;
1253 + preg_match_all( $pattern, $contents, $m );
1254 + // shortcode arguments list
1255 + if ( isset( $m[3] ) && is_array( $m[3] ) && count( $m[3] ) > 0 ) {
1256 + $found = false;
1257 + foreach ( $m[3] as $args ) {
1258 + if ( is_string( $args ) ) {
1259 + $args = trim( $args );
1260 + // decode in case unicode escape sequences used for quotes surrounding attributes need to be decoded for comparison
1261 + $atts_decoded = json_decode( '"' . $args . '"' );
1262 + $attributes = shortcode_parse_atts( $atts_decoded ?? $args );
1263 + $match = true;
1264 + foreach ( $attributes as $key => $value ) {
1265 + if ( !array_key_exists( $key, $atts ) || $atts[$key] !== $value ) {
1266 + $match = false;
1267 + break;
1268 + }
1269 + }
1270 + if ( $match ) {
1271 + $found = true;
1272 + break;
1273 + }
1274 + }
1275 + }
1276 + if ( $found ) {
1277 + $valid = true;
1278 + }
1279 + }
1280 + }
1281 + }
1282 + }
1283 + }
1284 +
1285 + return $valid;
1286 + }
1287 +
1288 + /**
1289 + * Use return value short-circuit to signal shortcode instance processing.
1290 + *
1291 + * @since 4.7.0
1292 + *
1293 + * @param boolean|string $output short-circuit value
1294 + * @param string $tag shortcode tag/name
1295 + * @param array $attr shortcode attributes
1296 + * @param array $m regex match
1297 + *
1298 + * @return boolean|string
1299 + */
1300 + public static function pre_do_shortcode_tag( $output, $tag, $attr, $m ) {
1301 + $hash = md5( json_encode( $attr ) );
1302 + self::$shortcode_queue[] = array( 'tag' => $tag, 'hash' => $hash );
1303 + return $output;
1304 + }
1305 +
1306 + /**
1307 + * Use output filter to signal shortcode processed.
1308 + *
1309 + * @since 4.7.0
1310 + *
1311 + * @param string $output shortcode output
1312 + * @param string $tag shortcode tag/name
1313 + * @param array $attr shortcode attributes
1314 + * @param array $m regex match
1315 + *
1316 + * @return string
1317 + */
1318 + public static function do_shortcode_tag( $output, $tag, $attr, $m ) {
1319 + $queue = array();
1320 + for ( $i = count( self::$shortcode_queue ) - 1; $i >= 0; $i-- ) {
1321 + if ( $tag === self::$shortcode_queue[$i]['tag'] ) {
1322 + $hash = md5( json_encode( $attr ) );
1323 + if ( $hash !== self::$shortcode_queue[$i]['hash'] ) {
1324 + array_unshift( $queue, self::$shortcode_queue[$i] );
1325 + }
1326 + }
1327 + }
1328 + self::$shortcode_queue = $queue;
1329 + return $output;
1330 + }
1331 +
1332 + /**
1333 + * Use callback filter to gather shortcodes in widgets.
1334 + *
1335 + * @since 4.7.1
1336 + *
1337 + * @param array $instance widget instance settings
1338 + * @param WP_Widget $widget widget object
1339 + * @param array $args widget arguments
1340 + *
1341 + * @return array
1342 + */
1343 + public static function widget_display_callback( $instance, $widget, $args ) {
1344 + if ( !empty( $instance ) && is_array( $instance ) ) {
1345 + if ( is_object( $widget ) ) {
1346 + if ( $widget instanceof WP_Widget_Text ) {
1347 + if ( isset( $instance['text'] ) && is_string( $instance['text'] ) ) {
1348 + self::$widgets_contents[] = $instance['text'];
1349 + }
1350 + } else if ( $widget instanceof WP_Widget_Block ) {
1351 + if ( isset( $instance['content'] ) && is_string( $instance['content'] ) ) {
1352 + self::$widgets_contents[] = $instance['content'];
1353 + }
1354 + } else {
1355 + /**
1356 + * Allow widgets to provide content that should be checked during shortcode validation.
1357 + *
1358 + * @since 4.7.1
1359 + *
1360 + * @param string|null $content widget content to check for shortcodes
1361 + * @param array $instance widget instance settings
1362 + * @param WP_Widget $widget widget object
1363 + * @param array $args widget arguments
1364 + *
1365 + * @return string|null string content of the widget or null if widget's content should not be considered
1366 + */
1367 + $content = apply_filters( 'groups_shortcodes_widget_display_callback_widget_content', null, $instance, $widget, $args );
1368 + if ( is_string( $content ) ) {
1369 + self::$widgets_contents[] = $content;
1370 + }
1371 + }
1372 + }
1373 + }
1374 + return $instance;
1375 + }
1376 +
1377 + /**
1378 + * Is processing shortcode tag with given attributes.
1379 + *
1380 + * @since 4.7.0
1381 + *
1382 + * @param string $tag
1383 + * @param array $atts
1384 + *
1385 + * @return boolean
1386 + */
1387 + private static function is_processing( $tag, $atts ) {
1388 + $result = false;
1389 + for ( $i = 0; $i < count( self::$shortcode_queue ); $i++ ) {
1390 + if ( $tag === self::$shortcode_queue[$i]['tag'] ) {
1391 + $hash = md5( json_encode( $atts ) );
1392 + if ( $hash === self::$shortcode_queue[$i]['hash'] ) {
1393 + $result = true;
1394 + }
1395 + }
1396 + }
1397 + return $result;
1398 + }
606 1399 }
1400 +
607 1401 Groups_Shortcodes::init();