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