PluginProbe
bbPress / 2.6.15
bbPress v2.6.15
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 2.6.15, at includes/common/functions.php

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