PluginProbe
bbPress / 2.6.19
bbPress v2.6.19
2.6.19 2.6.18 2.6.17 trunk 2.0 2.0-beta-1 2.0-beta-2b 2.0-beta-3 2.0-beta-3b 2.0-rc-2 2.0-rc-3 2.0-rc-4 2.0-rc-5 2.0.1 2.0.2 2.0.3 2.1 2.1-beta-1 2.1-rc1 2.1-rc2 2.1-rc3 2.1-rc4 2.1.1 2.1.2 2.1.3 All 74 releases
bbpress / includes / common / functions.php

functions.php in bbPress 2.6.19, at includes/common/functions.php

3,164 lines 93.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * bbPress Common Functions
5 *
6 * Common functions are ones that are used by more than one component, like
7 * forums, topics, replies, users, topic tags, etc...
8 *
9 * @package bbPress
10 * @subpackage Functions
11 */
12
13 // Exit if accessed directly
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Return array of bbPress registered post types
18 *
19 * @since 2.6.0 bbPress (r6814)
20 *
21 * @param array $args Array of arguments to pass into `get_post_types()`
22 *
23 * @return array
24 */
25 function bbp_get_post_types( $args = array() ) {
26
27 // Parse args
28 $r = bbp_parse_args(
29 $args,
30 array(
31 'source' => 'bbpress'
32 ),
33 'get_post_types'
34 );
35
36 // Return post types
37 return get_post_types( $r );
38 }
39
40 /**
41 * Exclude bbPress content from WordPress public author discovery.
42 *
43 * WordPress counts published posts without checking their forum visibility.
44 * A published topic or reply may belong to a private or hidden forum.
45 *
46 * @since 2.6.19 bbPress (r7710)
47 *
48 * @param array $args User query arguments.
49 * @param WP_REST_Request|null $request Optional REST request.
50 * @return array Filtered user query arguments.
51 */
52 function bbp_exclude_forum_posts_from_user_query( $args, $request = null ) {
53 if ( $request instanceof WP_REST_Request && current_user_can( 'list_users' ) ) {
54 return $args;
55 }
56
57 if ( empty( $args['has_published_posts'] ) || ! is_array( $args['has_published_posts'] ) ) {
58 return $args;
59 }
60
61 $args['has_published_posts'] = array_values( array_diff( $args['has_published_posts'], bbp_get_post_types() ) );
62
63 // An empty post-type list would otherwise remove the author restriction.
64 if ( empty( $args['has_published_posts'] ) ) {
65 $args['include'] = array( 0 );
66 }
67
68 return $args;
69 }
70
71 /**
72 * Exclude replies in non-public topics from public bbPress queries.
73 *
74 * The query flag scopes this SQL clause to listings that expose replies
75 * outside a single topic, so pagination counts match the visible results.
76 *
77 * @since 2.6.19 bbPress (r7618)
78 *
79 * @param string $where SQL WHERE clause.
80 * @param WP_Query $posts_query Posts query.
81 * @return string SQL WHERE clause.
82 */
83 function bbp_public_topic_replies_where( $where = '', $posts_query = null ) {
84 if ( ! $posts_query instanceof WP_Query || ! $posts_query->get( '_bbp_public_topic_replies' ) ) {
85 return $where;
86 }
87
88 $bbp_db = bbp_db();
89 $statuses = bbp_get_public_topic_statuses();
90 if ( $posts_query->get( '_bbp_search_private_topic_replies' ) && current_user_can( 'read_private_topics' ) && ! is_feed() && ! $posts_query->is_feed() ) {
91 $statuses[] = bbp_get_private_status_id();
92 }
93
94 if ( empty( $statuses ) ) {
95 return $where . $bbp_db->prepare( " AND {$bbp_db->posts}.post_type <> %s", bbp_get_reply_post_type() );
96 }
97
98 $placeholders = implode( ', ', array_fill( 0, count( $statuses ), '%s' ) );
99 $where .= $bbp_db->prepare(
100 " AND ( {$bbp_db->posts}.post_type <> %s OR EXISTS ( SELECT 1 FROM {$bbp_db->posts} AS bbp_parent_topic WHERE bbp_parent_topic.ID = {$bbp_db->posts}.post_parent AND bbp_parent_topic.post_type = %s AND bbp_parent_topic.post_status IN ({$placeholders}) ) )",
101 array_merge( array( bbp_get_reply_post_type(), bbp_get_topic_post_type() ), $statuses )
102 );
103
104 return $where;
105 }
106
107 /** URLs **********************************************************************/
108
109 /**
110 * Return the unescaped redirect_to request value
111 *
112 * @bbPress (r4655)
113 *
114 * @return string The URL to redirect to, if set
115 */
116 function bbp_get_redirect_to() {
117
118 // Check 'redirect_to' request parameter
119 $retval = ! empty( $_REQUEST['redirect_to'] )
120 ? $_REQUEST['redirect_to']
121 : '';
122
123 // Filter & return
124 return apply_filters( 'bbp_get_redirect_to', $retval );
125 }
126
127 /**
128 * Append 'view=all' to query string if it's already there from referer
129 *
130 * @since 2.0.0 bbPress (r2957)
131 *
132 * @param string $original_link Original Link to be modified
133 * @param bool $force Override bbp_get_view_all() check
134 * @return string The link with 'view=all' appended if necessary
135 */
136 function bbp_add_view_all( $original_link = '', $force = false ) {
137
138 // Are we appending the view=all vars?
139 $link = ( bbp_get_view_all() || ! empty( $force ) )
140 ? add_query_arg( array( 'view' => 'all' ), $original_link )
141 : $original_link;
142
143 // Filter & return
144 return apply_filters( 'bbp_add_view_all', $link, $original_link );
145 }
146
147 /**
148 * Remove 'view=all' from query string
149 *
150 * @since 2.0.0 bbPress (r3325)
151 *
152 * @param string $original_link Original Link to be modified
153 * @return string The link with 'view=all' appended if necessary
154 */
155 function bbp_remove_view_all( $original_link = '' ) {
156
157 // Remove `view' argument
158 $link = remove_query_arg( 'view', $original_link );
159
160 // Filter & return
161 return apply_filters( 'bbp_remove_view_all', $link, $original_link );
162 }
163
164 /**
165 * If current user can and is viewing all topics/replies
166 *
167 * @since 2.0.0 bbPress (r3325)
168 *
169 * @param string $cap Capability used to ensure user can view all
170 *
171 * @return bool Whether current user can and is viewing all
172 */
173 function bbp_get_view_all( $cap = 'moderate' ) {
174 $retval = ( ( ! empty( $_GET['view'] ) && ( 'all' === $_GET['view'] ) && current_user_can( $cap ) ) );
175
176 // Filter & return
177 return (bool) apply_filters( 'bbp_get_view_all', (bool) $retval, $cap );
178 }
179
180 /**
181 * Assist pagination by returning correct page number
182 *
183 * @since 2.0.0 bbPress (r2634)
184 *
185 * @return int Current page number
186 */
187 function bbp_get_paged() {
188 $wp_query = bbp_get_wp_query();
189
190 // Check the query var
191 if ( get_query_var( 'paged' ) ) {
192 $paged = get_query_var( 'paged' );
193
194 // Check query paged
195 } elseif ( ! empty( $wp_query->query['paged'] ) ) {
196 $paged = $wp_query->query['paged'];
197 }
198
199 // Paged found
200 if ( ! empty( $paged ) ) {
201 return (int) $paged;
202 }
203
204 // Default to first page
205 return 1;
206 }
207
208 /** Misc **********************************************************************/
209
210 /**
211 * Return the unique non-empty values of an array.
212 *
213 * @since 2.6.0 bbPress (r6481)
214 *
215 * @param array $array Array to get values of
216 *
217 * @return array
218 */
219 // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.arrayFound -- Preserve the public parameter name for PHP 8 named arguments.
220 function bbp_get_unique_array_values( $array = array() ) {
221 return array_unique( array_filter( array_values( $array ) ) );
222 }
223
224 /**
225 * Return the non-empty string values of an array.
226 *
227 * @since 2.6.17 bbPress (r7506)
228 *
229 * @param mixed $arr Value or array to get string values of
230 *
231 * @return array
232 */
233 function bbp_get_string_array_values( $arr = array() ) {
234 $retval = array();
235
236 foreach ( (array) $arr as $value ) {
237 if ( is_string( $value ) && ( '' !== $value ) ) {
238 $retval[] = $value;
239 }
240 }
241
242 return $retval;
243 }
244
245 /**
246 * Fix post author id on post save
247 *
248 * When a logged in user changes the status of an anonymous reply or topic, or
249 * edits it, the post_author field is set to the logged in user's id. This
250 * function fixes that.
251 *
252 * @since 2.0.0 bbPress (r2734)
253 *
254 * @param array $data Post data
255 * @param array $postarr Original post array (includes post id)
256 * @return array Data
257 */
258 function bbp_fix_post_author( $data = array(), $postarr = array() ) {
259
260 // Post is not being updated or the post_author is already 0, return
261 if ( empty( $postarr['ID'] ) || empty( $data['post_author'] ) ) {
262 return $data;
263 }
264
265 // Post is not a topic or reply, return
266 if ( ! in_array( $data['post_type'], array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ), true ) ) {
267 return $data;
268 }
269
270 // Is the post by an anonymous user?
271 if (
272 ( bbp_get_topic_post_type() === $data['post_type'] && ! bbp_is_topic_anonymous( $postarr['ID'] ) )
273 ||
274 ( bbp_get_reply_post_type() === $data['post_type'] && ! bbp_is_reply_anonymous( $postarr['ID'] ) )
275 ) {
276 return $data;
277 }
278
279 // The post is being updated. It is a topic or a reply and is written by an anonymous user.
280 // Set the post_author back to 0
281 $data['post_author'] = 0;
282
283 return $data;
284 }
285
286 /**
287 * Use the previous status when restoring a forum, topic, or reply.
288 *
289 * Fixes an issue since WordPress 5.6.0. See
290 * {@link https://bbpress.trac.wordpress.org/ticket/3433}.
291 *
292 * @since 2.6.10 bbPress (r7234)
293 *
294 * @param string $new_status New status to use when untrashing. Default: 'draft'
295 * @param int $post_id Post ID
296 * @param string $previous_status Previous post status from '_wp_trash_meta_status' meta key. Default: 'pending'
297 */
298 function bbp_fix_untrash_post_status( $new_status = 'draft', $post_id = 0, $previous_status = 'pending' ) {
299
300 // Bail if not a forum, topic, or reply
301 if ( ! bbp_is_forum( $post_id ) && ! bbp_is_topic( $post_id ) && ! bbp_is_reply( $post_id ) ) {
302 return $new_status;
303 }
304
305 // Prefer the previous status, falling back to the new status
306 $retval = ! empty( $previous_status )
307 ? $previous_status
308 : $new_status;
309
310 return $retval;
311 }
312
313 /**
314 * Update related counts when a topic or reply is created or changes status.
315 *
316 * @since 2.6.17 bbPress (r7451)
317 *
318 * @param string $new_status New post status.
319 * @param string $old_status Old post status.
320 * @param WP_Post $post Post object.
321 */
322 function bbp_update_counts_on_transition_post_status( $new_status = '', $old_status = '', $post = false ) {
323
324 /**
325 * Short-circuits count updates for a persisted post-status transition.
326 *
327 * Returning a non-null value prevents the normal topic and reply count
328 * updates. This allows integrations with custom post-status lifecycles or
329 * count storage to replace the complete transition operation.
330 *
331 * @since 2.6.17 bbPress (r7478)
332 *
333 * @param null|bool $check Whether to short-circuit count updates.
334 * @param string $new_status New post status.
335 * @param string $old_status Old post status.
336 * @param WP_Post $post Post object.
337 */
338 $check = apply_filters( 'bbp_pre_update_counts_on_transition_post_status', null, $new_status, $old_status, $post );
339 if ( null !== $check ) {
340 return (bool) $check;
341 }
342
343 // Bail if the status did not change
344 if ( $new_status === $old_status ) {
345 return;
346 }
347
348 $is_new = ( 'new' === $old_status );
349
350 // Topic counts
351 if ( bbp_get_topic_post_type() === $post->post_type ) {
352 $was_public = in_array( $old_status, bbp_get_public_topic_statuses(), true );
353 $is_public = in_array( $new_status, bbp_get_public_topic_statuses(), true );
354 $was_hidden = in_array( $old_status, bbp_get_non_public_topic_statuses(), true );
355 $is_hidden = in_array( $new_status, bbp_get_non_public_topic_statuses(), true );
356 $public_difference = (int) $is_public - (int) $was_public;
357 $hidden_difference = (int) $is_hidden - (int) $was_hidden;
358
359 // A new topic or count boundary crossing changes at least one count
360 if ( ! empty( $public_difference ) || ! empty( $hidden_difference ) ) {
361 $forum_id = $is_new
362 ? $post->post_parent
363 : bbp_get_topic_forum_id( $post->ID );
364
365 // Update the forum's public topic count
366 if ( ! empty( $forum_id ) && ! empty( $public_difference ) ) {
367 bbp_bump_forum_topic_count( $forum_id, $public_difference );
368 }
369
370 // Update the forum's hidden topic count
371 if ( ! empty( $forum_id ) && ! empty( $hidden_difference ) ) {
372 bbp_bump_forum_topic_count_hidden( $forum_id, $hidden_difference );
373 }
374
375 // User counts only include public topics
376 if ( ! empty( $public_difference ) ) {
377 bbp_bump_user_topic_count( $post->post_author, $public_difference );
378 }
379
380 // Apply every public reply when its topic crosses the public boundary
381 if ( ! $is_new && ! empty( $forum_id ) && ! empty( $public_difference ) ) {
382 $reply_count = bbp_get_topic_reply_count( $post->ID, true );
383 $reply_difference = $reply_count * $public_difference;
384
385 if ( ! empty( $reply_difference ) ) {
386 bbp_bump_forum_reply_count( $forum_id, $reply_difference );
387 }
388 }
389 }
390
391 // Reply counts
392 } elseif ( bbp_get_reply_post_type() === $post->post_type ) {
393 $was_public = in_array( $old_status, bbp_get_public_reply_statuses(), true );
394 $is_public = in_array( $new_status, bbp_get_public_reply_statuses(), true );
395 $was_hidden = in_array( $old_status, bbp_get_non_public_reply_statuses(), true );
396 $is_hidden = in_array( $new_status, bbp_get_non_public_reply_statuses(), true );
397 $public_difference = (int) $is_public - (int) $was_public;
398 $hidden_difference = (int) $is_hidden - (int) $was_hidden;
399
400 // A new reply or count boundary crossing changes at least one count
401 if ( ! empty( $public_difference ) || ! empty( $hidden_difference ) ) {
402 $topic_id = $is_new
403 ? $post->post_parent
404 : bbp_get_reply_topic_id( $post->ID );
405 $forum_id = $is_new
406 ? bbp_get_topic_forum_id( $topic_id )
407 : bbp_get_reply_forum_id( $post->ID );
408 $forum_public_difference = bbp_is_topic_public( $topic_id )
409 ? $public_difference
410 : 0;
411
412 // Update the topic's public reply count
413 if ( ! empty( $topic_id ) && ! empty( $public_difference ) ) {
414 bbp_bump_topic_reply_count( $topic_id, $public_difference );
415 }
416
417 // Update the topic's hidden reply count
418 if ( ! empty( $topic_id ) && ! empty( $hidden_difference ) ) {
419 bbp_bump_topic_reply_count_hidden( $topic_id, $hidden_difference );
420 }
421
422 // Update the forum's public reply count
423 if ( ! empty( $forum_id ) && ! empty( $forum_public_difference ) ) {
424 bbp_bump_forum_reply_count( $forum_id, $forum_public_difference );
425 }
426
427 // Update the forum's hidden reply count
428 if ( ! empty( $forum_id ) && ! empty( $hidden_difference ) ) {
429 bbp_bump_forum_reply_count_hidden( $forum_id, $hidden_difference );
430 }
431
432 // User counts only include public replies
433 if ( ! empty( $public_difference ) ) {
434 bbp_bump_user_reply_count( $post->post_author, $public_difference );
435 }
436 }
437 }
438 }
439
440 /**
441 * Check a date against the length of time something can be edited.
442 *
443 * It is recommended to leave $utc set to true and to work with UTC/GMT dates.
444 * Turning this off will use the WordPress offset which is likely undesirable.
445 *
446 * @since 2.0.0 bbPress (r3134)
447 * @since 2.6.0 bbPress (r6868) Inverted some logic and added unit tests
448 *
449 * @param string $datetime Gets run through strtotime()
450 * @param boolean $utc Default true. Is the timestamp in UTC?
451 *
452 * @return bool True by default, if date is past, or editing is disabled.
453 */
454 function bbp_past_edit_lock( $datetime = '', $utc = true ) {
455
456 // Default value
457 $retval = true;
458
459 // Check if date and editing is allowed
460 if ( bbp_allow_content_edit() ) {
461
462 // Get number of minutes to allow editing for
463 $minutes = bbp_get_edit_lock();
464
465 // 0 minutes means forever, so can never be past edit-lock time
466 if ( 0 === $minutes ) {
467 $retval = false;
468
469 // Checking against a specific datetime
470 } elseif ( ! empty( $datetime ) ) {
471
472 // Period of time
473 $lockable = "+{$minutes} minutes";
474 if ( true === $utc ) {
475 $lockable .= ' UTC';
476 }
477
478 // Now
479 $cur_time = current_time( 'timestamp', $utc ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
480
481 // Get the duration in seconds
482 $duration = strtotime( $lockable ) - $cur_time;
483
484 // Diff the times down to seconds
485 $lock_time = strtotime( $lockable, $cur_time );
486 $past_time = strtotime( $datetime, $cur_time );
487 $diff_time = ( $lock_time - $past_time ) - $duration;
488
489 // Check if less than lock time
490 if ( $diff_time < $duration ) {
491 $retval = false;
492 }
493 }
494 }
495
496 // Filter & return
497 return (bool) apply_filters( 'bbp_past_edit_lock', $retval, $datetime, $utc );
498 }
499
500 /**
501 * Get number of days something should remain trashed for before it is cleaned
502 * up by WordPress Cron. If set to 0, items will skip trash and be deleted
503 * immediately.
504 *
505 * @since 2.6.0 bbPress (r6432)
506 *
507 * @param string $context Provide context for additional filtering
508 * @return int Number of days items remain in trash
509 */
510 function bbp_get_trash_days( $context = 'forum' ) {
511
512 // Sanitize the context
513 $context = sanitize_key( $context );
514
515 // Check the WordPress constant
516 $days = defined( 'EMPTY_TRASH_DAYS' )
517 ? (int) EMPTY_TRASH_DAYS
518 : 30;
519
520 // Filter & return
521 return (int) apply_filters( 'bbp_get_trash_days', $days, $context );
522 }
523
524 /** Statistics ****************************************************************/
525
526 /**
527 * Get the forum statistics
528 *
529 * @since 2.0.0 bbPress (r2769)
530 * @since 2.6.0 bbPress (r6055) Added:
531 * `count_pending_topics`
532 * `count_pending_replies`
533 * @since 2.6.10 bbPress (r7235) Renamed:
534 * `count_trashed_topics` to `count_trash_topics`
535 * `count_trashed_replies` to `count_trash_replies`
536 * `count_spammed_topics` to `count_spam_topics`
537 * `count_spammed_replies` to `count_spam_replies`
538 * Added:
539 * `count_hidden_topics`
540 * `count_hidden_replies`
541 *
542 * @param array $args Optional. The function supports these arguments (all
543 * default to true):
544 *
545 * - count_users: Count users?
546 * - count_forums: Count forums?
547 * - count_topics: Count topics? If set to false, private, spam and
548 * trash topics are also not counted.
549 * - count_pending_topics: Count pending topics? (only counted if the current
550 * user has edit_others_topics cap)
551 * - count_private_topics: Count private topics? (only counted if the current
552 * user has read_private_topics cap)
553 * - count_hidden_topics: Count hidden topics? (only counted if the current
554 * user has read_hidden_forums cap)
555 * - count_spam_topics: Count spam topics? (only counted if the current
556 * user has edit_others_topics cap)
557 * - count_trash_topics: Count trash topics? (only counted if the current
558 * user has view_trash cap)
559 * - count_replies: Count replies? If set to false, private, spam and
560 * trash replies are also not counted.
561 * - count_pending_replies: Count pending replies? (only counted if the current
562 * user has edit_others_replies cap)
563 * - count_private_replies: Count private replies? (only counted if the current
564 * user has read_private_replies cap)
565 * - count_hidden_replies: Count hidden replies? (only counted if the current
566 * user has read_hidden_forums cap)
567 * - count_spam_replies: Count spam replies? (only counted if the current
568 * user has edit_others_replies cap)
569 * - count_trash_replies: Count trash replies? (only counted if the current
570 * user has view_trash cap)
571 * - count_tags: Count tags? If set to false, empty tags are also
572 * not counted
573 * - count_empty_tags: Count empty tags?
574 *
575 * @return array Array of statistics
576 */
577 function bbp_get_statistics( $args = array() ) {
578
579 // Parse arguments against default values
580 $r = bbp_parse_args(
581 $args,
582 array(
583
584 // Users
585 'count_users' => true,
586
587 // Forums
588 'count_forums' => true,
589
590 // Topics
591 'count_topics' => true,
592 'count_pending_topics' => true,
593 'count_private_topics' => true,
594 'count_spam_topics' => true,
595 'count_trash_topics' => true,
596 'count_hidden_topics' => true,
597
598 // Replies
599 'count_replies' => true,
600 'count_pending_replies' => true,
601 'count_private_replies' => true,
602 'count_spam_replies' => true,
603 'count_trash_replies' => true,
604 'count_hidden_replies' => true,
605
606 // Topic tags
607 'count_tags' => true,
608 'count_empty_tags' => true
609
610 ),
611 'get_statistics'
612 );
613
614 // Defaults
615 $topic_count = $topic_count_hidden = 0;
616 $reply_count = $reply_count_hidden = 0;
617 $topic_tag_count = $empty_topic_tag_count = 0;
618 $hidden_topic_title = $hidden_reply_title = '';
619
620 // Post statuses
621 $publish = bbp_get_public_status_id();
622 $closed = bbp_get_closed_status_id();
623 $pending = bbp_get_pending_status_id();
624 $private = bbp_get_private_status_id();
625 $hidden = bbp_get_hidden_status_id();
626 $spam = bbp_get_spam_status_id();
627 $trash = bbp_get_trash_status_id();
628
629 // Users
630 $user_count = ! empty( $r['count_users'] )
631 ? bbp_get_total_users()
632 : 0;
633
634 // Forums
635 $forum_count = ! empty( $r['count_forums'] )
636 ? wp_count_posts( bbp_get_forum_post_type() )->{$publish}
637 : 0;
638
639 // Default capabilities
640 $caps = array(
641 'view_trash' => false,
642 'read_private_topics' => false,
643 'read_hidden_forums' => false,
644 'edit_others_topics' => false,
645 'read_private_replies' => false,
646 'edit_others_replies' => false,
647 'edit_topic_tags' => false
648 );
649
650 // Get capabilities
651 foreach ( array_keys( $caps ) as $cap ) {
652 $caps[ $cap ] = current_user_can( $cap );
653 }
654
655 // Topics
656 if ( ! empty( $r['count_topics'] ) ) {
657
658 // Count all topics
659 $all_topics = wp_count_posts( bbp_get_topic_post_type() );
660
661 // Published (publish + closed)
662 $topic_count = $all_topics->{$publish} + $all_topics->{$closed};
663
664 // Declare empty arrays
665 $topics = $topic_titles = array_fill_keys( bbp_get_non_public_topic_statuses(), '' );
666
667 // Pending
668 if ( ! empty( $r['count_pending_topics'] ) && ! empty( $caps['edit_others_topics'] ) ) {
669 $topics[ $pending ] = bbp_number_not_negative( $all_topics->{$pending} );
670 /* translators: %s: Number of pending topics */
671 $topic_titles[ $pending ] = sprintf( esc_html__( 'Pending: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $pending ] ) );
672 }
673
674 // Private
675 if ( ! empty( $r['count_private_topics'] ) && ! empty( $caps['read_private_topics'] ) ) {
676 $topics[ $private ] = bbp_number_not_negative( $all_topics->{$private} );
677 /* translators: %s: Number of private topics */
678 $topic_titles[ $private ] = sprintf( esc_html__( 'Private: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $private ] ) );
679 }
680
681 // Hidden
682 if ( ! empty( $r['count_hidden_topics'] ) && ! empty( $caps['read_hidden_forums'] ) ) {
683 $topics[ $hidden ] = bbp_number_not_negative( $all_topics->{$hidden} );
684 /* translators: %s: Number of hidden topics */
685 $topic_titles[ $hidden ] = sprintf( esc_html__( 'Hidden: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $hidden ] ) );
686 }
687
688 // Spam
689 if ( ! empty( $r['count_spam_topics'] ) && ! empty( $caps['edit_others_topics'] ) ) {
690 $topics[ $spam ] = bbp_number_not_negative( $all_topics->{$spam} );
691 /* translators: %s: Number of spam topics */
692 $topic_titles[ $spam ] = sprintf( esc_html__( 'Spammed: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $spam ] ) );
693 }
694
695 // Trash
696 if ( ! empty( $r['count_trash_topics'] ) && ! empty( $caps['view_trash'] ) ) {
697 $topics[ $trash ] = bbp_number_not_negative( $all_topics->{$trash} );
698 /* translators: %s: Number of trashed topics */
699 $topic_titles[ $trash ] = sprintf( esc_html__( 'Trashed: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $trash ] ) );
700 }
701
702 // Total hidden (pending, private, hidden, spam, trash)
703 $topic_count_hidden = array_sum( array_filter( $topics ) );
704
705 // Compile the hidden topic title
706 $hidden_topic_title = implode( ' | ', array_filter( $topic_titles ) );
707 }
708
709 // Replies
710 if ( ! empty( $r['count_replies'] ) ) {
711
712 // Count all replies
713 $all_replies = wp_count_posts( bbp_get_reply_post_type() );
714
715 // Published
716 $reply_count = $all_replies->{$publish};
717
718 // Declare empty arrays
719 $replies = $reply_titles = array_fill_keys( bbp_get_non_public_reply_statuses(), '' );
720
721 // Pending
722 if ( ! empty( $r['count_pending_replies'] ) && ! empty( $caps['edit_others_replies'] ) ) {
723 $replies[ $pending ] = bbp_number_not_negative( $all_replies->{$pending} );
724 /* translators: %s: Number of pending replies */
725 $reply_titles[ $pending ] = sprintf( esc_html__( 'Pending: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $pending ] ) );
726 }
727
728 // Private
729 if ( ! empty( $r['count_private_replies'] ) && ! empty( $caps['read_private_replies'] ) ) {
730 $replies[ $private ] = bbp_number_not_negative( $all_replies->{$private} );
731 /* translators: %s: Number of private replies */
732 $reply_titles[ $private ] = sprintf( esc_html__( 'Private: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $private ] ) );
733 }
734
735 // Hidden
736 if ( ! empty( $r['count_hidden_replies'] ) && ! empty( $caps['read_hidden_forums'] ) ) {
737 $replies[ $hidden ] = bbp_number_not_negative( $all_replies->{$hidden} );
738 /* translators: %s: Number of hidden replies */
739 $reply_titles[ $hidden ] = sprintf( esc_html__( 'Hidden: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $hidden ] ) );
740 }
741
742 // Spam
743 if ( ! empty( $r['count_spam_replies'] ) && ! empty( $caps['edit_others_replies'] ) ) {
744 $replies[ $spam ] = bbp_number_not_negative( $all_replies->{$spam} );
745 /* translators: %s: Number of spam replies */
746 $reply_titles[ $spam ] = sprintf( esc_html__( 'Spammed: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $spam ] ) );
747 }
748
749 // Trash
750 if ( ! empty( $r['count_trash_replies'] ) && ! empty( $caps['view_trash'] ) ) {
751 $replies[ $trash ] = bbp_number_not_negative( $all_replies->{$trash} );
752 /* translators: %s: Number of trashed replies */
753 $reply_titles[ $trash ] = sprintf( esc_html__( 'Trashed: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $trash ] ) );
754 }
755
756 // Total hidden (pending, private, hidden, spam, trash)
757 $reply_count_hidden = array_sum( array_filter( $replies ) );
758
759 // Compile the hidden replies title
760 $hidden_reply_title = implode( ' | ', $reply_titles );
761 }
762
763 // Topic Tags
764 if ( ! empty( $r['count_tags'] ) && bbp_allow_topic_tags() ) {
765
766 // Get the topic-tag taxonomy ID
767 $tt_id = bbp_get_topic_tag_tax_id();
768
769 // Get the count
770 $topic_tag_count = wp_count_terms(
771 array(
772 'taxonomy' => $tt_id,
773 'hide_empty' => true
774 )
775 );
776
777 // Empty tags
778 if ( ! empty( $r['count_empty_tags'] ) && ! empty( $caps['edit_topic_tags'] ) ) {
779 $empty_topic_tag_count = wp_count_terms( $tt_id ) - $topic_tag_count;
780 }
781 }
782
783 // Tally the tallies
784 $counts = compact(
785 'user_count',
786 'forum_count',
787 'topic_count',
788 'topic_count_hidden',
789 'reply_count',
790 'reply_count_hidden',
791 'topic_tag_count',
792 'empty_topic_tag_count'
793 );
794
795 // Define return value
796 $statistics = array();
797
798 // Loop through and store the integer and i18n formatted counts
799 foreach ( $counts as $key => $count ) {
800 $not_negative = bbp_number_not_negative( $count );
801 $statistics[ $key ] = bbp_number_format_i18n( $not_negative );
802 $statistics[ "{$key}_int" ] = $not_negative;
803 }
804
805 // Add the hidden (topic/reply) count title attribute strings
806 $statistics['hidden_topic_title'] = $hidden_topic_title;
807 $statistics['hidden_reply_title'] = $hidden_reply_title;
808
809 // Filter & return
810 return (array) apply_filters( 'bbp_get_statistics', $statistics, $r, $args );
811 }
812
813 /** New/edit topic/reply helpers **********************************************/
814
815 /**
816 * Filter anonymous post data
817 *
818 * We use REMOTE_ADDR here directly. If you are behind a proxy, you should
819 * ensure that it is properly set, such as in wp-config.php, for your
820 * environment. See {@link https://core.trac.wordpress.org/ticket/9235}
821 *
822 * Note that bbp_pre_anonymous_filters() is responsible for sanitizing each
823 * of the filtered core anonymous values here.
824 *
825 * If there are any errors, those are directly added to {@link bbPress:errors}
826 *
827 * @since 2.0.0 bbPress (r2734)
828 *
829 * @param array $args Optional. If no args are there, then $_POST values are
830 * @return bool|array False on errors, values in an array on success
831 */
832 function bbp_filter_anonymous_post_data( $args = array() ) {
833
834 // Parse arguments against default values
835 $r = bbp_parse_args(
836 $args,
837 array(
838 'bbp_anonymous_name' => ! empty( $_POST['bbp_anonymous_name'] ) ? $_POST['bbp_anonymous_name'] : false,
839 'bbp_anonymous_email' => ! empty( $_POST['bbp_anonymous_email'] ) ? $_POST['bbp_anonymous_email'] : false,
840 'bbp_anonymous_website' => ! empty( $_POST['bbp_anonymous_website'] ) ? $_POST['bbp_anonymous_website'] : false,
841 ),
842 'filter_anonymous_post_data'
843 );
844
845 // Strip invalid characters
846 $r = bbp_sanitize_anonymous_post_author( $r );
847
848 // Filter name
849 $r['bbp_anonymous_name'] = apply_filters( 'bbp_pre_anonymous_post_author_name', $r['bbp_anonymous_name'] );
850 if ( empty( $r['bbp_anonymous_name'] ) ) {
851 bbp_add_error( 'bbp_anonymous_name', __( '<strong>Error</strong>: Invalid author name.', 'bbpress' ) );
852 }
853
854 // Filter email address
855 $r['bbp_anonymous_email'] = apply_filters( 'bbp_pre_anonymous_post_author_email', $r['bbp_anonymous_email'] );
856 if ( empty( $r['bbp_anonymous_email'] ) ) {
857 bbp_add_error( 'bbp_anonymous_email', __( '<strong>Error</strong>: Invalid email address.', 'bbpress' ) );
858 }
859
860 // Website is optional (can be empty)
861 $r['bbp_anonymous_website'] = apply_filters( 'bbp_pre_anonymous_post_author_website', $r['bbp_anonymous_website'] );
862
863 // Filter & return
864 return (array) apply_filters( 'bbp_filter_anonymous_post_data', $r, $args );
865 }
866
867 /**
868 * Sanitize an array of anonymous post author data
869 *
870 * @since 2.6.0 bbPress (r6400)
871 *
872 * @param array $anonymous_data
873 * @return array
874 */
875 function bbp_sanitize_anonymous_post_author( $anonymous_data = array() ) {
876
877 // Make sure anonymous data is an array
878 if ( ! is_array( $anonymous_data ) ) {
879 $anonymous_data = array();
880 }
881
882 // Map meta data to comment fields (as guides for stripping invalid text)
883 $fields = array(
884 'bbp_anonymous_name' => 'comment_author',
885 'bbp_anonymous_email' => 'comment_author_email',
886 'bbp_anonymous_website' => 'comment_author_url'
887 );
888
889 // Setup a new return array
890 $r = $anonymous_data;
891
892 // Get the database
893 $bbp_db = bbp_db();
894
895 // Strip invalid text from fields
896 foreach ( $fields as $bbp_field => $comment_field ) {
897 if ( ! empty( $r[ $bbp_field ] ) ) {
898 $r[ $bbp_field ] = $bbp_db->strip_invalid_text_for_column( $bbp_db->comments, $comment_field, $r[ $bbp_field ] );
899 }
900 }
901
902 // Filter & return
903 return (array) apply_filters( 'bbp_sanitize_anonymous_post_author', $r, $anonymous_data );
904 }
905
906 /**
907 * Update the relevant meta-data for an anonymous post author
908 *
909 * @since 2.6.0 bbPress (r6400)
910 *
911 * @param int $post_id
912 * @param array $anonymous_data
913 * @param string $post_type
914 */
915 function bbp_update_anonymous_post_author( $post_id = 0, $anonymous_data = array(), $post_type = '' ) {
916
917 // Maybe look for anonymous
918 if ( empty( $anonymous_data ) ) {
919 $anonymous_data = bbp_filter_anonymous_post_data();
920 }
921
922 // Sanitize parameters
923 $post_id = (int) $post_id;
924 $post_type = sanitize_key( $post_type );
925
926 // Bail if missing required data
927 if ( empty( $post_id ) || empty( $post_type ) || empty( $anonymous_data ) ) {
928 return;
929 }
930
931 // Parse arguments against default values
932 $r = bbp_parse_args(
933 $anonymous_data,
934 array(
935 'bbp_anonymous_name' => '',
936 'bbp_anonymous_email' => '',
937 'bbp_anonymous_website' => '',
938 ),
939 "update_{$post_type}"
940 );
941
942 // Update all anonymous metas
943 foreach ( $r as $anon_key => $anon_value ) {
944
945 // Update, or delete if empty
946 ! empty( $anon_value )
947 ? update_post_meta( $post_id, '_' . $anon_key, (string) $anon_value, false )
948 : delete_post_meta( $post_id, '_' . $anon_key );
949 }
950 }
951
952 /**
953 * Check for duplicate topics/replies
954 *
955 * Check to make sure that a user is not making a duplicate post
956 *
957 * @since 2.0.0 bbPress (r2763)
958 *
959 * @param array $post_data Contains information about the comment
960 * @return bool True if it is not a duplicate, false if it is
961 */
962 function bbp_check_for_duplicate( $post_data = array() ) {
963
964 // Parse arguments against default values
965 $r = bbp_parse_args(
966 $post_data,
967 array(
968 'post_author' => 0,
969 'post_type' => array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ),
970 'post_parent' => 0,
971 'post_content' => '',
972 'post_status' => bbp_get_trash_status_id(),
973 'anonymous_data' => array()
974 ),
975 'check_for_duplicate'
976 );
977
978 // No duplicate checks for those who can throttle
979 if ( user_can( (int) $r['post_author'], 'throttle' ) ) {
980 return true;
981 }
982
983 // Get the DB
984 $bbp_db = bbp_db();
985
986 // Default clauses
987 $join = $where = '';
988
989 // Check for anonymous post
990 if ( empty( $r['post_author'] ) && ( ! empty( $r['anonymous_data'] ) && ! empty( $r['anonymous_data']['bbp_anonymous_email'] ) ) ) {
991
992 // Sanitize the email address for querying
993 $email = sanitize_email( $r['anonymous_data']['bbp_anonymous_email'] );
994
995 // Only proceed
996 if ( ! empty( $email ) && is_email( $email ) ) {
997
998 // Get the meta SQL
999 $clauses = get_meta_sql(
1000 array(
1001 array(
1002 'key' => '_bbp_anonymous_email',
1003 'value' => $email,
1004 )
1005 ),
1006 'post',
1007 $bbp_db->posts,
1008 'ID'
1009 );
1010
1011 // Set clauses
1012 $join = $clauses['join'];
1013
1014 // "'", "%", "$" and are valid characters in email addresses
1015 $where = $bbp_db->remove_placeholder_escape( $clauses['where'] );
1016 }
1017 }
1018
1019 // Unslash $r to pass through DB->prepare()
1020 //
1021 // @see: https://bbpress.trac.wordpress.org/ticket/2185/
1022 // @see: https://core.trac.wordpress.org/changeset/23973/
1023 $r = wp_unslash( $r );
1024
1025 // Prepare duplicate check query
1026 $query = "SELECT ID FROM {$bbp_db->posts} {$join}";
1027 $query .= $bbp_db->prepare('WHERE post_type = %s AND post_status != %s AND post_author = %d AND post_content = %s', $r['post_type'], $r['post_status'], $r['post_author'], $r['post_content'] );
1028 $query .= ! empty( $r['post_parent'] )
1029 ? $bbp_db->prepare( ' AND post_parent = %d', $r['post_parent'] )
1030 : '';
1031 $query .= $where;
1032 $query .= ' LIMIT 1';
1033 $dupe = apply_filters( 'bbp_check_for_duplicate_query', $query, $r );
1034
1035 // Dupe found
1036 if ( $bbp_db->get_var( $dupe ) ) {
1037 do_action( 'bbp_check_for_duplicate_trigger', $post_data );
1038 return false;
1039 }
1040
1041 // Dupe not found
1042 return true;
1043 }
1044
1045 /**
1046 * Check for flooding
1047 *
1048 * Check to make sure that a user is not making too many posts in a short amount
1049 * of time.
1050 *
1051 * @since 2.0.0 bbPress (r2734)
1052 *
1053 * @param array $anonymous_data Optional - if it's an anonymous post. Do not
1054 * supply if supplying $author_id. Should be
1055 * sanitized (see {@link bbp_filter_anonymous_post_data()}
1056 * @param int $author_id Optional. Supply if it's a post by a logged in user.
1057 * Do not supply if supplying $anonymous_data.
1058 * @return bool True if there is no flooding, false if there is
1059 */
1060 function bbp_check_for_flood( $anonymous_data = array(), $author_id = 0 ) {
1061
1062 // Allow for flood check to be skipped
1063 if ( apply_filters( 'bbp_bypass_check_for_flood', false, $anonymous_data, $author_id ) ) {
1064 return true;
1065 }
1066
1067 // Option disabled. No flood checks.
1068 $throttle_time = get_option( '_bbp_throttle_time' );
1069 if ( empty( $throttle_time ) || ! bbp_allow_content_throttle() ) {
1070 return true;
1071 }
1072
1073 // User is anonymous, so check a transient based on the IP
1074 if ( ! empty( $anonymous_data ) ) {
1075 $last_posted = get_transient( '_bbp_' . bbp_current_author_ip() . '_last_posted' );
1076
1077 if ( ! empty( $last_posted ) && ( time() < ( $last_posted + $throttle_time ) ) ) {
1078 return false;
1079 }
1080
1081 // User is logged in, so check their last posted time
1082 } elseif ( ! empty( $author_id ) ) {
1083 $author_id = (int) $author_id;
1084 $last_posted = bbp_get_user_last_posted( $author_id );
1085
1086 if ( ! empty( $last_posted ) && ( time() < ( $last_posted + $throttle_time ) ) && ! user_can( $author_id, 'throttle' ) ) {
1087 return false;
1088 }
1089 } else {
1090 return false;
1091 }
1092
1093 return true;
1094 }
1095
1096 /**
1097 * Checks topics and replies against the discussion moderation of blocked keys
1098 *
1099 * @since 2.1.0 bbPress (r3581)
1100 *
1101 * @param array $anonymous_data Optional - if it's an anonymous post. Do not
1102 * supply if supplying $author_id. Should be
1103 * sanitized (see {@link bbp_filter_anonymous_post_data()}
1104 * @param int $author_id Topic or reply author ID
1105 * @param string $title The title of the content
1106 * @param string $content The content being posted
1107 * @param mixed $strict False for moderation_keys. True for disallowed_keys.
1108 * String for custom keys.
1109 * @return bool True if test is passed, false if fail
1110 */
1111 function bbp_check_for_moderation( $anonymous_data = array(), $author_id = 0, $title = '', $content = '', $strict = false ) {
1112
1113 // Custom moderation option key
1114 if ( is_string( $strict ) ) {
1115 $strict = sanitize_key( $strict );
1116
1117 // Use custom key
1118 if ( ! empty( $strict ) ) {
1119 $hook_name = $strict;
1120 $option_name = "{$strict}_keys";
1121
1122 // Key was invalid, so default to moderation keys
1123 } else {
1124 $strict = false;
1125 }
1126 }
1127
1128 // Strict mode uses WordPress "blacklist" settings
1129 if ( true === $strict ) {
1130 $hook_name = 'blacklist';
1131 $option_name = 'disallowed_keys';
1132
1133 // Non-strict uses WordPress "moderation" settings
1134 } elseif ( false === $strict ) {
1135 $hook_name = 'moderation';
1136 $option_name = 'moderation_keys';
1137 }
1138
1139 // Allow for moderation check to be skipped
1140 if ( apply_filters( "bbp_bypass_check_for_{$hook_name}", false, $anonymous_data, $author_id, $title, $content, $strict ) ) {
1141 return true;
1142 }
1143
1144 // Maybe perform some author-specific capability checks
1145 if ( ! empty( $author_id ) ) {
1146
1147 // Bail if user is a keymaster
1148 if ( bbp_is_user_keymaster( $author_id ) ) {
1149 return true;
1150
1151 // Bail if user can moderate
1152 // https://bbpress.trac.wordpress.org/ticket/2726
1153 } elseif ( ( false === $strict ) && user_can( $author_id, 'moderate' ) ) {
1154 return true;
1155 }
1156 }
1157
1158 // Define local variable(s)
1159 $_post = array();
1160 $match_out = '';
1161
1162 /** Max Links *************************************************************/
1163
1164 // Only check max_links when not being strict
1165 if ( false === $strict ) {
1166 $max_links = get_option( 'comment_max_links' );
1167 if ( ! empty( $max_links ) ) {
1168
1169 // How many links?
1170 $num_links = preg_match_all( '/(http|ftp|https):\/\//i', $content, $match_out );
1171
1172 // Allow for bumping the max to include the user's URL
1173 if ( ! empty( $_post['url'] ) ) {
1174 $num_links = apply_filters( 'comment_max_links_url', $num_links, $_post['url'], $content );
1175 }
1176
1177 // Das ist zu viele links!
1178 if ( $num_links >= $max_links ) {
1179 return false;
1180 }
1181 }
1182 }
1183
1184 /** Moderation ************************************************************/
1185
1186 /**
1187 * Filters the bbPress moderation keys.
1188 *
1189 * @since 2.6.0 bbPress (r6855)
1190 *
1191 * @param string $moderation List of moderation keys. One per new line.
1192 */
1193 $moderation = apply_filters( "bbp_{$hook_name}_keys", trim( get_option( $option_name ) ) );
1194
1195 // Bail if no words to look for
1196 if ( empty( $moderation ) ) {
1197 return true;
1198 }
1199
1200 /** User Data *************************************************************/
1201
1202 // Map anonymous user data
1203 if ( ! empty( $anonymous_data ) ) {
1204 $_post['author'] = $anonymous_data['bbp_anonymous_name'];
1205 $_post['email'] = $anonymous_data['bbp_anonymous_email'];
1206 $_post['url'] = $anonymous_data['bbp_anonymous_website'];
1207
1208 // Map current user data
1209 } elseif ( ! empty( $author_id ) ) {
1210
1211 // Get author data
1212 $user = get_userdata( $author_id );
1213
1214 // If data exists, map it
1215 if ( ! empty( $user ) ) {
1216 $_post['author'] = $user->display_name;
1217 $_post['email'] = $user->user_email;
1218 $_post['url'] = $user->user_url;
1219 }
1220 }
1221
1222 // Current user IP and user agent
1223 $_post['user_ip'] = bbp_current_author_ip();
1224 $_post['user_ua'] = bbp_current_author_ua();
1225
1226 // Post title and content
1227 $_post['title'] = $title;
1228 $_post['content'] = $content;
1229
1230 // Ensure HTML tags are not being used to bypass the moderation list.
1231 $_post['comment_without_html'] = wp_strip_all_tags( $content );
1232
1233 /** Words *****************************************************************/
1234
1235 // Get words separated by new lines
1236 $words = explode( "\n", $moderation );
1237
1238 // Loop through words
1239 foreach ( (array) $words as $word ) {
1240
1241 // Trim the whitespace from the word
1242 $word = trim( $word );
1243
1244 // Skip empty lines
1245 if ( empty( $word ) ) {
1246 continue;
1247 }
1248
1249 // Do some escaping magic so that '#' chars in the
1250 // spam words don't break things:
1251 $word = preg_quote( $word, '#' );
1252 $pattern = "#{$word}#iu";
1253
1254 // Loop through post data
1255 foreach ( $_post as $post_data ) {
1256
1257 // Check each user data for current word
1258 if ( preg_match( $pattern, $post_data ) ) {
1259
1260 // Post does not pass
1261 return false;
1262 }
1263 }
1264 }
1265
1266 // Check passed successfully
1267 return true;
1268 }
1269
1270 /**
1271 * Deprecated. Use bbp_check_for_moderation() with strict flag set.
1272 *
1273 * @since 2.0.0 bbPress (r3447)
1274 * @since 2.6.0 bbPress (r6854)
1275 * @deprecated 2.6.0 Use bbp_check_for_moderation() with strict flag set
1276 */
1277 function bbp_check_for_blacklist( $anonymous_data = array(), $author_id = 0, $title = '', $content = '' ) {
1278 return bbp_check_for_moderation( $anonymous_data, $author_id, $title, $content, true );
1279 }
1280
1281 /** Subscriptions *************************************************************/
1282
1283 /**
1284 * Get the "Do Not Reply" email address to use when sending subscription emails.
1285 *
1286 * We make some educated guesses here based on the home URL. Filters are
1287 * available to customize this address further. In the future, we may consider
1288 * using `admin_email` instead, though this is not normally publicized.
1289 *
1290 * Use the parsed home URL host to preserve mapped domains without including
1291 * paths or relying on a request-supplied server name.
1292 *
1293 * @since 2.6.0 bbPress (r5409)
1294 *
1295 * @see wp_mail
1296 * @see wp_notify_postauthor
1297 * @link https://bbpress.trac.wordpress.org/ticket/2618
1298 *
1299 * @return string
1300 */
1301 function bbp_get_do_not_reply_address() {
1302 $sitename = wp_parse_url( home_url(), PHP_URL_HOST );
1303 if ( empty( $sitename ) ) {
1304 $sitename = wp_parse_url( site_url(), PHP_URL_HOST );
1305 }
1306 $sitename = strtolower( (string) $sitename );
1307 if ( substr( $sitename, 0, 4 ) === 'www.' ) {
1308 $sitename = substr( $sitename, 4 );
1309 }
1310
1311 // Filter & return
1312 return apply_filters( 'bbp_get_do_not_reply_address', 'noreply@' . $sitename );
1313 }
1314
1315 /**
1316 * Remove subscribers who cannot read notification content.
1317 *
1318 * Subscription relationships can outlive a user's access to a forum. Check
1319 * current access immediately before preparing a notification so restricted
1320 * content is not sent to former participants.
1321 *
1322 * @since 2.6.17 bbPress (r7494)
1323 *
1324 * @param array $user_ids Subscriber user IDs.
1325 * @param int $forum_id Forum ID.
1326 * @param int $topic_id Topic ID.
1327 * @param int $reply_id Reply ID.
1328 * @return array User IDs that can read the notification content.
1329 */
1330 function bbp_filter_subscription_user_ids( $user_ids = array(), $forum_id = 0, $topic_id = 0, $reply_id = 0 ) {
1331 $forum_id = bbp_get_forum_id( $forum_id );
1332 $topic_id = bbp_get_topic_id( $topic_id );
1333 $reply_id = bbp_get_reply_id( $reply_id );
1334
1335 foreach ( $user_ids as $key => $user_id ) {
1336 $can_view_forum = user_can( $user_id, 'read_forum', $forum_id );
1337
1338 /**
1339 * Filters whether a subscription recipient can view a forum.
1340 *
1341 * @since 2.6.17 bbPress (r7494)
1342 *
1343 * @param bool $can_view Whether the user can view the forum.
1344 * @param int $user_id User ID.
1345 * @param int $forum_id Forum ID.
1346 * @param int $topic_id Topic ID.
1347 * @param int $reply_id Reply ID.
1348 */
1349 $can_view_forum = (bool) apply_filters( 'bbp_subscription_user_can_view_forum', $can_view_forum, $user_id, $forum_id, $topic_id, $reply_id );
1350
1351 $can_view = user_can( $user_id, 'spectate' ) && $can_view_forum;
1352
1353 if ( ! empty( $topic_id ) ) {
1354 $can_view = $can_view && user_can( $user_id, 'read_topic', $topic_id );
1355 }
1356
1357 if ( ! empty( $reply_id ) ) {
1358 $can_view = $can_view && user_can( $user_id, 'read_reply', $reply_id );
1359 }
1360
1361 if ( false === $can_view ) {
1362 unset( $user_ids[ $key ] );
1363 }
1364 }
1365
1366 return $user_ids;
1367 }
1368
1369 /**
1370 * Sends notification emails for new replies to subscribed topics
1371 *
1372 * Gets new post ID and check if there are subscribed users to that topic, and
1373 * if there are, send notifications
1374 *
1375 * Note: in bbPress 2.6, we've moved away from 1 email per subscriber to 1 email
1376 * with everyone BCC'd. This may have negative repercussions for email services
1377 * that limit the number of addresses in a BCC field (often to around 500.) In
1378 * those cases, we recommend unhooking this function and creating your own
1379 * custom email script.
1380 *
1381 * @since 2.6.0 bbPress (r5413)
1382 *
1383 * @param int $reply_id ID of the newly made reply
1384 * @param int $topic_id ID of the topic of the reply
1385 * @param int $forum_id ID of the forum of the reply
1386 * @param array $anonymous_data Optional - if it's an anonymous post. Do not
1387 * supply if supplying $author_id. Should be
1388 * sanitized (see {@link bbp_filter_anonymous_post_data()}
1389 * @param int $reply_author ID of the topic author ID
1390 * @return bool True on success, false on failure
1391 */
1392 function bbp_notify_topic_subscribers( $reply_id = 0, $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $reply_author = 0 ) {
1393
1394 // Bail if subscriptions are turned off
1395 if ( ! bbp_is_subscriptions_active() ) {
1396 return false;
1397 }
1398
1399 // Bail if importing
1400 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
1401 return false;
1402 }
1403
1404 /** Validation ************************************************************/
1405
1406 $reply_id = bbp_get_reply_id( $reply_id );
1407 $topic_id = bbp_get_topic_id( $topic_id );
1408 $forum_id = bbp_get_forum_id( $forum_id );
1409 $password_protected = bbp_is_password_protected( $reply_id );
1410
1411 /** Topic *****************************************************************/
1412
1413 // Bail if topic is not public (includes closed)
1414 if ( ! bbp_is_topic_public( $topic_id ) ) {
1415 return false;
1416 }
1417
1418 /** Reply *****************************************************************/
1419
1420 // Bail if reply is not published
1421 if ( ! bbp_is_reply_published( $reply_id ) ) {
1422 return false;
1423 }
1424
1425 // Poster name
1426 $reply_author_name = bbp_get_reply_author_display_name( $reply_id );
1427
1428 /** Users *****************************************************************/
1429
1430 // Get topic subscribers and bail if empty
1431 $user_ids = bbp_get_subscribers( $topic_id );
1432
1433 // Remove the reply author from the list.
1434 $reply_author_key = array_search( (int) $reply_author, $user_ids, true );
1435 if ( false !== $reply_author_key ) {
1436 unset( $user_ids[ $reply_author_key ] );
1437 }
1438
1439 // Dedicated filter to manipulate user ID's to send emails to
1440 $user_ids = (array) apply_filters( 'bbp_topic_subscription_user_ids', $user_ids, $reply_id, $topic_id );
1441
1442 // Remove subscribers who cannot read the notification content
1443 $user_ids = bbp_filter_subscription_user_ids( $user_ids, $forum_id, $topic_id, $reply_id );
1444
1445 // Bail of the reply author was the only one subscribed.
1446 if ( empty( $user_ids ) ) {
1447 return false;
1448 }
1449
1450 // Get email addresses, bail if empty
1451 $email_addresses = bbp_get_email_addresses_from_user_ids( $user_ids );
1452 if ( empty( $email_addresses ) ) {
1453 return false;
1454 }
1455
1456 /** Mail ******************************************************************/
1457
1458 // Remove filters from reply content and topic title to prevent content
1459 // from being encoded with HTML entities, wrapped in paragraph tags, etc...
1460 bbp_remove_all_filters( 'bbp_get_reply_content' );
1461 bbp_remove_all_filters( 'bbp_get_topic_title' );
1462 bbp_remove_all_filters( 'the_title' );
1463
1464 // Strip tags from text and setup mail data
1465 $forum_title = wp_specialchars_decode( strip_tags( bbp_get_forum_title( $forum_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1466 $topic_title = wp_specialchars_decode( strip_tags( bbp_get_topic_title( $topic_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1467 $reply_url = bbp_get_reply_url( $reply_id );
1468
1469 // Do not include protected content in subscription emails
1470 if ( ! empty( $password_protected ) ) {
1471 $message = sprintf(
1472
1473 /* translators: %s: Reply URL */
1474 esc_html__(
1475 'A new reply was posted in a password-protected discussion.
1476
1477 Post Link: %s
1478
1479 -----------
1480
1481 You are receiving this email because you subscribed to a forum topic.
1482
1483 Login and visit the topic to unsubscribe from these emails.',
1484 'bbpress'
1485 ),
1486 $reply_url
1487 );
1488
1489 // Include the reply details in normal subscription emails
1490 } else {
1491 $reply_author_name = wp_specialchars_decode( strip_tags( $reply_author_name ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1492 $reply_content = wp_specialchars_decode( strip_tags( bbp_get_reply_content( $reply_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1493 $message = sprintf(
1494
1495 /* translators: 1: Reply author name, 2: Reply content, 3: Reply URL */
1496 esc_html__(
1497 '%1$s wrote:
1498
1499 %2$s
1500
1501 Post Link: %3$s
1502
1503 -----------
1504
1505 You are receiving this email because you subscribed to a forum topic.
1506
1507 Login and visit the topic to unsubscribe from these emails.',
1508 'bbpress'
1509 ),
1510 $reply_author_name,
1511 $reply_content,
1512 $reply_url
1513 );
1514 }
1515
1516 // For plugins to filter messages per reply/topic/user
1517 $message = apply_filters( 'bbp_subscription_mail_message', $message, $reply_id, $topic_id );
1518 if ( empty( $message ) ) {
1519 bbp_restore_all_filters( 'bbp_get_reply_content' );
1520 bbp_restore_all_filters( 'bbp_get_topic_title' );
1521 bbp_restore_all_filters( 'the_title' );
1522 return;
1523 }
1524
1525 // For plugins to filter titles per reply/topic/user
1526 $subject = apply_filters( 'bbp_subscription_mail_title', '[' . $forum_title . '] ' . $topic_title, $reply_id, $topic_id );
1527 if ( empty( $subject ) ) {
1528 bbp_restore_all_filters( 'bbp_get_reply_content' );
1529 bbp_restore_all_filters( 'bbp_get_topic_title' );
1530 bbp_restore_all_filters( 'the_title' );
1531 return;
1532 }
1533
1534 /** Headers ***************************************************************/
1535
1536 // Default bbPress X-header
1537 $headers = array( bbp_get_email_header() );
1538
1539 // Get the noreply@ address
1540 $no_reply = bbp_get_do_not_reply_address();
1541
1542 // Setup "From" email address
1543 $from_email = apply_filters( 'bbp_subscription_from_email', $no_reply );
1544
1545 // Setup the From header
1546 $headers[] = 'From: ' . get_bloginfo( 'name' ) . ' <' . $from_email . '>';
1547
1548 // Loop through addresses
1549 foreach ( (array) $email_addresses as $address ) {
1550 $headers[] = 'Bcc: ' . $address;
1551 }
1552
1553 /** Send it ***************************************************************/
1554
1555 // Custom headers
1556 $headers = apply_filters( 'bbp_subscription_mail_headers', $headers );
1557 $to_email = apply_filters( 'bbp_subscription_to_email', $no_reply );
1558
1559 // Before
1560 do_action( 'bbp_pre_notify_subscribers', $reply_id, $topic_id, $user_ids );
1561
1562 // Send notification email
1563 wp_mail( $to_email, $subject, $message, $headers );
1564
1565 // After
1566 do_action( 'bbp_post_notify_subscribers', $reply_id, $topic_id, $user_ids );
1567
1568 // Restore previously removed filters
1569 bbp_restore_all_filters( 'bbp_get_reply_content' );
1570 bbp_restore_all_filters( 'bbp_get_topic_title' );
1571 bbp_restore_all_filters( 'the_title' );
1572
1573 return true;
1574 }
1575
1576 /**
1577 * Sends notification emails for new topics to subscribed forums
1578 *
1579 * Gets new post ID and check if there are subscribed users to that forum, and
1580 * if there are, send notifications
1581 *
1582 * Note: in bbPress 2.6, we've moved away from 1 email per subscriber to 1 email
1583 * with everyone BCC'd. This may have negative repercussions for email services
1584 * that limit the number of addresses in a BCC field (often to around 500.) In
1585 * those cases, we recommend unhooking this function and creating your own
1586 * custom email script.
1587 *
1588 * @since 2.5.0 bbPress (r5156)
1589 *
1590 * @param int $topic_id ID of the newly made reply
1591 * @param int $forum_id ID of the forum for the topic
1592 * @param array $anonymous_data Optional - if it's an anonymous post. Do not
1593 * supply if supplying $author_id. Should be
1594 * sanitized (see {@link bbp_filter_anonymous_post_data()}
1595 * @param int $topic_author ID of the topic author ID
1596 * @return bool True on success, false on failure
1597 */
1598 function bbp_notify_forum_subscribers( $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $topic_author = 0 ) {
1599
1600 // Bail if subscriptions are turned off
1601 if ( ! bbp_is_subscriptions_active() ) {
1602 return false;
1603 }
1604
1605 // Bail if importing
1606 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
1607 return false;
1608 }
1609
1610 /** Validation ************************************************************/
1611
1612 $topic_id = bbp_get_topic_id( $topic_id );
1613 $forum_id = bbp_get_forum_id( $forum_id );
1614 $password_protected = bbp_is_password_protected( $topic_id );
1615
1616 /**
1617 * Necessary for backwards compatibility
1618 *
1619 * @see https://bbpress.trac.wordpress.org/ticket/2620
1620 */
1621 $user_id = 0;
1622
1623 /** Topic *****************************************************************/
1624
1625 // Bail if topic is not public (includes closed)
1626 if ( ! bbp_is_topic_public( $topic_id ) ) {
1627 return false;
1628 }
1629
1630 // Poster name
1631 $topic_author_name = bbp_get_topic_author_display_name( $topic_id );
1632
1633 /** Users *****************************************************************/
1634
1635 // Get topic subscribers and bail if empty
1636 $user_ids = bbp_get_subscribers( $forum_id );
1637
1638 // Remove the topic author from the list.
1639 $topic_author_key = array_search( (int) $topic_author, $user_ids, true );
1640 if ( false !== $topic_author_key ) {
1641 unset( $user_ids[ $topic_author_key ] );
1642 }
1643
1644 // Dedicated filter to manipulate user ID's to send emails to
1645 $user_ids = (array) apply_filters( 'bbp_forum_subscription_user_ids', $user_ids, $topic_id, $forum_id );
1646
1647 // Remove subscribers who cannot read the notification content
1648 $user_ids = bbp_filter_subscription_user_ids( $user_ids, $forum_id, $topic_id );
1649
1650 // Bail of the reply author was the only one subscribed.
1651 if ( empty( $user_ids ) ) {
1652 return false;
1653 }
1654
1655 // Get email addresses, bail if empty
1656 $email_addresses = bbp_get_email_addresses_from_user_ids( $user_ids );
1657 if ( empty( $email_addresses ) ) {
1658 return false;
1659 }
1660
1661 /** Mail ******************************************************************/
1662
1663 // Remove filters from reply content and topic title to prevent content
1664 // from being encoded with HTML entities, wrapped in paragraph tags, etc...
1665 bbp_remove_all_filters( 'bbp_get_topic_content' );
1666 bbp_remove_all_filters( 'bbp_get_topic_title' );
1667 bbp_remove_all_filters( 'the_title' );
1668
1669 // Strip tags from text and setup mail data
1670 $forum_title = wp_specialchars_decode( strip_tags( bbp_get_forum_title( $forum_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1671 $topic_title = wp_specialchars_decode( strip_tags( bbp_get_topic_title( $topic_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1672 $topic_url = get_permalink( $topic_id );
1673
1674 // Do not include protected content in subscription emails
1675 if ( ! empty( $password_protected ) ) {
1676 $message = sprintf(
1677
1678 /* translators: %s: Topic URL */
1679 esc_html__(
1680 'A new topic was posted in a password-protected discussion.
1681
1682 Topic Link: %s
1683
1684 -----------
1685
1686 You are receiving this email because you subscribed to a forum.
1687
1688 Login and visit the topic to unsubscribe from these emails.',
1689 'bbpress'
1690 ),
1691 $topic_url
1692 );
1693
1694 // Include the topic details in normal subscription emails
1695 } else {
1696 $topic_author_name = wp_specialchars_decode( strip_tags( $topic_author_name ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1697 $topic_content = wp_specialchars_decode( strip_tags( bbp_get_topic_content( $topic_id ) ), ENT_QUOTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
1698 $message = sprintf(
1699
1700 /* translators: 1: Topic author name, 2: Topic content, 3: Topic URL */
1701 esc_html__(
1702 '%1$s wrote:
1703
1704 %2$s
1705
1706 Topic Link: %3$s
1707
1708 -----------
1709
1710 You are receiving this email because you subscribed to a forum.
1711
1712 Login and visit the topic to unsubscribe from these emails.',
1713 'bbpress'
1714 ),
1715 $topic_author_name,
1716 $topic_content,
1717 $topic_url
1718 );
1719 }
1720
1721 // For plugins to filter messages per reply/topic/user
1722 $message = apply_filters( 'bbp_forum_subscription_mail_message', $message, $topic_id, $forum_id, $user_id );
1723 if ( empty( $message ) ) {
1724 bbp_restore_all_filters( 'bbp_get_topic_content' );
1725 bbp_restore_all_filters( 'bbp_get_topic_title' );
1726 bbp_restore_all_filters( 'the_title' );
1727 return;
1728 }
1729
1730 // For plugins to filter titles per reply/topic/user
1731 $subject = apply_filters( 'bbp_forum_subscription_mail_title', '[' . $forum_title . '] ' . $topic_title, $topic_id, $forum_id, $user_id );
1732 if ( empty( $subject ) ) {
1733 bbp_restore_all_filters( 'bbp_get_topic_content' );
1734 bbp_restore_all_filters( 'bbp_get_topic_title' );
1735 bbp_restore_all_filters( 'the_title' );
1736 return;
1737 }
1738
1739 /** Headers ***************************************************************/
1740
1741 // Default bbPress X-header
1742 $headers = array( bbp_get_email_header() );
1743
1744 // Get the noreply@ address
1745 $no_reply = bbp_get_do_not_reply_address();
1746
1747 // Setup "From" email address
1748 $from_email = apply_filters( 'bbp_subscription_from_email', $no_reply );
1749
1750 // Setup the From header
1751 $headers[] = 'From: ' . get_bloginfo( 'name' ) . ' <' . $from_email . '>';
1752
1753 // Loop through addresses
1754 foreach ( (array) $email_addresses as $address ) {
1755 $headers[] = 'Bcc: ' . $address;
1756 }
1757
1758 /** Send it ***************************************************************/
1759
1760 // Custom headers
1761 $headers = apply_filters( 'bbp_subscription_mail_headers', $headers );
1762 $to_email = apply_filters( 'bbp_subscription_to_email', $no_reply );
1763
1764 // Before
1765 do_action( 'bbp_pre_notify_forum_subscribers', $topic_id, $forum_id, $user_ids );
1766
1767 // Send notification email
1768 wp_mail( $to_email, $subject, $message, $headers );
1769
1770 // After
1771 do_action( 'bbp_post_notify_forum_subscribers', $topic_id, $forum_id, $user_ids );
1772
1773 // Restore previously removed filters
1774 bbp_restore_all_filters( 'bbp_get_topic_content' );
1775 bbp_restore_all_filters( 'bbp_get_topic_title' );
1776 bbp_restore_all_filters( 'the_title' );
1777
1778 return true;
1779 }
1780
1781 /**
1782 * Sends notification emails for new replies to subscribed topics
1783 *
1784 * This function is deprecated. Please use: bbp_notify_topic_subscribers()
1785 *
1786 * @since 2.0.0 bbPress (r2668)
1787 *
1788 * @deprecated 2.6.0 bbPress (r5412)
1789 *
1790 * @param int $reply_id ID of the newly made reply
1791 * @param int $topic_id ID of the topic of the reply
1792 * @param int $forum_id ID of the forum of the reply
1793 * @param array $anonymous_data Optional - if it's an anonymous post. Do not
1794 * supply if supplying $author_id. Should be
1795 * sanitized (see {@link bbp_filter_anonymous_post_data()}
1796 * @param int $reply_author ID of the topic author ID
1797 *
1798 * @return bool True on success, false on failure
1799 */
1800 function bbp_notify_subscribers( $reply_id = 0, $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $reply_author = 0 ) {
1801 return bbp_notify_topic_subscribers( $reply_id, $topic_id, $forum_id, $anonymous_data, $reply_author );
1802 }
1803
1804 /**
1805 * Return an array of user email addresses from an array of user IDs
1806 *
1807 * @since 2.6.0 bbPress (r6725)
1808 *
1809 * @param array $user_ids
1810 * @return array
1811 */
1812 function bbp_get_email_addresses_from_user_ids( $user_ids = array() ) {
1813
1814 // Default return value
1815 $retval = array();
1816
1817 // Maximum number of users to get per database query
1818 $limit = apply_filters( 'bbp_get_users_chunk_limit', 100 );
1819
1820 // Only do the work if there are user IDs to query for
1821 if ( ! empty( $user_ids ) ) {
1822
1823 // Get total number of sets
1824 $steps = ceil( count( $user_ids ) / $limit );
1825 $range = array_map( 'intval', range( 1, $steps ) );
1826
1827 // Loop through users
1828 foreach ( $range as $loop ) {
1829
1830 // Initial loop has no offset
1831 $offset = $limit * ( $loop - 1 );
1832
1833 // Calculate user IDs to include
1834 $loop_ids = array_slice( $user_ids, $offset, $limit );
1835
1836 // Skip if something went wrong
1837 if ( empty( $loop_ids ) ) {
1838 continue;
1839 }
1840
1841 // Call get_users() in a way that users are cached
1842 $loop_users = get_users(
1843 array(
1844 'blog_id' => 0,
1845 'fields' => 'all_with_meta',
1846 'include' => $loop_ids
1847 )
1848 );
1849
1850 // Pluck emails from users
1851 $loop_emails = wp_list_pluck( $loop_users, 'user_email' );
1852
1853 // Clean-up memory, for big user sets
1854 unset( $loop_users );
1855
1856 // Merge users into return value
1857 if ( ! empty( $loop_emails ) ) {
1858 $retval = array_merge( $retval, $loop_emails );
1859 }
1860 }
1861
1862 // No duplicates
1863 $retval = bbp_get_unique_array_values( $retval );
1864 }
1865
1866 // Filter & return
1867 return apply_filters( 'bbp_get_email_addresses_from_user_ids', $retval, $user_ids, $limit );
1868 }
1869
1870 /**
1871 * Automatically splits bbPress emails with many Bcc recipients into chunks.
1872 *
1873 * This middleware is useful because topics and forums with many subscribers
1874 * run into problems with Bcc limits, and many hosting companies & third-party
1875 * services limit the size of a Bcc audience to prevent spamming.
1876 *
1877 * The default "chunk" size is 40 users per iteration, and can be filtered if
1878 * desired. A future version of bbPress will introduce a setting to more easily
1879 * tune this.
1880 *
1881 * @since 2.6.0 bbPress (r6919)
1882 *
1883 * @param array $args Original arguments passed to wp_mail().
1884 * @return array
1885 */
1886 function bbp_chunk_emails( $args = array() ) {
1887
1888 // Get the maximum number of Bcc's per chunk
1889 $max_num = apply_filters( 'bbp_get_bcc_chunk_limit', 40, $args );
1890
1891 // Look for "bcc: " in a case-insensitive way, and split into 2 sets
1892 $match = '/^bcc: (\w+)/i';
1893 $old_headers = preg_grep( $match, $args['headers'], PREG_GREP_INVERT );
1894 $bcc_headers = preg_grep( $match, $args['headers'] );
1895
1896 // Bail if less than $max_num recipients
1897 if ( empty( $bcc_headers ) || ( count( $bcc_headers ) < $max_num ) ) {
1898 return $args;
1899 }
1900
1901 // Reindex the headers arrays
1902 $old_headers = array_values( $old_headers );
1903 $bcc_headers = array_values( $bcc_headers );
1904
1905 // Break the Bcc emails into chunks
1906 foreach ( array_chunk( $bcc_headers, $max_num ) as $i => $chunk ) {
1907
1908 // Skip the first chunk (it will get used in the original wp_mail() call)
1909 if ( 0 === $i ) {
1910 $first_chunk = $chunk;
1911 continue;
1912 }
1913
1914 // Send out the chunk
1915 $chunk_headers = array_merge( $old_headers, $chunk );
1916
1917 // Recursion alert, but should be OK!
1918 wp_mail(
1919 $args['to'],
1920 $args['subject'],
1921 $args['message'],
1922 $chunk_headers,
1923 $args['attachments']
1924 );
1925 }
1926
1927 // Set headers to old headers + the $first_chunk of Bcc's
1928 $args['headers'] = array_merge( $old_headers, $first_chunk );
1929
1930 // Return the reduced args, with the first chunk of Bcc's
1931 return $args;
1932 }
1933
1934 /**
1935 * Return the string used for the bbPress specific X-header.
1936 *
1937 * @since 2.6.0 bbPress (r6919)
1938 *
1939 * @return string
1940 */
1941 function bbp_get_email_header() {
1942 return apply_filters( 'bbp_get_email_header', 'X-bbPress: ' . bbp_get_version() );
1943 }
1944
1945 /** Login *********************************************************************/
1946
1947 /**
1948 * Return a clean and reliable logout URL
1949 *
1950 * This function is used to filter `logout_url`. If no $redirect_to value is
1951 * passed, it will default to the request uri, then the forum root.
1952 *
1953 * See: `wp_logout_url()`
1954 *
1955 * @since 2.0.0 bbPress (r2815)
1956 *
1957 * @param string $url URL used to log out
1958 * @param string $redirect_to Where to redirect to?
1959 *
1960 * @return string The url
1961 */
1962 function bbp_logout_url( $url = '', $redirect_to = '' ) {
1963
1964 // If there is no redirect in the URL, let's add one...
1965 if ( ! strstr( $url, 'redirect_to' ) ) {
1966
1967 // Get the forum root, to maybe use as a default
1968 $forum_root = bbp_get_root_url();
1969
1970 // No redirect passed, so check referer and fallback to request uri
1971 if ( empty( $redirect_to ) ) {
1972
1973 // Check for a valid referer
1974 $redirect_to = wp_get_referer();
1975
1976 // Fallback to request uri if invalid referer
1977 if ( false === $redirect_to ) {
1978 $redirect_to = bbp_get_url_scheme() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
1979 }
1980 }
1981
1982 // Filter the $redirect_to destination
1983 $filtered = apply_filters( 'bbp_logout_url_redirect_to', $redirect_to );
1984
1985 // Validate $redirect_to, default to root
1986 $validated = wp_validate_redirect( $filtered, $forum_root );
1987
1988 // Assemble $redirect_to and add it (encoded) to full $url
1989 $appended = add_query_arg( array( 'loggedout' => 'true' ), $validated );
1990 $encoded = urlencode( $appended );
1991 $url = add_query_arg( array( 'redirect_to' => $encoded ), $url );
1992 }
1993
1994 // Filter & return
1995 return apply_filters( 'bbp_logout_url', $url, $redirect_to );
1996 }
1997
1998 /** Queries *******************************************************************/
1999
2000 /**
2001 * Merge user defined arguments into defaults array.
2002 *
2003 * This function is used throughout bbPress to allow for either a string or array
2004 * to be merged into another array. It is identical to wp_parse_args() except
2005 * it allows for arguments to be passively or aggressively filtered using the
2006 * optional $filter_key parameter.
2007 *
2008 * @since 2.1.0 bbPress (r3840)
2009 *
2010 * @param string|array $args Value to merge with $defaults
2011 * @param array $defaults Array that serves as the defaults.
2012 * @param string $filter_key String to key the filters from
2013 * @return array Merged user defined values with defaults.
2014 */
2015 function bbp_parse_args( $args, $defaults = array(), $filter_key = '' ) {
2016
2017 // Setup a temporary array from $args
2018 if ( is_object( $args ) ) {
2019 $r = get_object_vars( $args );
2020 } elseif ( is_array( $args ) ) {
2021 $r =& $args;
2022 } else {
2023 wp_parse_str( $args, $r );
2024 }
2025
2026 // Passively filter the args before the parse
2027 if ( ! empty( $filter_key ) ) {
2028 $r = apply_filters( "bbp_before_{$filter_key}_parse_args", $r, $args, $defaults );
2029 }
2030
2031 // Parse
2032 if ( is_array( $defaults ) && ! empty( $defaults ) ) {
2033 $r = array_merge( $defaults, $r );
2034 }
2035
2036 // Aggressively filter the args after the parse
2037 if ( ! empty( $filter_key ) ) {
2038 $r = apply_filters( "bbp_after_{$filter_key}_parse_args", $r, $args, $defaults );
2039 }
2040
2041 // Return the parsed results
2042 return $r;
2043 }
2044
2045 /**
2046 * Adds ability to include or exclude specific post_parent ID's
2047 *
2048 * @since 2.0.0 bbPress (r2997)
2049 *
2050 * @deprecated 2.5.8 bbPress (r5814)
2051 *
2052 * @global WP $wp
2053 * @param string $where
2054 * @param WP_Query $object
2055 * @return string
2056 */
2057 function bbp_query_post_parent__in( $where, $object = '' ) {
2058 global $wp;
2059
2060 // Noop if WP core supports this already
2061 if ( in_array( 'post_parent__in', $wp->private_query_vars, true ) ) {
2062 return $where;
2063 }
2064
2065 // Bail if no object passed
2066 if ( empty( $object ) ) {
2067 return $where;
2068 }
2069
2070 // Only 1 post_parent so return $where
2071 if ( is_numeric( $object->query_vars['post_parent'] ) ) {
2072 return $where;
2073 }
2074
2075 // Get the DB
2076 $bbp_db = bbp_db();
2077
2078 // Including specific post_parent's
2079 if ( ! empty( $object->query_vars['post_parent__in'] ) ) {
2080 $ids = implode( ',', wp_parse_id_list( $object->query_vars['post_parent__in'] ) );
2081 $where .= " AND {$bbp_db->posts}.post_parent IN ($ids)";
2082
2083 // Excluding specific post_parent's
2084 } elseif ( ! empty( $object->query_vars['post_parent__not_in'] ) ) {
2085 $ids = implode( ',', wp_parse_id_list( $object->query_vars['post_parent__not_in'] ) );
2086 $where .= " AND {$bbp_db->posts}.post_parent NOT IN ($ids)";
2087 }
2088
2089 // Return possibly modified $where
2090 return $where;
2091 }
2092
2093 /**
2094 * Query the DB and get the last public post_id that has parent_id as post_parent
2095 *
2096 * @since 2.0.0 bbPress (r2868)
2097 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects
2098 *
2099 * @param int $parent_id Parent id.
2100 * @param string $post_type Post type. Defaults to 'post'.
2101 * @return int The last active post_id
2102 */
2103 function bbp_get_public_child_last_id( $parent_id = 0, $post_type = 'post' ) {
2104
2105 // Bail if nothing passed
2106 if ( empty( $parent_id ) ) {
2107 return false;
2108 }
2109
2110 // Which statuses
2111 switch ( $post_type ) {
2112
2113 // Forum
2114 case bbp_get_forum_post_type() :
2115 $post_status = bbp_get_public_forum_statuses();
2116 break;
2117
2118 // Topic
2119 case bbp_get_topic_post_type() :
2120 $post_status = bbp_get_public_topic_statuses();
2121 break;
2122
2123 // Reply
2124 case bbp_get_reply_post_type() :
2125 default :
2126 $post_status = bbp_get_public_reply_statuses();
2127 break;
2128 }
2129
2130 $query = new WP_Query(
2131 array(
2132 'fields' => 'ids',
2133 'post_parent' => $parent_id,
2134 'post_status' => $post_status,
2135 'post_type' => $post_type,
2136 'posts_per_page' => 1,
2137 'orderby' => array(
2138 'post_date' => 'DESC',
2139 'ID' => 'DESC'
2140 ),
2141
2142 // Performance
2143 'suppress_filters' => true,
2144 'update_post_term_cache' => false,
2145 'update_post_meta_cache' => false,
2146 'ignore_sticky_posts' => true,
2147 'no_found_rows' => true
2148 )
2149 );
2150 $child_id = array_shift( $query->posts );
2151 unset( $query );
2152
2153 // Filter & return
2154 return (int) apply_filters( 'bbp_get_public_child_last_id', $child_id, $parent_id, $post_type );
2155 }
2156
2157 /**
2158 * Query the database for child counts, grouped by type & status
2159 *
2160 * @since 2.6.0 bbPress (r6827)
2161 *
2162 * @param int $parent_id
2163 */
2164 function bbp_get_child_counts( $parent_id = 0 ) {
2165
2166 // Create cache key
2167 $parent_id = absint( $parent_id );
2168 $key = md5(
2169 serialize(
2170 array(
2171 'parent_id' => $parent_id,
2172 'post_type' => bbp_get_post_types()
2173 )
2174 )
2175 );
2176 $last_changed = wp_cache_get_last_changed( 'bbpress_posts' );
2177 $cache_key = "bbp_child_counts:{$key}:{$last_changed}";
2178
2179 // Check for cache and set if needed
2180 $retval = wp_cache_get( $cache_key, 'bbpress_posts' );
2181 if ( false === $retval ) {
2182
2183 // Setup the DB & query
2184 $bbp_db = bbp_db();
2185 $sql = "SELECT
2186 p.post_type AS type,
2187 p.post_status AS status,
2188 COUNT( * ) AS count
2189 FROM {$bbp_db->posts} AS p
2190 LEFT JOIN {$bbp_db->postmeta} AS pm
2191 ON p.ID = pm.post_id
2192 AND pm.meta_key = %s
2193 WHERE pm.meta_value = %s
2194 GROUP BY p.post_status, p.post_type";
2195
2196 // Get prepare vars
2197 $post_type = get_post_type( $parent_id );
2198 $meta_key = "_bbp_{$post_type}_id";
2199
2200 // Prepare & get results
2201 $query = $bbp_db->prepare( $sql, $meta_key, $parent_id );
2202 $results = $bbp_db->get_results( $query, ARRAY_A );
2203
2204 // Setup return value
2205 $retval = wp_list_pluck( $results, 'type', 'type' );
2206 $statuses = get_post_stati();
2207
2208 // Loop through results
2209 foreach ( $results as $row ) {
2210
2211 // Setup empties
2212 if ( ! is_array( $retval[ $row['type'] ] ) ) {
2213 $retval[ $row['type'] ] = array_fill_keys( $statuses, 0 );
2214 }
2215
2216 // Set statuses
2217 $retval[ $row['type'] ][ $row['status'] ] = bbp_number_not_negative( $row['count'] );
2218 }
2219
2220 // Always cache the results
2221 wp_cache_set( $cache_key, $retval, 'bbpress_posts' );
2222 }
2223
2224 // Make sure results are INTs
2225 return (array) apply_filters( 'bbp_get_child_counts', $retval, $parent_id );
2226 }
2227
2228 /**
2229 * Filter a list of child counts, from `bbp_get_child_counts()`
2230 *
2231 * @since 2.6.0 bbPress (r6827)
2232 *
2233 * @param int $parent_id ID of post to get child counts from
2234 * @param array $types Optional. An array of post types to filter by
2235 * @param array $statuses Optional. An array of post statuses to filter by
2236 *
2237 * @return array A list of objects or object fields.
2238 */
2239 function bbp_filter_child_counts_list( $parent_id = 0, $types = array( 'post' ), $statuses = array() ) {
2240
2241 // Setup local vars
2242 $retval = array();
2243 $types = array_flip( (array) $types );
2244 $statuses = array_flip( (array) $statuses );
2245 $counts = bbp_get_child_counts( $parent_id );
2246
2247 // Loop through counts by type
2248 foreach ( $counts as $type => $type_counts ) {
2249
2250 // Skip if not this type
2251 if ( ! isset( $types[ $type ] ) ) {
2252 continue;
2253 }
2254
2255 // Maybe filter statuses
2256 if ( ! empty( $statuses ) ) {
2257 $type_counts = array_intersect_key( $type_counts, $statuses );
2258 }
2259
2260 // Add type counts to return array
2261 $retval[ $type ] = $type_counts;
2262 }
2263
2264 // Filter & return
2265 return (array) apply_filters( 'bbp_filter_child_counts_list', $retval, $parent_id, $types, $statuses );
2266 }
2267
2268 /**
2269 * Query the DB and get a count of public children
2270 *
2271 * @since 2.0.0 bbPress (r2868)
2272 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects
2273 *
2274 * @param int $parent_id Parent id.
2275 * @param string $post_type Post type. Defaults to 'post'.
2276 * @return int The number of children
2277 */
2278 function bbp_get_public_child_count( $parent_id = 0, $post_type = 'post' ) {
2279
2280 // Bail if nothing passed
2281 if ( empty( $post_type ) ) {
2282 return false;
2283 }
2284
2285 // Which statuses
2286 switch ( $post_type ) {
2287
2288 // Forum
2289 case bbp_get_forum_post_type() :
2290 $post_status = bbp_get_public_forum_statuses();
2291 break;
2292
2293 // Topic
2294 case bbp_get_topic_post_type() :
2295 $post_status = bbp_get_public_topic_statuses();
2296 break;
2297
2298 // Reply
2299 case bbp_get_reply_post_type() :
2300 default :
2301 $post_status = bbp_get_public_reply_statuses();
2302 break;
2303 }
2304
2305 // Get counts
2306 $counts = bbp_filter_child_counts_list( $parent_id, $post_type, $post_status );
2307 $child_count = isset( $counts[ $post_type ] )
2308 ? bbp_number_not_negative( array_sum( array_values( $counts[ $post_type ] ) ) )
2309 : 0;
2310
2311 // Filter & return
2312 return (int) apply_filters( 'bbp_get_public_child_count', $child_count, $parent_id, $post_type );
2313 }
2314 /**
2315 * Query the DB and get a count of public children
2316 *
2317 * @since 2.6.0 bbPress (r6827)
2318 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects
2319 *
2320 * @param int $parent_id Parent id.
2321 * @param string $post_type Post type. Defaults to 'post'.
2322 * @return int The number of children
2323 */
2324 function bbp_get_non_public_child_count( $parent_id = 0, $post_type = 'post' ) {
2325
2326 // Bail if nothing passed
2327 if ( empty( $parent_id ) || empty( $post_type ) ) {
2328 return false;
2329 }
2330
2331 // Which statuses
2332 switch ( $post_type ) {
2333
2334 // Forum
2335 case bbp_get_forum_post_type() :
2336 $post_status = bbp_get_non_public_forum_statuses();
2337 break;
2338
2339 // Topic
2340 case bbp_get_topic_post_type() :
2341 $post_status = bbp_get_non_public_topic_statuses();
2342 break;
2343
2344 // Reply
2345 case bbp_get_reply_post_type() :
2346 $post_status = bbp_get_non_public_reply_statuses();
2347 break;
2348
2349 // Any
2350 default :
2351 $post_status = bbp_get_public_status_id();
2352 break;
2353 }
2354
2355 // Get counts
2356 $counts = bbp_filter_child_counts_list( $parent_id, $post_type, $post_status );
2357 $child_count = isset( $counts[ $post_type ] )
2358 ? bbp_number_not_negative( array_sum( array_values( $counts[ $post_type ] ) ) )
2359 : 0;
2360
2361 // Filter & return
2362 return (int) apply_filters( 'bbp_get_non_public_child_count', $child_count, $parent_id, $post_type );
2363 }
2364
2365 /**
2366 * Query the DB and get the child id's of public children
2367 *
2368 * @since 2.0.0 bbPress (r2891)
2369 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects
2370 *
2371 * @param int $parent_id Parent id.
2372 * @param string $post_type Post type. Defaults to 'post'.
2373 *
2374 * @return array The array of children
2375 */
2376 function bbp_get_public_child_ids( $parent_id = 0, $post_type = 'post' ) {
2377
2378 // Bail if nothing passed
2379 if ( empty( $parent_id ) || empty( $post_type ) ) {
2380 return array();
2381 }
2382
2383 // Which statuses
2384 switch ( $post_type ) {
2385
2386 // Forum
2387 case bbp_get_forum_post_type() :
2388 $post_status = bbp_get_public_forum_statuses();
2389 break;
2390
2391 // Topic
2392 case bbp_get_topic_post_type() :
2393 $post_status = bbp_get_public_topic_statuses();
2394 break;
2395
2396 // Reply
2397 case bbp_get_reply_post_type() :
2398 default :
2399 $post_status = bbp_get_public_reply_statuses();
2400 break;
2401 }
2402
2403 $query = new WP_Query(
2404 array(
2405 'fields' => 'ids',
2406 'post_parent' => $parent_id,
2407 'post_status' => $post_status,
2408 'post_type' => $post_type,
2409 'posts_per_page' => -1,
2410 'orderby' => array(
2411 'post_date' => 'DESC',
2412 'ID' => 'DESC'
2413 ),
2414
2415 // Performance
2416 'nopaging' => true,
2417 'suppress_filters' => true,
2418 'update_post_term_cache' => false,
2419 'update_post_meta_cache' => false,
2420 'ignore_sticky_posts' => true,
2421 'no_found_rows' => true
2422 )
2423 );
2424
2425 $child_ids = ! empty( $query->posts )
2426 ? $query->posts
2427 : array();
2428
2429 unset( $query );
2430
2431 // Filter & return
2432 return (array) apply_filters( 'bbp_get_public_child_ids', $child_ids, $parent_id, $post_type );
2433 }
2434
2435 /**
2436 * Query the DB and get the child id's of all children
2437 *
2438 * @since 2.0.0 bbPress (r3325)
2439 *
2440 * @param int $parent_id Parent id
2441 * @param string $post_type Post type. Defaults to 'post'
2442 *
2443 * @return array The array of children
2444 */
2445 function bbp_get_all_child_ids( $parent_id = 0, $post_type = 'post' ) {
2446
2447 // Bail if nothing passed
2448 if ( empty( $parent_id ) || empty( $post_type ) ) {
2449 return array();
2450 }
2451
2452 // Make cache key
2453 $not_in = array( 'draft', 'future' );
2454 $key = md5(
2455 serialize(
2456 array(
2457 'parent_id' => $parent_id,
2458 'post_type' => $post_type,
2459 'post_status' => $not_in
2460 )
2461 )
2462 );
2463
2464 // Check last changed
2465 $last_changed = wp_cache_get_last_changed( 'bbpress_posts' );
2466 $cache_key = "bbp_child_ids:{$key}:{$last_changed}";
2467
2468 // Check for cache and set if needed
2469 $child_ids = wp_cache_get( $cache_key, 'bbpress_posts' );
2470
2471 // Not already cached
2472 if ( false === $child_ids ) {
2473
2474 // Join post statuses to specifically exclude together
2475 $post_status = "'" . implode( "', '", $not_in ) . "'";
2476 $bbp_db = bbp_db();
2477
2478 // Note that we can't use WP_Query here thanks to post_status assumptions
2479 $query = $bbp_db->prepare( "SELECT ID FROM {$bbp_db->posts} WHERE post_parent = %d AND post_status NOT IN ( {$post_status} ) AND post_type = %s ORDER BY ID DESC", $parent_id, $post_type );
2480 $child_ids = (array) $bbp_db->get_col( $query );
2481
2482 // Always cache the results
2483 wp_cache_set( $cache_key, $child_ids, 'bbpress_posts' );
2484 }
2485
2486 // Make sure results are INTs
2487 $child_ids = wp_parse_id_list( $child_ids );
2488
2489 // Filter & return
2490 return (array) apply_filters( 'bbp_get_all_child_ids', $child_ids, $parent_id, $post_type );
2491 }
2492
2493 /**
2494 * Prime familial post caches.
2495 *
2496 * This function uses _prime_post_caches() to prepare the object cache for
2497 * imminent requests to post objects that aren't naturally cached by the primary
2498 * WP_Query calls themselves. Post author caches are also primed.
2499 *
2500 * This is triggered when a `update_post_family_cache` argument is set to true.
2501 *
2502 * Also see: bbp_update_post_author_caches()
2503 *
2504 * @since 2.6.0 bbPress (r6700)
2505 *
2506 * @param array $objects Array of objects, fresh from a query
2507 *
2508 * @return bool True if some IDs were cached
2509 */
2510 function bbp_update_post_family_caches( $objects = array() ) {
2511
2512 // Bail if no posts
2513 if ( empty( $objects ) ) {
2514 return false;
2515 }
2516
2517 // Default value
2518 $post_ids = array();
2519
2520 // Filter the types of IDs to prime
2521 $ids = apply_filters(
2522 'bbp_update_post_family_caches',
2523 array(
2524 '_bbp_last_active_id',
2525 '_bbp_last_reply_id',
2526 '_bbp_last_topic_id',
2527 '_bbp_reply_to'
2528 ),
2529 $objects
2530 );
2531
2532 // Get the last active IDs
2533 foreach ( $objects as $object ) {
2534 $object = get_post( $object );
2535
2536 // Skip if post ID is empty.
2537 if ( empty( $object->ID ) ) {
2538 continue;
2539 }
2540
2541 // Meta IDs
2542 foreach ( $ids as $key ) {
2543 $post_ids[] = get_post_meta( $object->ID, $key, true );
2544 }
2545
2546 // This post ID is already cached, but the post author may not be
2547 $post_ids[] = $object->ID;
2548 }
2549
2550 // Unique, non-zero values
2551 $post_ids = bbp_get_unique_array_values( $post_ids );
2552
2553 // Bail if no IDs to prime
2554 if ( empty( $post_ids ) ) {
2555 return false;
2556 }
2557
2558 // Prime post caches
2559 _prime_post_caches( $post_ids, true, true );
2560
2561 // Prime post author caches
2562 bbp_update_post_author_caches( $post_ids );
2563
2564 // Return
2565 return true;
2566 }
2567
2568 /**
2569 * Prime post author caches.
2570 *
2571 * This function uses cache_users() to prepare the object cache for
2572 * imminent requests to user objects that aren't naturally cached by the primary
2573 * WP_Query calls themselves.
2574 *
2575 * This is triggered when a `update_post_author_cache` argument is set to true.
2576 *
2577 * @since 2.6.0 bbPress (r6700)
2578 *
2579 * @param array $objects Array of objects, fresh from a query
2580 *
2581 * @return bool True if some IDs were cached
2582 */
2583 function bbp_update_post_author_caches( $objects = array() ) {
2584
2585 // Bail if no posts
2586 if ( empty( $objects ) ) {
2587 return false;
2588 }
2589
2590 // Default value
2591 $user_ids = array();
2592
2593 // Get the user IDs (could use wp_list_pluck() if this is ever a bottleneck)
2594 foreach ( $objects as $object ) {
2595 $object = get_post( $object );
2596
2597 // Skip if post does not have an author ID.
2598 if ( empty( $object->post_author ) ) {
2599 continue;
2600 }
2601
2602 // If post exists, add post author to the array.
2603 $user_ids[] = (int) $object->post_author;
2604 }
2605
2606 // Unique, non-zero values
2607 $user_ids = bbp_get_unique_array_values( $user_ids );
2608
2609 // Bail if no IDs to prime
2610 if ( empty( $user_ids ) ) {
2611 return false;
2612 }
2613
2614 // Try to prime user caches
2615 cache_users( $user_ids );
2616
2617 // Return
2618 return true;
2619 }
2620
2621 /** Globals *******************************************************************/
2622
2623 /**
2624 * Get the unfiltered value of a global $post's key
2625 *
2626 * Used most frequently when editing a forum/topic/reply
2627 *
2628 * @since 2.1.0 bbPress (r3702)
2629 *
2630 * @param string $field Name of the key
2631 * @param string $context How to sanitize - raw|edit|db|display|attribute|js
2632 * @return string Field value
2633 */
2634 function bbp_get_global_post_field( $field = 'ID', $context = 'edit' ) {
2635
2636 // Get the post, and maybe get a field from it
2637 $post = get_post();
2638 $retval = isset( $post->{$field} )
2639 ? sanitize_post_field( $field, $post->{$field}, $post->ID, $context )
2640 : '';
2641
2642 // Filter & return
2643 return apply_filters( 'bbp_get_global_post_field', $retval, $post, $field, $context );
2644 }
2645
2646 /** Nonces ********************************************************************/
2647
2648 /**
2649 * Makes sure the user requested an action from another page on this site.
2650 *
2651 * To avoid security exploits within the theme.
2652 *
2653 * @since 2.1.0 bbPress (r4023)
2654 *
2655 * @param string $action Action nonce
2656 * @param string $query_arg where to look for nonce in $_REQUEST
2657 */
2658 function bbp_verify_nonce_request( $action = '', $query_arg = '_wpnonce' ) {
2659
2660 /** Home URL **************************************************************/
2661
2662 // Parse home_url() into pieces to remove query-strings, strange characters,
2663 // and other funny things that plugins might to do to it.
2664 $parsed_home = wp_parse_url( home_url( '/', ( is_ssl() ? 'https' : 'http' ) ) );
2665
2666 // Maybe include the port, if it's included
2667 if ( isset( $parsed_home['port'] ) ) {
2668 $parsed_host = $parsed_home['host'] . ':' . $parsed_home['port'];
2669 } else {
2670 $parsed_host = $parsed_home['host'];
2671 }
2672
2673 // Set the home URL for use in comparisons
2674 $home_url = trim( strtolower( $parsed_home['scheme'] . '://' . $parsed_host . $parsed_home['path'] ), '/' );
2675
2676 /** Requested URL *********************************************************/
2677
2678 // Maybe include the port, if it's included in home_url()
2679 if ( isset( $parsed_home['port'] ) && false === strpos( $_SERVER['HTTP_HOST'], ':' ) ) {
2680 $request_host = $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'];
2681 } else {
2682 $request_host = $_SERVER['HTTP_HOST'];
2683 }
2684
2685 // Build the currently requested URL
2686 $scheme = bbp_get_url_scheme();
2687 $requested_url = strtolower( $scheme . $request_host . $_SERVER['REQUEST_URI'] );
2688
2689 /** Look for match ********************************************************/
2690
2691 /**
2692 * Filters the requested URL being nonce-verified.
2693 *
2694 * Useful for configurations like reverse proxying.
2695 *
2696 * @since 2.2.0 bbPress (r4361)
2697 *
2698 * @param string $requested_url The requested URL.
2699 */
2700 $matched_url = apply_filters( 'bbp_verify_nonce_request_url', $requested_url );
2701
2702 // Check the nonce
2703 $result = isset( $_REQUEST[ $query_arg ] )
2704 ? wp_verify_nonce( $_REQUEST[ $query_arg ], $action )
2705 : false;
2706
2707 // Nonce check failed
2708 if ( empty( $result ) || empty( $action ) || ( strpos( $matched_url, $home_url ) !== 0 ) ) {
2709 $result = false;
2710 }
2711
2712 /**
2713 * Fires at the end of the nonce verification check.
2714 *
2715 * @since 2.1.0 bbPress (r4023)
2716 *
2717 * @param string $action Action nonce.
2718 * @param bool $result Boolean result of nonce verification.
2719 */
2720 do_action( 'bbp_verify_nonce_request', $action, $result );
2721
2722 return $result;
2723 }
2724
2725 /** Feeds *********************************************************************/
2726
2727 /**
2728 * Escape CDATA terminators in bbPress feed content.
2729 *
2730 * Mirrors the handling in WordPress's get_the_content_feed() while preserving
2731 * bbPress's topic and reply content filters.
2732 *
2733 * @since 2.6.19 bbPress (r7620)
2734 *
2735 * @param string $content Feed content.
2736 * @return string Content safe to include in a CDATA section.
2737 */
2738 function bbp_escape_feed_cdata( $content = '' ) {
2739 return str_replace( ']]>', ']]&gt;', $content );
2740 }
2741
2742 /**
2743 * This function is hooked into the WordPress 'request' action and is
2744 * responsible for sniffing out the query vars and serving up RSS2 feeds if
2745 * the stars align and the user has requested a feed of any bbPress type.
2746 *
2747 * @since 2.0.0 bbPress (r3171)
2748 *
2749 * @param array $query_vars
2750 * @return array
2751 */
2752 function bbp_request_feed_trap( $query_vars = array() ) {
2753
2754 // Looking at a feed
2755 if ( isset( $query_vars['feed'] ) ) {
2756
2757 // Forum/Topic/Reply Feed
2758 if ( isset( $query_vars['post_type'] ) ) {
2759
2760 // Matched post type
2761 $post_type = false;
2762
2763 // Post types to check
2764 $post_types = array(
2765 bbp_get_forum_post_type(),
2766 bbp_get_topic_post_type(),
2767 bbp_get_reply_post_type()
2768 );
2769
2770 // Cast query vars as array outside of foreach loop
2771 $qv_array = (array) $query_vars['post_type'];
2772
2773 // Check if this query is for a bbPress post type
2774 foreach ( $post_types as $bbp_pt ) {
2775 if ( in_array( $bbp_pt, $qv_array, true ) ) {
2776 $post_type = $bbp_pt;
2777 break;
2778 }
2779 }
2780
2781 // Looking at a bbPress post type
2782 if ( ! empty( $post_type ) ) {
2783
2784 // Supported select query vars
2785 $select_query_vars = array(
2786 'p' => false,
2787 'name' => false,
2788 $post_type => false,
2789 );
2790
2791 // Setup matched variables to select
2792 foreach ( $query_vars as $key => $value ) {
2793 if ( isset( $select_query_vars[ $key ] ) ) {
2794 $select_query_vars[ $key ] = $value;
2795 }
2796 }
2797
2798 // Remove any empties
2799 $select_query_vars = array_filter( $select_query_vars );
2800
2801 // What bbPress post type are we looking for feeds on?
2802 switch ( $post_type ) {
2803
2804 // Forum
2805 case bbp_get_forum_post_type() :
2806
2807 // Define local variable(s)
2808 $meta_query = array();
2809
2810 // Single forum
2811 if ( ! empty( $select_query_vars ) ) {
2812
2813 // Load up our own query
2814 // phpcs:ignore WordPress.WP.DiscouragedFunctions.query_posts_query_posts
2815 query_posts(
2816 array_merge(
2817 array(
2818 'post_type' => bbp_get_forum_post_type(),
2819 'feed' => true
2820 ),
2821 $select_query_vars
2822 )
2823 );
2824
2825 // Restrict to specific forum ID
2826 $meta_query = array(
2827 array(
2828 'key' => '_bbp_forum_id',
2829 'value' => bbp_get_forum_id(),
2830 'type' => 'NUMERIC',
2831 'compare' => '='
2832 )
2833 );
2834 }
2835
2836 // Only forum replies
2837 if ( ! empty( $_GET['type'] ) && ( bbp_get_reply_post_type() === $_GET['type'] ) ) {
2838
2839 // The query
2840 $the_query = array(
2841 'author' => 0,
2842 'feed' => true,
2843 'post_type' => bbp_get_reply_post_type(),
2844 'post_parent' => 'any',
2845 'post_status' => bbp_get_public_reply_statuses(),
2846 'posts_per_page' => bbp_get_replies_per_rss_page(),
2847 'order' => 'DESC',
2848 'meta_query' => $meta_query
2849 );
2850
2851 // Output the feed
2852 bbp_display_replies_feed_rss2( $the_query );
2853
2854 // Only forum topics
2855 } elseif ( ! empty( $_GET['type'] ) && ( bbp_get_topic_post_type() === $_GET['type'] ) ) {
2856
2857 // The query
2858 $the_query = array(
2859 'author' => 0,
2860 'feed' => true,
2861 'post_type' => bbp_get_topic_post_type(),
2862 'post_parent' => bbp_get_forum_id(),
2863 'post_status' => bbp_get_public_topic_statuses(),
2864 'posts_per_page' => bbp_get_topics_per_rss_page(),
2865 'order' => 'DESC'
2866 );
2867
2868 // Output the feed
2869 bbp_display_topics_feed_rss2( $the_query );
2870
2871 // All forum topics and replies
2872 } else {
2873
2874 // Exclude private/hidden forums if not looking at single
2875 if ( empty( $select_query_vars ) ) {
2876 $meta_query = array( bbp_exclude_forum_ids( 'meta_query' ) );
2877 }
2878
2879 // The query
2880 $the_query = array(
2881 'author' => 0,
2882 'feed' => true,
2883 'post_type' => array( bbp_get_reply_post_type(), bbp_get_topic_post_type() ),
2884 'post_parent' => 'any',
2885 'post_status' => bbp_get_public_topic_statuses(),
2886 'posts_per_page' => bbp_get_replies_per_rss_page(),
2887 'order' => 'DESC',
2888 'meta_query' => $meta_query
2889 );
2890
2891 // Output the feed
2892 bbp_display_replies_feed_rss2( $the_query );
2893 }
2894
2895 break;
2896
2897 // Topic feed - Show replies
2898 case bbp_get_topic_post_type() :
2899
2900 // Single topic
2901 if ( ! empty( $select_query_vars ) ) {
2902
2903 // Load up our own query
2904 // phpcs:ignore WordPress.WP.DiscouragedFunctions.query_posts_query_posts
2905 query_posts(
2906 array_merge(
2907 array(
2908 'post_type' => bbp_get_topic_post_type(),
2909 'feed' => true
2910 ),
2911 $select_query_vars
2912 )
2913 );
2914
2915 // Output the feed
2916 bbp_display_replies_feed_rss2( array( 'feed' => true ) );
2917
2918 // All topics
2919 } else {
2920
2921 // The query
2922 $the_query = array(
2923 'author' => 0,
2924 'feed' => true,
2925 'post_parent' => 'any',
2926 'posts_per_page' => bbp_get_topics_per_rss_page(),
2927 'show_stickies' => false
2928 );
2929
2930 // Output the feed
2931 bbp_display_topics_feed_rss2( $the_query );
2932 }
2933
2934 break;
2935
2936 // Replies
2937 case bbp_get_reply_post_type() :
2938
2939 // The query
2940 $the_query = array(
2941 'posts_per_page' => bbp_get_replies_per_rss_page(),
2942 'meta_query' => array( array() ),
2943 'feed' => true
2944 );
2945
2946 // All replies
2947 if ( empty( $select_query_vars ) ) {
2948 bbp_display_replies_feed_rss2( $the_query );
2949 }
2950
2951 break;
2952 }
2953 }
2954
2955 // Single Topic Vview
2956 } elseif ( isset( $query_vars[ bbp_get_view_rewrite_id() ] ) ) {
2957
2958 // Get the view
2959 $view = $query_vars[ bbp_get_view_rewrite_id() ];
2960
2961 // We have a view to display a feed
2962 if ( ! empty( $view ) ) {
2963
2964 // Get the view query
2965 $the_query = bbp_get_view_query_args( $view );
2966
2967 // Output the feed if view exists
2968 if ( ! empty( $the_query ) ) {
2969 bbp_display_topics_feed_rss2( $the_query );
2970 }
2971 }
2972 }
2973
2974 // @todo User profile feeds
2975 }
2976
2977 // No feed so continue on
2978 return $query_vars;
2979 }
2980
2981 /** Templates ******************************************************************/
2982
2983 /**
2984 * Used to guess if page exists at requested path
2985 *
2986 * @since 2.0.0 bbPress (r3304)
2987 *
2988 * @param string $path
2989 * @return mixed False if no page, Page object if true
2990 */
2991 function bbp_get_page_by_path( $path = '' ) {
2992
2993 // Default to false
2994 $retval = false;
2995
2996 // Path is not empty
2997 if ( ! empty( $path ) ) {
2998
2999 // Pretty permalinks are on so path might exist
3000 if ( get_option( 'permalink_structure' ) ) {
3001 $page = get_page_by_path( $path );
3002
3003 // Only public pages can provide archive titles and content.
3004 if ( ! empty( $page ) && ( 'page' === $page->post_type ) && is_post_publicly_viewable( $page ) && empty( $page->post_password ) ) {
3005 $retval = $page;
3006 }
3007 }
3008 }
3009
3010 // Filter & return
3011 return apply_filters( 'bbp_get_page_by_path', $retval, $path );
3012 }
3013
3014 /**
3015 * Prevent WordPress from guessing permalinks for bbPress post types.
3016 *
3017 * bbPress post types are excluded from search because their visibility depends
3018 * on forum access. Older versions of WordPress may otherwise guess a restricted
3019 * forum or topic permalink from a partial slug and expose its full title.
3020 *
3021 * @since 2.6.17 bbPress (r7500)
3022 *
3023 * @param bool $do_redirect_guess Whether to attempt to guess a redirect URL.
3024 *
3025 * @return bool Whether to attempt to guess a redirect URL.
3026 */
3027 function bbp_do_not_guess_404_permalink( $do_redirect_guess = true ) {
3028 $post_types = (array) get_query_var( 'post_type' );
3029
3030 if ( bbp_is_custom_post_type( $post_types ) ) {
3031 $do_redirect_guess = false;
3032 }
3033
3034 return $do_redirect_guess;
3035 }
3036
3037 /**
3038 * Sets the 404 status.
3039 *
3040 * Used primarily with topics/replies inside hidden forums.
3041 *
3042 * @since 2.0.0 bbPress (r3051)
3043 * @since 2.6.0 bbPress (r6583) Use status_header() & nocache_headers()
3044 *
3045 * @param WP_Query $query The query being checked
3046 *
3047 * @return bool Always returns true
3048 */
3049 function bbp_set_404( $query = null ) {
3050
3051 // Global fallback
3052 if ( empty( $query ) ) {
3053 $query = bbp_get_wp_query();
3054 }
3055
3056 // Setup environment
3057 $query->set_404();
3058
3059 // Setup request
3060 status_header( 404 );
3061 nocache_headers();
3062 }
3063
3064 /**
3065 * Sets the 200 status header.
3066 *
3067 * @since 2.6.0 bbPress (r6583)
3068 */
3069 function bbp_set_200() {
3070 status_header( 200 );
3071 }
3072
3073 /**
3074 * Maybe handle the default 404 handling for some bbPress conditions
3075 *
3076 * Some conditions (like private/hidden forums and edits) have their own checks
3077 * on `bbp_template_redirect` and are not currently 404s.
3078 *
3079 * @since 2.6.0 bbPress (r6554)
3080 *
3081 * @param bool $override Whether to override the default handler
3082 * @param WP_Query $wp_query The posts query being referenced
3083 *
3084 * @return bool False to leave alone, true to override
3085 */
3086 function bbp_pre_handle_404( $override = false, $wp_query = false ) {
3087
3088 // Handle a bbPress 404 condition
3089 if ( isset( $wp_query->bbp_is_404 ) ) {
3090
3091 // Either force a 404 when 200, or a 200 when 404
3092 if ( true === $wp_query->bbp_is_404 ) {
3093 bbp_set_404( $wp_query );
3094 } else {
3095 bbp_set_200();
3096 }
3097
3098 // Overridden
3099 $override = true;
3100 }
3101
3102 // Return, maybe overridden
3103 return $override;
3104 }
3105
3106 /**
3107 * Maybe pre-assign the posts that are returned from a WP_Query.
3108 *
3109 * This effectively short-circuits the default query for posts, which is
3110 * currently only used to avoid calling the main query when it's not necessary.
3111 *
3112 * @since 2.6.0 bbPress (r6583)
3113 *
3114 * @param mixed $posts Default null. Array of posts (possibly empty)
3115 * @param WP_Query $wp_query
3116 *
3117 * @return mixed Null if no override. Array if overridden.
3118 */
3119 function bbp_posts_pre_query( $posts = null, $wp_query = false ) {
3120
3121 // Custom 404 handler is set, so set posts to empty array to avoid 2 queries
3122 if ( ! empty( $wp_query->bbp_is_404 ) ) {
3123 $posts = array();
3124 }
3125
3126 // Return, maybe overridden
3127 return $posts;
3128 }
3129
3130 /**
3131 * Get scheme for a URL based on is_ssl() results.
3132 *
3133 * @since 2.6.0 bbPress (r6760)
3134 *
3135 * @return string https:// if is_ssl(), otherwise http://
3136 */
3137 function bbp_get_url_scheme() {
3138 return is_ssl()
3139 ? 'https://'
3140 : 'http://';
3141 }
3142
3143 /** Titles ********************************************************************/
3144
3145 /**
3146 * Is a title longer that the maximum title length?
3147 *
3148 * Uses mb_strlen() in `8bit` mode to treat strings as raw. This matches the
3149 * behavior present in Comments, PHPMailer, RandomCompat, and others.
3150 *
3151 * @since 2.6.0 bbPress (r6784)
3152 *
3153 * @param string $title
3154 * @return bool
3155 */
3156 function bbp_is_title_too_long( $title = '' ) {
3157 $max = bbp_get_title_max_length();
3158 $len = mb_strlen( $title, '8bit' );
3159 $result = ( $len > $max );
3160
3161 // Filter & return
3162 return (bool) apply_filters( 'bbp_is_title_too_long', $result, $title, $max, $len );
3163 }
3164