PluginProbe
bbPress / trunk
bbPress vtrunk
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 2.2 2.2.1 2.2.2 All 71 releases
bbpress / includes / common / functions.php

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

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