PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / trunk
Forumax – AI Powered Advanced Community Forum Plugin vtrunk
2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 2.0.0 2.1.0 All 29 releases
bbp-core / lib / bbpress / includes / common / functions.php

functions.php in Forumax – AI Powered Advanced Community Forum Plugin trunk, at lib/bbpress/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