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