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
groups / lib / views / class-groups-shortcodes.php

class-groups-shortcodes.php in Groups – Memberships and Access Control 4.7.0, at lib/views/class-groups-shortcodes.php

1,324 lines 40.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class-groups-shortcodes.php
4 *
5 * Copyright (c) "kento" Karim Rahimpur www.itthinx.com
6 *
7 * This code is released under the GNU General Public License.
8 * See COPYRIGHT.txt and LICENSE.txt.
9 *
10 * This code is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * This header and all notices must be kept intact.
16 *
17 * @author Karim Rahimpur
18 * @package groups
19 * @since groups 1.0.0
20 */
21
22 if ( !defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.WP.AlternativeFunctions.rand_rand, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
27
28 /**
29 * Shortcode handlers
30 */
31 class Groups_Shortcodes {
32
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 * Adds shortcodes.
69 */
70 public static function init() {
71 // login
72 add_shortcode( 'groups_login', array( __CLASS__, 'groups_login' ) );
73 // logout
74 add_shortcode( 'groups_logout', array( __CLASS__, 'groups_logout' ) );
75 // group info
76 add_shortcode( 'groups_group_info', array( __CLASS__, 'groups_group_info' ) );
77 // user groups
78 add_shortcode( 'groups_user_groups', array( __CLASS__, 'groups_user_groups' ) );
79 // groups
80 add_shortcode( 'groups_groups', array( __CLASS__, 'groups_groups' ) );
81 // join a group
82 add_shortcode( 'groups_join', array( __CLASS__, 'groups_join' ) );
83 // leave a group
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 );
92 }
93
94 /**
95 * Renders the Groups login form.
96 *
97 * The user is redirected to the current page after login by default.
98 * The user can be redirected to a specific URL after login by
99 * indicating the <code>redirect</code> attribute.
100 *
101 * @param array $atts
102 * @param string $content
103 *
104 * @return string the rendered form or empty
105 */
106 public static function groups_login( $atts, $content = null ) {
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
119 );
120 $redirect = isset( $atts['redirect'] ) ? trim( $atts['redirect'] ) : $current_url;
121 $show_logout = isset( $atts['show_logout'] ) ? trim( strtolower( $atts['show_logout'] ) ) : 'no';
122 $output = '';
123 if ( !is_user_logged_in() ) {
124 $output .= wp_login_form(
125 array(
126 'echo' => false,
127 'redirect' => $redirect
128 )
129 );
130 } else {
131 if ( $show_logout == 'yes' ) {
132 $output .= self::groups_logout(
133 array(
134 'redirect' => $redirect
135 )
136 );
137 }
138 }
139 return $output; // nosemgrep audit.php.wp.security.sqli.shortcode-attr, audit.php.wp.security.xss.shortcode-attr
140 }
141
142 /**
143 * Renders the Groups logout link.
144 *
145 * The link is rendered if the user is logged in.
146 * The user is redirected to the current page after logout by default.
147 * The user can be redirected to a specific URL after logout by
148 * indicating the <code>redirect</code> attribute.
149 *
150 * @param array $atts
151 * @param string $content not used
152 *
153 * @return string logout link, is empty if not logged in
154 */
155 public static function groups_logout( $atts, $content = null ) {
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
167 );
168 $redirect = isset( $atts['redirect'] ) ? trim( $atts['redirect'] ) : $current_url;
169 $output = '';
170 if ( is_user_logged_in() ) {
171 $output .= sprintf( '<a href="%s">', esc_url( wp_logout_url( $redirect ) ) );
172 $output .= esc_html__( 'Log out', 'groups' );
173 $output .= '</a>';
174 }
175 return $output;
176 }
177
178 /**
179 * Renders information about a group.
180 *
181 * Attributes:
182 * - "group" : group name or id
183 * - "show" : what to show, can be "name", "description", "count"
184 * - "format" :
185 * - "single" : used with show="count", single form, defaults to '1'
186 * - "plural" : used with show="count", plural form, defaults to '%d', must contain %d to show number
187 *
188 * @param array $atts attributes
189 * @param string $content content to render
190 *
191 * @return string rendered information
192 */
193 public static function groups_group_info( $atts, $content = null ) {
194
195 global $wpdb;
196
197 if ( !self::validate( 'groups_group_info', $atts, $content ) ) {
198 return '';
199 }
200
201 $output = '';
202 $options = shortcode_atts(
203 array(
204 'group' => '',
205 'show' => '',
206 'format' => '',
207 'none' => '0',
208 'single' => '1',
209 'plural' => '%d'
210 ),
211 $atts
212 );
213 $group = trim( $options['group'] );
214 $current_group = Groups_Group::read( $group );
215 if ( !$current_group ) {
216 $current_group = Groups_Group::read_by_name( $group );
217 }
218 if ( $current_group ) {
219 switch ( $options['show'] ) {
220 case 'name' :
221 $output .= wp_filter_nohtml_kses( $current_group->name );
222 break;
223 case 'description' :
224 $output .= wp_filter_nohtml_kses( $current_group->description );
225 break;
226 case 'count' :
227 $user_group_table = _groups_get_tablename( 'user_group' );
228 $count = $wpdb->get_var( $wpdb->prepare(
229 "SELECT COUNT(*) FROM $user_group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
230 Groups_Utility::id( $current_group->group_id )
231 ) );
232 if ( $count === null ) {
233 $count = 0;
234 } else {
235 $count = intval( $count );
236 }
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 }
247 break;
248 case 'users' :
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' );
252 $users = $wpdb->get_results( $wpdb->prepare(
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
254 Groups_Utility::id( $current_group->group_id )
255 ) );
256 if ( $users ) {
257 $output .= '<ul>';
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>';
261 }
262 $output .= '</ul>';
263 }
264 break;
265 }
266 }
267 return $output; // nosemgrep audit.php.wp.security.sqli.shortcode-attr, audit.php.wp.security.xss.shortcode-attr
268 }
269
270 /**
271 * Renders the current or a specific user's groups.
272 *
273 * Attributes:
274 * - "user_id" OR "user_login" OR "user_email" to identify the user, if none given assumes the current user
275 * - "format" : one of "list" "div" "ul" or "ol" - "list" and "ul" are equivalent
276 * - "list_class" : defaults to "groups"
277 * - "item_class" : defaults to "name"
278 * - "order_by" : defaults to "name", also accepts "group_id"
279 * - "order" : default to "ASC", also accepts "asc", "desc" and "DESC"
280 *
281 * @param array $atts attributes
282 * @param string $content not used
283 *
284 * @return string rendered groups for current user
285 */
286 public static function groups_user_groups( $atts, $content = null ) {
287
288 if ( !self::validate( 'groups_user_groups', $atts, $content ) ) {
289 return '';
290 }
291
292 $output = '';
293 $options = shortcode_atts(
294 array(
295 'user_id' => null,
296 'user_login' => null,
297 'user_email' => null,
298 'format' => 'list',
299 'list_class' => 'groups',
300 'item_class' => 'name',
301 'order_by' => 'name',
302 'order' => 'ASC',
303 'group' => null,
304 'exclude_group' => null
305 ),
306 $atts
307 );
308 $user_id = null;
309 if ( $options['user_id'] !== null ) {
310 if ( $user = get_user_by( 'id', $options['user_id'] ) ) {
311 $user_id = $user->ID;
312 }
313 } else if ( $options['user_id'] !== null ) {
314 if ( $user = get_user_by( 'login', $options['user_login'] ) ) {
315 $user_id = $user->ID;
316 }
317 } else if ( $options['user_email'] !== null ) {
318 if ( $user = get_user_by( 'email', $options['user_login'] ) ) {
319 $user_id = $user->ID;
320 }
321 }
322 if ( $user_id === null ) {
323 $user_id = get_current_user_id();
324 }
325 if ( $user_id !== null ) {
326 $user = new Groups_User( $user_id );
327 $groups = $user->get_groups();
328
329 if ( !empty( $groups ) ) {
330 // group attr
331 if ( $options['group'] !== null ) {
332 $groups = array();
333 $groups_incl = explode( ',', $options['group'] );
334 foreach ( $groups_incl as $group_incl ) {
335 $group = trim( $group_incl );
336 $current_group = Groups_Group::read( $group );
337 if ( !$current_group ) {
338 $current_group = Groups_Group::read_by_name( $group );
339 }
340 if ( $current_group ) {
341 if ( Groups_User::user_is_member( $user_id, $current_group->group_id ) ) {
342 $groups[] = $current_group;
343 }
344 }
345 }
346 }
347 // exclude_group attr
348 if ( $options['exclude_group'] !== null ) {
349 $groups_excl = explode( ',', $options['exclude_group'] );
350 foreach ( $groups_excl as $key => $group_excl ) {
351 $group = trim( $group_excl );
352 $current_group = Groups_Group::read( $group );
353 if ( !$current_group ) {
354 $current_group = Groups_Group::read_by_name( $group );
355 }
356 if ( $current_group ) {
357 $groups_excl[$key] = $current_group->group_id;
358 } else {
359 unset( $groups_excl[$key] );
360 }
361 }
362 foreach ( $groups as $key => $group ) {
363 if ( in_array( $group->group_id, $groups_excl ) ) {
364 unset( $groups[$key] );
365 }
366 }
367 }
368 switch ( $options['order_by'] ) {
369 case 'group_id' :
370 usort( $groups, array( __CLASS__, 'sort_id' ) );
371 break;
372 default :
373 usort( $groups, array( __CLASS__, 'sort_name' ) );
374 }
375 switch ( $options['order'] ) {
376 case 'desc' :
377 case 'DESC' :
378 $groups = array_reverse( $groups );
379 break;
380 }
381
382 switch ( $options['format'] ) {
383 case 'list' :
384 case 'ul' :
385 $output .= '<ul class="' . esc_attr( $options['list_class'] ) . '">';
386 break;
387 case 'ol' :
388 $output .= '<ol class="' . esc_attr( $options['list_class'] ) . '">';
389 break;
390 default :
391 $output .= '<div class="' . esc_attr( $options['list_class'] ) . '">';
392 }
393 foreach ( $groups as $group ) {
394 switch ( $options['format'] ) {
395 case 'list' :
396 case 'ul' :
397 case 'ol' :
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>';
401 break;
402 default :
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>';
406 }
407 }
408 switch ( $options['format'] ) {
409 case 'list' :
410 case 'ul' :
411 $output .= '</ul>';
412 break;
413 case 'ol' :
414 $output .= '</ol>';
415 break;
416 default :
417 $output .= '</div>';
418 }
419 }
420 }
421 return $output;
422 }
423
424 /**
425 * Group comparison by group_id.
426 *
427 * @param Groups_Group $a
428 * @param Groups_Group $b
429 *
430 * @return int
431 */
432 public static function sort_id( $a, $b ) {
433 return $a->get_id() - $b->get_id();
434 }
435
436 /**
437 * Group comparison by name.
438 *
439 * @param Groups_Group $a
440 * @param Groups_Group $b
441 *
442 * @return int
443 */
444 public static function sort_name( $a, $b ) {
445 return strcmp( $a->get_name(), $b->get_name() );
446 }
447
448 /**
449 * Renders a list of the site's groups.
450 *
451 * Attributes:
452 * - "format" : one of "list" "div" "ul" or "ol" - "list" and "ul" are equivalent
453 * - "list_class" : defaults to "groups"
454 * - "item_class" : defaults to "name"
455 * - "order_by" : defaults to "name", also accepts "group_id"
456 * - "order" : default to "ASC", also accepts "asc", "desc" and "DESC"
457 *
458 * @param array $atts attributes
459 * @param string $content not used
460 *
461 * @return string rendered groups
462 */
463 public static function groups_groups( $atts, $content = null ) {
464
465 global $wpdb;
466
467 if ( !self::validate( 'groups_groups', $atts, $content ) ) {
468 return '';
469 }
470
471 $output = '';
472 $options = shortcode_atts(
473 array(
474 'format' => 'list',
475 'list_class' => 'groups',
476 'item_class' => 'name',
477 'order_by' => 'name',
478 'order' => 'ASC'
479 ),
480 $atts
481 );
482 switch ( $options['order_by'] ) {
483 case 'group_id' :
484 case 'name' :
485 $order_by = $options['order_by'];
486 break;
487 default :
488 $order_by = 'name';
489 }
490 switch ( $options['order'] ) {
491 case 'asc' :
492 case 'ASC' :
493 case 'desc' :
494 case 'DESC' :
495 $order = strtoupper( $options['order'] );
496 break;
497 default :
498 $order = 'ASC';
499 }
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'] ) {
505 case 'list' :
506 case 'ul' :
507 $output .= '<ul class="' . esc_attr( $options['list_class'] ) . '">';
508 break;
509 case 'ol' :
510 $output .= '<ol class="' . esc_attr( $options['list_class'] ) . '">';
511 break;
512 default :
513 $output .= '<div class="' . esc_attr( $options['list_class'] ) . '">';
514 }
515 foreach ( $groups as $group ) {
516 $group = new Groups_Group( $group->group_id );
517 switch ( $options['format'] ) {
518 case 'list' :
519 case 'ul' :
520 case 'ol' :
521 $output .= '<li class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $group->get_name() ) ) . '</li>';
522 break;
523 default :
524 $output .= '<div class="' . esc_attr( $options['item_class'] ) . '">' . stripslashes( esc_html( $group->get_name() ) ) . '</div>';
525 }
526 }
527 switch ( $options['format'] ) {
528 case 'list' :
529 case 'ul' :
530 $output .= '</ul>';
531 break;
532 case 'ol' :
533 $output .= '</ol>';
534 break;
535 default :
536 $output .= '</div>';
537 }
538 }
539 return $output;
540 }
541
542 /**
543 * Renders a form that lets a user join a group.
544 *
545 * Attributes:
546 *
547 * - "group" : (required) group name or id
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 *
555 * @param array $atts attributes
556 * @param string $content not used
557 *
558 * @return string
559 */
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
568 $nonce_action = 'groups_action';
569 $nonce = 'nonce_join';
570 $output = '';
571
572 $options = shortcode_atts(
573 array(
574 'class' => '',
575 'group' => '',
576 'display_message' => true,
577 'display_is_member' => false,
578 'redirect' => true,
579 'submit_class' => '',
580 /* translators: group name */
581 'submit_text' => esc_html__( 'Join the %s group', 'groups' )
582 ),
583 $atts
584 );
585
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 }
613 }
614
615 $class = trim( $options['class'] );
616 $submit_class = trim( $options['submit_class'] );
617 $group = trim( $options['group'] );
618 $current_group = Groups_Group::read( $group );
619 if ( !$current_group ) {
620 $current_group = Groups_Group::read_by_name( $group );
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
645 if ( $current_group ) {
646 if ( $user_id = get_current_user_id() ) {
647 $joined = false;
648 $submitted = false;
649 $invalid_nonce = false;
650 if ( groups_sanitize_post( 'groups_action' ) === 'join' ) {
651 $submitted = true;
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
654 $invalid_nonce = true;
655 }
656 }
657 if ( $submitted && !$invalid_nonce ) {
658 // add user to group
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 }
692 }
693 }
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 );
718 $output .= '<form action="#" method="post">';
719 $output .= '<input type="hidden" name="groups_action" value="join" />';
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 );
727 $output .= '</form>';
728 $output .= '</div>';
729 } else if ( $display_message ) {
730 if ( $joined ) {
731 $output .= '<div class="groups-join joined">';
732 /* translators: group name */
733 $output .= sprintf( esc_html__( 'You have joined the %s group.', 'groups' ), stripslashes( wp_filter_nohtml_kses( $current_group->name ) ) );
734 $output .= '</div>';
735 } else if ( $display_is_member && $current_group !== false ) {
736 $output .= '<div class="groups-join member">';
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 ) ) );
739 $output .= '</div>';
740 }
741 }
742 }
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
765 return $output;
766 }
767
768 /**
769 * Renders a form that lets a user leave a group.
770 *
771 * Attributes:
772 *
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
779 *
780 * @param array $atts attributes
781 * @param string $content not used
782 *
783 * @return string
784 */
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
793 $nonce_action = 'groups_action';
794 $nonce = 'nonce_leave';
795 $output = '';
796
797 $options = shortcode_atts(
798 array(
799 'class' => '',
800 'group' => '',
801 'display_message' => true,
802 'redirect' => true,
803 'submit_class' => '',
804 /* translators: group name */
805 'submit_text' => esc_html__( 'Leave the %s group', 'groups' ),
806 ),
807 $atts
808 );
809
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 }
836 }
837
838 $class = trim( $options['class'] );
839 $submit_class = trim( $options['submit_class'] );
840 $group = trim( $options['group'] );
841 $current_group = Groups_Group::read( $group );
842 if ( !$current_group ) {
843 $current_group = Groups_Group::read_by_name( $group );
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
868 if ( $current_group ) {
869 if ( $user_id = get_current_user_id() ) {
870 $left = false;
871 $submitted = false;
872 $invalid_nonce = false;
873 if ( groups_sanitize_post( 'groups_action' ) === 'leave' ) {
874 $submitted = true;
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
877 $invalid_nonce = true;
878 }
879 }
880 if ( $submitted && !$invalid_nonce ) {
881 // remove user from group
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 }
910 }
911 }
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 );
936 $output .= '<form action="#" method="post">';
937 $output .= '<input type="hidden" name="groups_action" value="leave" />';
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 );
945 $output .= '</form>';
946 $output .= '</div>';
947 } else if ( $display_message ) {
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 ) ) );
952 $output .= '</div>';
953 }
954 }
955 }
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
978 return $output;
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 }
1321 }
1322
1323 Groups_Shortcodes::init();
1324