'bbpress' ), 'get_post_types' ); // Return post types return get_post_types( $r ); } /** URLs **********************************************************************/ /** * Return the unescaped redirect_to request value * * @bbPress (r4655) * * @return string The URL to redirect to, if set */ function bbp_get_redirect_to() { // Check 'redirect_to' request parameter $retval = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; // Filter & return return apply_filters( 'bbp_get_redirect_to', $retval ); } /** * Append 'view=all' to query string if it's already there from referer * * @since 2.0.0 bbPress (r3325) * * @param string $original_link Original Link to be modified * @param bool $force Override bbp_get_view_all() check * @return string The link with 'view=all' appended if necessary */ function bbp_add_view_all( $original_link = '', $force = false ) { // Are we appending the view=all vars? $link = ( bbp_get_view_all() || ! empty( $force ) ) ? add_query_arg( array( 'view' => 'all' ), $original_link ) : $original_link; // Filter & return return apply_filters( 'bbp_add_view_all', $link, $original_link ); } /** * Remove 'view=all' from query string * * @since 2.0.0 bbPress (r3325) * * @param string $original_link Original Link to be modified * @return string The link with 'view=all' appended if necessary */ function bbp_remove_view_all( $original_link = '' ) { // Remove `view' argument $link = remove_query_arg( 'view', $original_link ); // Filter & return return apply_filters( 'bbp_remove_view_all', $link, $original_link ); } /** * If current user can and is viewing all topics/replies * * @since 2.0.0 bbPress (r3325) * * @param string $cap Capability used to ensure user can view all * * @return bool Whether current user can and is viewing all */ function bbp_get_view_all( $cap = 'moderate' ) { $retval = ( ( ! empty( $_GET['view'] ) && ( 'all' === $_GET['view'] ) && current_user_can( $cap ) ) ); // Filter & return return (bool) apply_filters( 'bbp_get_view_all', (bool) $retval, $cap ); } /** * Assist pagination by returning correct page number * * @since 2.0.0 bbPress (r2628) * * @return int Current page number */ function bbp_get_paged() { $wp_query = bbp_get_wp_query(); // Check the query var if ( get_query_var( 'paged' ) ) { $paged = get_query_var( 'paged' ); // Check query paged } elseif ( ! empty( $wp_query->query['paged'] ) ) { $paged = $wp_query->query['paged']; } // Paged found if ( ! empty( $paged ) ) { return (int) $paged; } // Default to first page return 1; } /** Misc **********************************************************************/ /** * Return the unique non-empty values of an array. * * @since 2.6.0 bbPress (r6481) * * @param array $array Array to get values of * * @return array */ function bbp_get_unique_array_values( $array = array() ) { return array_unique( array_filter( array_values( $array ) ) ); } /** * Fix post author id on post save * * When a logged in user changes the status of an anonymous reply or topic, or * edits it, the post_author field is set to the logged in user's id. This * function fixes that. * * @since 2.0.0 bbPress (r2734) * * @param array $data Post data * @param array $postarr Original post array (includes post id) * @return array Data */ function bbp_fix_post_author( $data = array(), $postarr = array() ) { // Post is not being updated or the post_author is already 0, return if ( empty( $postarr['ID'] ) || empty( $data['post_author'] ) ) { return $data; } // Post is not a topic or reply, return if ( ! in_array( $data['post_type'], array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ), true ) ) { return $data; } // Is the post by an anonymous user? if ( ( bbp_get_topic_post_type() === $data['post_type'] && ! bbp_is_topic_anonymous( $postarr['ID'] ) ) || ( bbp_get_reply_post_type() === $data['post_type'] && ! bbp_is_reply_anonymous( $postarr['ID'] ) ) ) { return $data; } // The post is being updated. It is a topic or a reply and is written by an anonymous user. // Set the post_author back to 0 $data['post_author'] = 0; return $data; } /** * Use the previous status when restoring a topic or reply. * * Fixes an issue since WordPress 5.6.0. See * {@link https://bbpress.trac.wordpress.org/ticket/3433}. * * @since 2.6.10 bbPress (r7233) * * @param string $new_status New status to use when untrashing. Default: 'draft' * @param int $post_id Post ID * @param string $previous_status Previous post status from '_wp_trash_meta_status' meta key. Default: 'pending' */ function bbp_fix_untrash_post_status( $new_status = 'draft', $post_id = 0, $previous_status = 'pending' ) { // Bail if not Topic or Reply if ( ! bbp_is_topic( $post_id ) && ! bbp_is_reply( $post_id ) ) { return $new_status; } // Prefer the previous status, falling back to the new status $retval = ! empty( $previous_status ) ? $previous_status : $new_status; return $retval; } /** * Check a date against the length of time something can be edited. * * It is recommended to leave $utc set to true and to work with UTC/GMT dates. * Turning this off will use the WordPress offset which is likely undesirable. * * @since 2.0.0 bbPress (r3133) * @since 2.6.0 bbPress (r6868) Inverted some logic and added unit tests * * @param string $datetime Gets run through strtotime() * @param boolean $utc Default true. Is the timestamp in UTC? * * @return bool True by default, if date is past, or editing is disabled. */ function bbp_past_edit_lock( $datetime = '', $utc = true ) { // Default value $retval = true; // Check if date and editing is allowed if ( bbp_allow_content_edit() ) { // Get number of minutes to allow editing for $minutes = bbp_get_edit_lock(); // 0 minutes means forever, so can never be past edit-lock time if ( 0 === $minutes ) { $retval = false; // Checking against a specific datetime } elseif ( ! empty( $datetime ) ) { // Period of time $lockable = "+{$minutes} minutes"; if ( true === $utc ) { $lockable .= ' UTC'; } // Now $cur_time = current_time( 'timestamp', $utc ); // Get the duration in seconds $duration = strtotime( $lockable ) - $cur_time; // Diff the times down to seconds $lock_time = strtotime( $lockable, $cur_time ); $past_time = strtotime( $datetime, $cur_time ); $diff_time = ( $lock_time - $past_time ) - $duration; // Check if less than lock time if ( $diff_time < $duration ) { $retval = false; } } } // Filter & return return (bool) apply_filters( 'bbp_past_edit_lock', $retval, $datetime, $utc ); } /** * Get number of days something should remain trashed for before it is cleaned * up by WordPress Cron. If set to 0, items will skip trash and be deleted * immediately. * * @since 2.6.0 bbPress (r6424) * * @param string $context Provide context for additional filtering * @return int Number of days items remain in trash */ function bbp_get_trash_days( $context = 'forum' ) { // Sanitize the context $context = sanitize_key( $context ); // Check the WordPress constant $days = defined( 'EMPTY_TRASH_DAYS' ) ? (int) EMPTY_TRASH_DAYS : 30; // Filter & return return (int) apply_filters( 'bbp_get_trash_days', $days, $context ); } /** Statistics ****************************************************************/ /** * Get the forum statistics * * @since 2.0.0 bbPress (r2769) * @since 2.6.0 bbPress (r6055) Added: * `count_pending_topics` * `count_pending_replies` * @since 2.6.10 bbPress (r7235) Renamed: * `count_trashed_topics` to `count_trash_topics` * `count_trashed_replies` to `count_trash_replies` * `count_spammed_topics` to `count_spam_topics` * `count_spammed_replies` to `count_spam_replies` * Added: * `count_hidden_topics` * `count_hidden_replies` * * @param array $args Optional. The function supports these arguments (all * default to true): * * - count_users: Count users? * - count_forums: Count forums? * - count_topics: Count topics? If set to false, private, spam and * trash topics are also not counted. * - count_pending_topics: Count pending topics? (only counted if the current * user has edit_others_topics cap) * - count_private_topics: Count private topics? (only counted if the current * user has read_private_topics cap) * - count_hidden_topics: Count hidden topics? (only counted if the current * user has read_hidden_topics cap) * - count_spam_topics: Count spam topics? (only counted if the current * user has edit_others_topics cap) * - count_trash_topics: Count trash topics? (only counted if the current * user has view_trash cap) * - count_replies: Count replies? If set to false, private, spam and * trash replies are also not counted. * - count_pending_replies: Count pending replies? (only counted if the current * user has edit_others_replies cap) * - count_private_replies: Count private replies? (only counted if the current * user has read_private_replies cap) * - count_hidden_replies: Count hidden replies? (only counted if the current * user has read_hidden_replies cap) * - count_spam_replies: Count spam replies? (only counted if the current * user has edit_others_replies cap) * - count_trash_replies: Count trash replies? (only counted if the current * user has view_trash cap) * - count_tags: Count tags? If set to false, empty tags are also * not counted * - count_empty_tags: Count empty tags? * * @return array Array of statistics */ function bbp_get_statistics( $args = array() ) { // Parse arguments against default values $r = bbp_parse_args( $args, array( // Users 'count_users' => true, // Forums 'count_forums' => true, // Topics 'count_topics' => true, 'count_pending_topics' => true, 'count_private_topics' => true, 'count_spam_topics' => true, 'count_trash_topics' => true, 'count_hidden_topics' => true, // Replies 'count_replies' => true, 'count_pending_replies' => true, 'count_private_replies' => true, 'count_spam_replies' => true, 'count_trash_replies' => true, 'count_hidden_replies' => true, // Topic tags 'count_tags' => true, 'count_empty_tags' => true ), 'get_statistics' ); // Defaults $topic_count = $topic_count_hidden = 0; $reply_count = $reply_count_hidden = 0; $topic_tag_count = $empty_topic_tag_count = 0; $hidden_topic_title = $hidden_reply_title = ''; // Post statuses $publish = bbp_get_public_status_id(); $closed = bbp_get_closed_status_id(); $pending = bbp_get_pending_status_id(); $private = bbp_get_private_status_id(); $hidden = bbp_get_hidden_status_id(); $spam = bbp_get_spam_status_id(); $trash = bbp_get_trash_status_id(); // Users $user_count = ! empty( $r['count_users'] ) ? bbp_get_total_users() : 0; // Forums $forum_count = ! empty( $r['count_forums'] ) ? wp_count_posts( bbp_get_forum_post_type() )->{$publish} : 0; // Default capabilities $caps = array( 'view_trash' => false, 'read_private_topics' => false, 'edit_others_topics' => false, 'read_private_replies' => false, 'edit_others_replies' => false, 'edit_topic_tags' => false ); // Get capabilities foreach ( $caps as $key => $cap ) { $caps[ $key ] = current_user_can( $cap ); } // Topics if ( ! empty( $r['count_topics'] ) ) { // Count all topics $all_topics = wp_count_posts( bbp_get_topic_post_type() ); // Published (publish + closed) $topic_count = $all_topics->{$publish} + $all_topics->{$closed}; // Declare empty arrays $topics = $topic_titles = array_fill_keys( bbp_get_non_public_topic_statuses(), '' ); // Pending if ( ! empty( $r['count_pending_topics'] ) && ! empty( $caps['edit_others_topics'] ) ) { $topics[ $pending ] = bbp_number_not_negative( $all_topics->{$pending} ); $topic_titles[ $pending ] = sprintf( esc_html__( 'Pending: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $pending ] ) ); } // Private if ( ! empty( $r['count_private_topics'] ) && ! empty( $caps['read_private_topics'] ) ) { $topics[ $private ] = bbp_number_not_negative( $all_topics->{$private} ); $topic_titles[ $private ] = sprintf( esc_html__( 'Private: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $private ] ) ); } // Hidden if ( ! empty( $r['count_hidden_topics'] ) && ! empty( $caps['read_hidden_topics'] ) ) { $topics[ $hidden ] = bbp_number_not_negative( $all_topics->{$hidden} ); $topic_titles[ $hidden ] = sprintf( esc_html__( 'Hidden: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $hidden ] ) ); } // Spam if ( ! empty( $r['count_spam_topics'] ) && ! empty( $caps['edit_others_topics'] ) ) { $topics[ $spam ] = bbp_number_not_negative( $all_topics->{$spam} ); $topic_titles[ $spam ] = sprintf( esc_html__( 'Spammed: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $spam ] ) ); } // Trash if ( ! empty( $r['count_trash_topics'] ) && ! empty( $caps['view_trash'] ) ) { $topics[ $trash ] = bbp_number_not_negative( $all_topics->{$trash} ); $topic_titles[ $trash ] = sprintf( esc_html__( 'Trashed: %s', 'bbpress' ), bbp_number_format_i18n( $topics[ $trash ] ) ); } // Total hidden (pending, private, hidden, spam, trash) $topic_count_hidden = array_sum( array_filter( $topics ) ); // Compile the hidden topic title $hidden_topic_title = implode( ' | ', array_filter( $topic_titles ) ); } // Replies if ( ! empty( $r['count_replies'] ) ) { // Count all replies $all_replies = wp_count_posts( bbp_get_reply_post_type() ); // Published $reply_count = $all_replies->{$publish}; // Declare empty arrays $topics = $topic_titles = array_fill_keys( bbp_get_non_public_reply_statuses(), '' ); // Pending if ( ! empty( $r['count_pending_replies'] ) && ! empty( $caps['edit_others_replies'] ) ) { $replies[ $pending ] = bbp_number_not_negative( $all_replies->{$pending} ); $reply_titles[ $pending ] = sprintf( esc_html__( 'Pending: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $pending ] ) ); } // Private if ( ! empty( $r['count_private_replies'] ) && ! empty( $caps['read_private_replies'] ) ) { $replies[ $private ] = bbp_number_not_negative( $all_replies->{$private} ); $reply_titles[ $private ] = sprintf( esc_html__( 'Private: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $private ] ) ); } // Hidden if ( ! empty( $r['count_hidden_replies'] ) && ! empty( $caps['read_hidden_replies'] ) ) { $replies[ $hidden ] = bbp_number_not_negative( $all_replies->{$hidden} ); $reply_titles[ $hidden ] = sprintf( esc_html__( 'Hidden: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $hidden ] ) ); } // Spam if ( ! empty( $r['count_spam_replies'] ) && ! empty( $caps['edit_others_replies'] ) ) { $replies[ $spam ] = bbp_number_not_negative( $all_replies->{$spam} ); $reply_titles[ $spam ] = sprintf( esc_html__( 'Spammed: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $spam ] ) ); } // Trash if ( ! empty( $r['count_trash_replies'] ) && ! empty( $caps['view_trash'] ) ) { $replies[ $trash ] = bbp_number_not_negative( $all_replies->{$trash} ); $reply_titles[ $trash ] = sprintf( esc_html__( 'Trashed: %s', 'bbpress' ), bbp_number_format_i18n( $replies[ $trash ] ) ); } // Total hidden (pending, private, hidden, spam, trash) $reply_count_hidden = array_sum( $replies ); // Compile the hidden replies title $hidden_reply_title = implode( ' | ', $reply_titles ); } // Topic Tags if ( ! empty( $r['count_tags'] ) && bbp_allow_topic_tags() ) { // Get the topic-tag taxonomy ID $tt_id = bbp_get_topic_tag_tax_id(); // Get the count $topic_tag_count = wp_count_terms( $tt_id, array( 'hide_empty' => true ) ); // Empty tags if ( ! empty( $r['count_empty_tags'] ) && ! empty( 'edit_topic_tags' ) ) { $empty_topic_tag_count = wp_count_terms( $tt_id ) - $topic_tag_count; } } // Tally the tallies $counts = array_map( 'absint', compact( 'user_count', 'forum_count', 'topic_count', 'topic_count_hidden', 'reply_count', 'reply_count_hidden', 'topic_tag_count', 'empty_topic_tag_count' ) ); // Define return value $statistics = array(); // Loop through and store the integer and i18n formatted counts foreach ( $counts as $key => $count ) { $not_negative = bbp_number_not_negative( $count ); $statistics[ $key ] = bbp_number_format_i18n( $not_negative ); $statistics[ "{$key}_int" ] = $not_negative; } // Add the hidden (topic/reply) count title attribute strings $statistics['hidden_topic_title'] = $hidden_topic_title; $statistics['hidden_reply_title'] = $hidden_reply_title; // Filter & return return (array) apply_filters( 'bbp_get_statistics', $statistics, $r, $args ); } /** New/edit topic/reply helpers **********************************************/ /** * Filter anonymous post data * * We use REMOTE_ADDR here directly. If you are behind a proxy, you should * ensure that it is properly set, such as in wp-config.php, for your * environment. See {@link https://core.trac.wordpress.org/ticket/9235} * * Note that bbp_pre_anonymous_filters() is responsible for sanitizing each * of the filtered core anonymous values here. * * If there are any errors, those are directly added to {@link bbPress:errors} * * @since 2.0.0 bbPress (r2734) * * @param array $args Optional. If no args are there, then $_POST values are * @return bool|array False on errors, values in an array on success */ function bbp_filter_anonymous_post_data( $args = array() ) { // Parse arguments against default values $r = bbp_parse_args( $args, array( 'bbp_anonymous_name' => ! empty( $_POST['bbp_anonymous_name'] ) ? $_POST['bbp_anonymous_name'] : false, 'bbp_anonymous_email' => ! empty( $_POST['bbp_anonymous_email'] ) ? $_POST['bbp_anonymous_email'] : false, 'bbp_anonymous_website' => ! empty( $_POST['bbp_anonymous_website'] ) ? $_POST['bbp_anonymous_website'] : false, ), 'filter_anonymous_post_data' ); // Strip invalid characters $r = bbp_sanitize_anonymous_post_author( $r ); // Filter name $r['bbp_anonymous_name'] = apply_filters( 'bbp_pre_anonymous_post_author_name', $r['bbp_anonymous_name'] ); if ( empty( $r['bbp_anonymous_name'] ) ) { bbp_add_error( 'bbp_anonymous_name', __( 'Error: Invalid author name.', 'bbpress' ) ); } // Filter email address $r['bbp_anonymous_email'] = apply_filters( 'bbp_pre_anonymous_post_author_email', $r['bbp_anonymous_email'] ); if ( empty( $r['bbp_anonymous_email'] ) ) { bbp_add_error( 'bbp_anonymous_email', __( 'Error: Invalid email address.', 'bbpress' ) ); } // Website is optional (can be empty) $r['bbp_anonymous_website'] = apply_filters( 'bbp_pre_anonymous_post_author_website', $r['bbp_anonymous_website'] ); // Filter & return return (array) apply_filters( 'bbp_filter_anonymous_post_data', $r, $args ); } /** * Sanitize an array of anonymous post author data * * @since 2.6.0 bbPress (r6400) * * @param array $anonymous_data * @return array */ function bbp_sanitize_anonymous_post_author( $anonymous_data = array() ) { // Make sure anonymous data is an array if ( ! is_array( $anonymous_data ) ) { $anonymous_data = array(); } // Map meta data to comment fields (as guides for stripping invalid text) $fields = array( 'bbp_anonymous_name' => 'comment_author', 'bbp_anonymous_email' => 'comment_author_email', 'bbp_anonymous_website' => 'comment_author_url' ); // Setup a new return array $r = $anonymous_data; // Get the database $bbp_db = bbp_db(); // Strip invalid text from fields foreach ( $fields as $bbp_field => $comment_field ) { if ( ! empty( $r[ $bbp_field ] ) ) { $r[ $bbp_field ] = $bbp_db->strip_invalid_text_for_column( $bbp_db->comments, $comment_field, $r[ $bbp_field ] ); } } // Filter & return return (array) apply_filters( 'bbp_sanitize_anonymous_post_author', $r, $anonymous_data ); } /** * Update the relevant meta-data for an anonymous post author * * @since 2.6.0 bbPress (r6400) * * @param int $post_id * @param array $anonymous_data * @param string $post_type */ function bbp_update_anonymous_post_author( $post_id = 0, $anonymous_data = array(), $post_type = '' ) { // Maybe look for anonymous if ( empty( $anonymous_data ) ) { $anonymous_data = bbp_filter_anonymous_post_data(); } // Sanitize parameters $post_id = (int) $post_id; $post_type = sanitize_key( $post_type ); // Bail if missing required data if ( empty( $post_id ) || empty( $post_type ) || empty( $anonymous_data ) ) { return; } // Parse arguments against default values $r = bbp_parse_args( $anonymous_data, array( 'bbp_anonymous_name' => '', 'bbp_anonymous_email' => '', 'bbp_anonymous_website' => '', ), "update_{$post_type}" ); // Update all anonymous metas foreach ( $r as $anon_key => $anon_value ) { // Update, or delete if empty ! empty( $anon_value ) ? update_post_meta( $post_id, '_' . $anon_key, (string) $anon_value, false ) : delete_post_meta( $post_id, '_' . $anon_key ); } } /** * Check for duplicate topics/replies * * Check to make sure that a user is not making a duplicate post * * @since 2.0.0 bbPress (r2763) * * @param array $post_data Contains information about the comment * @return bool True if it is not a duplicate, false if it is */ function bbp_check_for_duplicate( $post_data = array() ) { // Parse arguments against default values $r = bbp_parse_args( $post_data, array( 'post_author' => 0, 'post_type' => array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ), 'post_parent' => 0, 'post_content' => '', 'post_status' => bbp_get_trash_status_id(), 'anonymous_data' => array() ), 'check_for_duplicate' ); // No duplicate checks for those who can throttle if ( user_can( (int) $r['post_author'], 'throttle' ) ) { return true; } // Get the DB $bbp_db = bbp_db(); // Default clauses $join = $where = ''; // Check for anonymous post if ( empty( $r['post_author'] ) && ( ! empty( $r['anonymous_data'] ) && ! empty( $r['anonymous_data']['bbp_anonymous_email'] ) ) ) { // Sanitize the email address for querying $email = sanitize_email( $r['anonymous_data']['bbp_anonymous_email'] ); // Only proceed if ( ! empty( $email ) && is_email( $email ) ) { // Get the meta SQL $clauses = get_meta_sql( array( array( 'key' => '_bbp_anonymous_email', 'value' => $email, ) ), 'post', $bbp_db->posts, 'ID' ); // Set clauses $join = $clauses['join']; // "'", "%", "$" and are valid characters in email addresses $where = $bbp_db->remove_placeholder_escape( $clauses['where'] ); } } // Unslash $r to pass through DB->prepare() // // @see: https://bbpress.trac.wordpress.org/ticket/2185/ // @see: https://core.trac.wordpress.org/changeset/23973/ $r = wp_unslash( $r ); // Prepare duplicate check query $query = "SELECT ID FROM {$bbp_db->posts} {$join}"; $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'] ); $query .= ! empty( $r['post_parent'] ) ? $bbp_db->prepare( ' AND post_parent = %d', $r['post_parent'] ) : ''; $query .= $where; $query .= ' LIMIT 1'; $dupe = apply_filters( 'bbp_check_for_duplicate_query', $query, $r ); // Dupe found if ( $bbp_db->get_var( $dupe ) ) { do_action( 'bbp_check_for_duplicate_trigger', $post_data ); return false; } // Dupe not found return true; } /** * Check for flooding * * Check to make sure that a user is not making too many posts in a short amount * of time. * * @since 2.0.0 bbPress (r2734) * * @param array $anonymous_data Optional - if it's an anonymous post. Do not * supply if supplying $author_id. Should be * sanitized (see {@link bbp_filter_anonymous_post_data()} * @param int $author_id Optional. Supply if it's a post by a logged in user. * Do not supply if supplying $anonymous_data. * @return bool True if there is no flooding, false if there is */ function bbp_check_for_flood( $anonymous_data = array(), $author_id = 0 ) { // Allow for flood check to be skipped if ( apply_filters( 'bbp_bypass_check_for_flood', false, $anonymous_data, $author_id ) ) { return true; } // Option disabled. No flood checks. $throttle_time = get_option( '_bbp_throttle_time' ); if ( empty( $throttle_time ) || ! bbp_allow_content_throttle() ) { return true; } // User is anonymous, so check a transient based on the IP if ( ! empty( $anonymous_data ) ) { $last_posted = get_transient( '_bbp_' . bbp_current_author_ip() . '_last_posted' ); if ( ! empty( $last_posted ) && ( time() < ( $last_posted + $throttle_time ) ) ) { return false; } // User is logged in, so check their last posted time } elseif ( ! empty( $author_id ) ) { $author_id = (int) $author_id; $last_posted = bbp_get_user_last_posted( $author_id ); if ( ! empty( $last_posted ) && ( time() < ( $last_posted + $throttle_time ) ) && ! user_can( $author_id, 'throttle' ) ) { return false; } } else { return false; } return true; } /** * Checks topics and replies against the discussion moderation of blocked keys * * @since 2.1.0 bbPress (r3581) * * @param array $anonymous_data Optional - if it's an anonymous post. Do not * supply if supplying $author_id. Should be * sanitized (see {@link bbp_filter_anonymous_post_data()} * @param int $author_id Topic or reply author ID * @param string $title The title of the content * @param string $content The content being posted * @param mixed $strict False for moderation_keys. True for blacklist_keys. * String for custom keys. * @return bool True if test is passed, false if fail */ function bbp_check_for_moderation( $anonymous_data = array(), $author_id = 0, $title = '', $content = '', $strict = false ) { // Custom moderation option key if ( is_string( $strict ) ) { $strict = sanitize_key( $strict ); // Use custom key if ( ! empty( $strict ) ) { $hook_name = $strict; $option_name = "{$strict}_keys"; // Key was invalid, so default to moderation keys } else { $strict = false; } } // Strict mode uses WordPress "blacklist" settings if ( true === $strict ) { $hook_name = 'blacklist'; $option_name = 'blacklist_keys'; // Non-strict uses WordPress "moderation" settings } elseif ( false === $strict ) { $hook_name = 'moderation'; $option_name = 'moderation_keys'; } // Allow for moderation check to be skipped if ( apply_filters( "bbp_bypass_check_for_{$hook_name}", false, $anonymous_data, $author_id, $title, $content, $strict ) ) { return true; } // Maybe perform some author-specific capability checks if ( ! empty( $author_id ) ) { // Bail if user is a keymaster if ( bbp_is_user_keymaster( $author_id ) ) { return true; // Bail if user can moderate // https://bbpress.trac.wordpress.org/ticket/2726 } elseif ( ( false === $strict ) && user_can( $author_id, 'moderate' ) ) { return true; } } // Define local variable(s) $_post = array(); $match_out = ''; /** Max Links *************************************************************/ // Only check max_links when not being strict if ( false === $strict ) { $max_links = get_option( 'comment_max_links' ); if ( ! empty( $max_links ) ) { // How many links? $num_links = preg_match_all( '/(http|ftp|https):\/\//i', $content, $match_out ); // Allow for bumping the max to include the user's URL if ( ! empty( $_post['url'] ) ) { $num_links = apply_filters( 'comment_max_links_url', $num_links, $_post['url'], $content ); } // Das ist zu viele links! if ( $num_links >= $max_links ) { return false; } } } /** Moderation ************************************************************/ /** * Filters the bbPress moderation keys. * * @since 2.6.0 bbPress (r6050) * * @param string $moderation List of moderation keys. One per new line. */ $moderation = apply_filters( "bbp_{$hook_name}_keys", trim( get_option( $option_name ) ) ); // Bail if no words to look for if ( empty( $moderation ) ) { return true; } /** User Data *************************************************************/ // Map anonymous user data if ( ! empty( $anonymous_data ) ) { $_post['author'] = $anonymous_data['bbp_anonymous_name']; $_post['email'] = $anonymous_data['bbp_anonymous_email']; $_post['url'] = $anonymous_data['bbp_anonymous_website']; // Map current user data } elseif ( ! empty( $author_id ) ) { // Get author data $user = get_userdata( $author_id ); // If data exists, map it if ( ! empty( $user ) ) { $_post['author'] = $user->display_name; $_post['email'] = $user->user_email; $_post['url'] = $user->user_url; } } // Current user IP and user agent $_post['user_ip'] = bbp_current_author_ip(); $_post['user_ua'] = bbp_current_author_ua(); // Post title and content $_post['title'] = $title; $_post['content'] = $content; // Ensure HTML tags are not being used to bypass the moderation list. $_post['comment_without_html'] = wp_strip_all_tags( $content ); /** Words *****************************************************************/ // Get words separated by new lines $words = explode( "\n", $moderation ); // Loop through words foreach ( (array) $words as $word ) { // Trim the whitespace from the word $word = trim( $word ); // Skip empty lines if ( empty( $word ) ) { continue; } // Do some escaping magic so that '#' chars in the // spam words don't break things: $word = preg_quote( $word, '#' ); $pattern = "#{$word}#i"; // Loop through post data foreach ( $_post as $post_data ) { // Check each user data for current word if ( preg_match( $pattern, $post_data ) ) { // Post does not pass return false; } } } // Check passed successfully return true; } /** * Deprecated. Use bbp_check_for_moderation() with strict flag set. * * @since 2.0.0 bbPress (r3446) * @since 2.6.0 bbPress (r6854) * @deprecated 2.6.0 Use bbp_check_for_moderation() with strict flag set */ function bbp_check_for_blacklist( $anonymous_data = array(), $author_id = 0, $title = '', $content = '' ) { return bbp_check_for_moderation( $anonymous_data, $author_id, $title, $content, true ); } /** Subscriptions *************************************************************/ /** * Get the "Do Not Reply" email address to use when sending subscription emails. * * We make some educated guesses here based on the home URL. Filters are * available to customize this address further. In the future, we may consider * using `admin_email` instead, though this is not normally publicized. * * We use `$_SERVER['SERVER_NAME']` here to mimic similar functionality in * WordPress core. Previously, we used `get_home_url()` to use already validated * user input, but it was causing issues in some installations. * * @since 2.6.0 bbPress (r5409) * * @see wp_mail * @see wp_notify_postauthor * @link https://bbpress.trac.wordpress.org/ticket/2618 * * @return string */ function bbp_get_do_not_reply_address() { $sitename = strtolower( $_SERVER['SERVER_NAME'] ); if ( substr( $sitename, 0, 4 ) === 'www.' ) { $sitename = substr( $sitename, 4 ); } // Filter & return return apply_filters( 'bbp_get_do_not_reply_address', 'noreply@' . $sitename ); } /** * Sends notification emails for new replies to subscribed topics * * Gets new post ID and check if there are subscribed users to that topic, and * if there are, send notifications * * Note: in bbPress 2.6, we've moved away from 1 email per subscriber to 1 email * with everyone BCC'd. This may have negative repercussions for email services * that limit the number of addresses in a BCC field (often to around 500.) In * those cases, we recommend unhooking this function and creating your own * custom email script. * * @since 2.6.0 bbPress (r5413) * * @param int $reply_id ID of the newly made reply * @param int $topic_id ID of the topic of the reply * @param int $forum_id ID of the forum of the reply * @param array $anonymous_data Optional - if it's an anonymous post. Do not * supply if supplying $author_id. Should be * sanitized (see {@link bbp_filter_anonymous_post_data()} * @param int $reply_author ID of the topic author ID * @return bool True on success, false on failure */ function bbp_notify_topic_subscribers( $reply_id = 0, $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $reply_author = 0 ) { // Bail if subscriptions are turned off if ( ! bbp_is_subscriptions_active() ) { return false; } // Bail if importing if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { return false; } /** Validation ************************************************************/ $reply_id = bbp_get_reply_id( $reply_id ); $topic_id = bbp_get_topic_id( $topic_id ); $forum_id = bbp_get_forum_id( $forum_id ); /** Topic *****************************************************************/ // Bail if topic is not public (includes closed) if ( ! bbp_is_topic_public( $topic_id ) ) { return false; } /** Reply *****************************************************************/ // Bail if reply is not published if ( ! bbp_is_reply_published( $reply_id ) ) { return false; } // Poster name $reply_author_name = bbp_get_reply_author_display_name( $reply_id ); /** Users *****************************************************************/ // Get topic subscribers and bail if empty $user_ids = bbp_get_subscribers( $topic_id ); // Remove the reply author from the list. $reply_author_key = array_search( (int) $reply_author, $user_ids, true ); if ( false !== $reply_author_key ) { unset( $user_ids[ $reply_author_key ] ); } // Dedicated filter to manipulate user ID's to send emails to $user_ids = (array) apply_filters( 'bbp_topic_subscription_user_ids', $user_ids, $reply_id, $topic_id ); // Bail of the reply author was the only one subscribed. if ( empty( $user_ids ) ) { return false; } // Get email addresses, bail if empty $email_addresses = bbp_get_email_addresses_from_user_ids( $user_ids ); if ( empty( $email_addresses ) ) { return false; } /** Mail ******************************************************************/ // Remove filters from reply content and topic title to prevent content // from being encoded with HTML entities, wrapped in paragraph tags, etc... bbp_remove_all_filters( 'bbp_get_reply_content' ); bbp_remove_all_filters( 'bbp_get_topic_title' ); bbp_remove_all_filters( 'the_title' ); // Strip tags from text and setup mail data $forum_title = wp_specialchars_decode( strip_tags( bbp_get_forum_title( $forum_id ) ), ENT_QUOTES ); $topic_title = wp_specialchars_decode( strip_tags( bbp_get_topic_title( $topic_id ) ), ENT_QUOTES ); $reply_author_name = wp_specialchars_decode( strip_tags( $reply_author_name ), ENT_QUOTES ); $reply_content = wp_specialchars_decode( strip_tags( bbp_get_reply_content( $reply_id ) ), ENT_QUOTES ); $reply_url = bbp_get_reply_url( $reply_id ); // For plugins to filter messages per reply/topic/user $message = sprintf( esc_html__( '%1$s wrote: %2$s Post Link: %3$s ----------- You are receiving this email because you subscribed to a forum topic. Login and visit the topic to unsubscribe from these emails.', 'bbpress' ), $reply_author_name, $reply_content, $reply_url ); $message = apply_filters( 'bbp_subscription_mail_message', $message, $reply_id, $topic_id ); if ( empty( $message ) ) { return; } // For plugins to filter titles per reply/topic/user $subject = apply_filters( 'bbp_subscription_mail_title', '[' . $forum_title . '] ' . $topic_title, $reply_id, $topic_id ); if ( empty( $subject ) ) { return; } /** Headers ***************************************************************/ // Default bbPress X-header $headers = array( bbp_get_email_header() ); // Get the noreply@ address $no_reply = bbp_get_do_not_reply_address(); // Setup "From" email address $from_email = apply_filters( 'bbp_subscription_from_email', $no_reply ); // Setup the From header $headers[] = 'From: ' . get_bloginfo( 'name' ) . ' <' . $from_email . '>'; // Loop through addresses foreach ( (array) $email_addresses as $address ) { $headers[] = 'Bcc: ' . $address; } /** Send it ***************************************************************/ // Custom headers $headers = apply_filters( 'bbp_subscription_mail_headers', $headers ); $to_email = apply_filters( 'bbp_subscription_to_email', $no_reply ); // Before do_action( 'bbp_pre_notify_subscribers', $reply_id, $topic_id, $user_ids ); // Send notification email wp_mail( $to_email, $subject, $message, $headers ); // After do_action( 'bbp_post_notify_subscribers', $reply_id, $topic_id, $user_ids ); // Restore previously removed filters bbp_restore_all_filters( 'bbp_get_topic_content' ); bbp_restore_all_filters( 'bbp_get_topic_title' ); bbp_restore_all_filters( 'the_title' ); return true; } /** * Sends notification emails for new topics to subscribed forums * * Gets new post ID and check if there are subscribed users to that forum, and * if there are, send notifications * * Note: in bbPress 2.6, we've moved away from 1 email per subscriber to 1 email * with everyone BCC'd. This may have negative repercussions for email services * that limit the number of addresses in a BCC field (often to around 500.) In * those cases, we recommend unhooking this function and creating your own * custom email script. * * @since 2.5.0 bbPress (r5156) * * @param int $topic_id ID of the newly made reply * @param int $forum_id ID of the forum for the topic * @param array $anonymous_data Optional - if it's an anonymous post. Do not * supply if supplying $author_id. Should be * sanitized (see {@link bbp_filter_anonymous_post_data()} * @param int $topic_author ID of the topic author ID * @return bool True on success, false on failure */ function bbp_notify_forum_subscribers( $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $topic_author = 0 ) { // Bail if subscriptions are turned off if ( ! bbp_is_subscriptions_active() ) { return false; } // Bail if importing if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { return false; } /** Validation ************************************************************/ $topic_id = bbp_get_topic_id( $topic_id ); $forum_id = bbp_get_forum_id( $forum_id ); /** * Necessary for backwards compatibility * * @see https://bbpress.trac.wordpress.org/ticket/2620 */ $user_id = 0; /** Topic *****************************************************************/ // Bail if topic is not public (includes closed) if ( ! bbp_is_topic_public( $topic_id ) ) { return false; } // Poster name $topic_author_name = bbp_get_topic_author_display_name( $topic_id ); /** Users *****************************************************************/ // Get topic subscribers and bail if empty $user_ids = bbp_get_subscribers( $forum_id ); // Remove the topic author from the list. $topic_author_key = array_search( (int) $topic_author, $user_ids, true ); if ( false !== $topic_author_key ) { unset( $user_ids[ $topic_author_key ] ); } // Dedicated filter to manipulate user ID's to send emails to $user_ids = (array) apply_filters( 'bbp_forum_subscription_user_ids', $user_ids, $topic_id, $forum_id ); // Bail of the reply author was the only one subscribed. if ( empty( $user_ids ) ) { return false; } // Get email addresses, bail if empty $email_addresses = bbp_get_email_addresses_from_user_ids( $user_ids ); if ( empty( $email_addresses ) ) { return false; } /** Mail ******************************************************************/ // Remove filters from reply content and topic title to prevent content // from being encoded with HTML entities, wrapped in paragraph tags, etc... bbp_remove_all_filters( 'bbp_get_topic_content' ); bbp_remove_all_filters( 'bbp_get_topic_title' ); bbp_remove_all_filters( 'the_title' ); // Strip tags from text and setup mail data $forum_title = wp_specialchars_decode( strip_tags( bbp_get_forum_title( $forum_id ) ), ENT_QUOTES ); $topic_title = wp_specialchars_decode( strip_tags( bbp_get_topic_title( $topic_id ) ), ENT_QUOTES ); $topic_author_name = wp_specialchars_decode( strip_tags( $topic_author_name ), ENT_QUOTES ); $topic_content = wp_specialchars_decode( strip_tags( bbp_get_topic_content( $topic_id ) ), ENT_QUOTES ); $topic_url = get_permalink( $topic_id ); // For plugins to filter messages per reply/topic/user $message = sprintf( esc_html__( '%1$s wrote: %2$s Topic Link: %3$s ----------- You are receiving this email because you subscribed to a forum. Login and visit the topic to unsubscribe from these emails.', 'bbpress' ), $topic_author_name, $topic_content, $topic_url ); $message = apply_filters( 'bbp_forum_subscription_mail_message', $message, $topic_id, $forum_id, $user_id ); if ( empty( $message ) ) { return; } // For plugins to filter titles per reply/topic/user $subject = apply_filters( 'bbp_forum_subscription_mail_title', '[' . $forum_title . '] ' . $topic_title, $topic_id, $forum_id, $user_id ); if ( empty( $subject ) ) { return; } /** Headers ***************************************************************/ // Default bbPress X-header $headers = array( bbp_get_email_header() ); // Get the noreply@ address $no_reply = bbp_get_do_not_reply_address(); // Setup "From" email address $from_email = apply_filters( 'bbp_subscription_from_email', $no_reply ); // Setup the From header $headers[] = 'From: ' . get_bloginfo( 'name' ) . ' <' . $from_email . '>'; // Loop through addresses foreach ( (array) $email_addresses as $address ) { $headers[] = 'Bcc: ' . $address; } /** Send it ***************************************************************/ // Custom headers $headers = apply_filters( 'bbp_subscription_mail_headers', $headers ); $to_email = apply_filters( 'bbp_subscription_to_email', $no_reply ); // Before do_action( 'bbp_pre_notify_forum_subscribers', $topic_id, $forum_id, $user_ids ); // Send notification email wp_mail( $to_email, $subject, $message, $headers ); // After do_action( 'bbp_post_notify_forum_subscribers', $topic_id, $forum_id, $user_ids ); // Restore previously removed filters bbp_restore_all_filters( 'bbp_get_topic_content' ); bbp_restore_all_filters( 'bbp_get_topic_title' ); bbp_restore_all_filters( 'the_title' ); return true; } /** * Sends notification emails for new replies to subscribed topics * * This function is deprecated. Please use: bbp_notify_topic_subscribers() * * @since 2.0.0 bbPress (r2668) * * @deprecated 2.6.0 bbPress (r5412) * * @param int $reply_id ID of the newly made reply * @param int $topic_id ID of the topic of the reply * @param int $forum_id ID of the forum of the reply * @param array $anonymous_data Optional - if it's an anonymous post. Do not * supply if supplying $author_id. Should be * sanitized (see {@link bbp_filter_anonymous_post_data()} * @param int $reply_author ID of the topic author ID * * @return bool True on success, false on failure */ function bbp_notify_subscribers( $reply_id = 0, $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $reply_author = 0 ) { return bbp_notify_topic_subscribers( $reply_id, $topic_id, $forum_id, $anonymous_data, $reply_author ); } /** * Return an array of user email addresses from an array of user IDs * * @since 2.6.0 bbPress (r6722) * * @param array $user_ids * @return array */ function bbp_get_email_addresses_from_user_ids( $user_ids = array() ) { // Default return value $retval = array(); // Maximum number of users to get per database query $limit = apply_filters( 'bbp_get_users_chunk_limit', 100 ); // Only do the work if there are user IDs to query for if ( ! empty( $user_ids ) ) { // Get total number of sets $steps = ceil( count( $user_ids ) / $limit ); $range = array_map( 'intval', range( 1, $steps ) ); // Loop through users foreach ( $range as $loop ) { // Initial loop has no offset $offset = $limit * ( $loop - 1 ); // Calculate user IDs to include $loop_ids = array_slice( $user_ids, $offset, $limit ); // Skip if something went wrong if ( empty( $loop_ids ) ) { continue; } // Call get_users() in a way that users are cached $loop_users = get_users( array( 'blog_id' => 0, 'fields' => 'all_with_meta', 'include' => $loop_ids ) ); // Pluck emails from users $loop_emails = wp_list_pluck( $loop_users, 'user_email' ); // Clean-up memory, for big user sets unset( $loop_users ); // Merge users into return value if ( ! empty( $loop_emails ) ) { $retval = array_merge( $retval, $loop_emails ); } } // No duplicates $retval = bbp_get_unique_array_values( $retval ); } // Filter & return return apply_filters( 'bbp_get_email_addresses_from_user_ids', $retval, $user_ids, $limit ); } /** * Automatically splits bbPress emails with many Bcc recipients into chunks. * * This middleware is useful because topics and forums with many subscribers * run into problems with Bcc limits, and many hosting companies & third-party * services limit the size of a Bcc audience to prevent spamming. * * The default "chunk" size is 40 users per iteration, and can be filtered if * desired. A future version of bbPress will introduce a setting to more easily * tune this. * * @since 2.6.0 bbPress (r6918) * * @param array $args Original arguments passed to wp_mail(). * @return array */ function bbp_chunk_emails( $args = array() ) { // Get the maximum number of Bcc's per chunk $max_num = apply_filters( 'bbp_get_bcc_chunk_limit', 40, $args ); // Look for "bcc: " in a case-insensitive way, and split into 2 sets $match = '/^bcc: (\w+)/i'; $old_headers = preg_grep( $match, $args['headers'], PREG_GREP_INVERT ); $bcc_headers = preg_grep( $match, $args['headers'] ); // Bail if less than $max_num recipients if ( empty( $bcc_headers ) || ( count( $bcc_headers ) < $max_num ) ) { return $args; } // Reindex the headers arrays $old_headers = array_values( $old_headers ); $bcc_headers = array_values( $bcc_headers ); // Break the Bcc emails into chunks foreach ( array_chunk( $bcc_headers, $max_num ) as $i => $chunk ) { // Skip the first chunk (it will get used in the original wp_mail() call) if ( 0 === $i ) { $first_chunk = $chunk; continue; } // Send out the chunk $chunk_headers = array_merge( $old_headers, $chunk ); // Recursion alert, but should be OK! wp_mail( $args['to'], $args['subject'], $args['message'], $chunk_headers, $args['attachments'] ); } // Set headers to old headers + the $first_chunk of Bcc's $args['headers'] = array_merge( $old_headers, $first_chunk ); // Return the reduced args, with the first chunk of Bcc's return $args; } /** * Return the string used for the bbPress specific X-header. * * @since 2.6.0 bbPress (r6919) * * @return string */ function bbp_get_email_header() { return apply_filters( 'bbp_get_email_header', 'X-bbPress: ' . bbp_get_version() ); } /** Login *********************************************************************/ /** * Return a clean and reliable logout URL * * This function is used to filter `logout_url`. If no $redirect_to value is * passed, it will default to the request uri, then the forum root. * * See: `wp_logout_url()` * * @since 2.1.0 bbPress (2815) * * @param string $url URL used to log out * @param string $redirect_to Where to redirect to? * * @return string The url */ function bbp_logout_url( $url = '', $redirect_to = '' ) { // If there is no redirect in the URL, let's add one... if ( ! strstr( $url, 'redirect_to' ) ) { // Get the forum root, to maybe use as a default $forum_root = bbp_get_root_url(); // No redirect passed, so check referer and fallback to request uri if ( empty( $redirect_to ) ) { // Check for a valid referer $redirect_to = wp_get_referer(); // Fallback to request uri if invalid referer if ( false === $redirect_to ) { $redirect_to = bbp_get_url_scheme() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } } // Filter the $redirect_to destination $filtered = apply_filters( 'bbp_logout_url_redirect_to', $redirect_to ); // Validate $redirect_to, default to root $validated = wp_validate_redirect( $filtered, $forum_root ); // Assemble $redirect_to and add it (encoded) to full $url $appended = add_query_arg( array( 'loggedout' => 'true' ), $validated ); $encoded = urlencode( $appended ); $url = add_query_arg( array( 'redirect_to' => $encoded ), $url ); } // Filter & return return apply_filters( 'bbp_logout_url', $url, $redirect_to ); } /** Queries *******************************************************************/ /** * Merge user defined arguments into defaults array. * * This function is used throughout bbPress to allow for either a string or array * to be merged into another array. It is identical to wp_parse_args() except * it allows for arguments to be passively or aggressively filtered using the * optional $filter_key parameter. * * @since 2.1.0 bbPress (r3839) * * @param string|array $args Value to merge with $defaults * @param array $defaults Array that serves as the defaults. * @param string $filter_key String to key the filters from * @return array Merged user defined values with defaults. */ function bbp_parse_args( $args, $defaults = array(), $filter_key = '' ) { // Setup a temporary array from $args if ( is_object( $args ) ) { $r = get_object_vars( $args ); } elseif ( is_array( $args ) ) { $r =& $args; } else { wp_parse_str( $args, $r ); } // Passively filter the args before the parse if ( ! empty( $filter_key ) ) { $r = apply_filters( "bbp_before_{$filter_key}_parse_args", $r, $args, $defaults ); } // Parse if ( is_array( $defaults ) && ! empty( $defaults ) ) { $r = array_merge( $defaults, $r ); } // Aggressively filter the args after the parse if ( ! empty( $filter_key ) ) { $r = apply_filters( "bbp_after_{$filter_key}_parse_args", $r, $args, $defaults ); } // Return the parsed results return $r; } /** * Adds ability to include or exclude specific post_parent ID's * * @since 2.0.0 bbPress (r2996) * * @deprecated 2.5.8 bbPress (r5814) * * @global WP $wp * @param string $where * @param WP_Query $object * @return string */ function bbp_query_post_parent__in( $where, $object = '' ) { global $wp; // Noop if WP core supports this already if ( in_array( 'post_parent__in', $wp->private_query_vars, true ) ) { return $where; } // Bail if no object passed if ( empty( $object ) ) { return $where; } // Only 1 post_parent so return $where if ( is_numeric( $object->query_vars['post_parent'] ) ) { return $where; } // Get the DB $bbp_db = bbp_db(); // Including specific post_parent's if ( ! empty( $object->query_vars['post_parent__in'] ) ) { $ids = implode( ',', wp_parse_id_list( $object->query_vars['post_parent__in'] ) ); $where .= " AND {$bbp_db->posts}.post_parent IN ($ids)"; // Excluding specific post_parent's } elseif ( ! empty( $object->query_vars['post_parent__not_in'] ) ) { $ids = implode( ',', wp_parse_id_list( $object->query_vars['post_parent__not_in'] ) ); $where .= " AND {$bbp_db->posts}.post_parent NOT IN ($ids)"; } // Return possibly modified $where return $where; } /** * Query the DB and get the last public post_id that has parent_id as post_parent * * @since 2.0.0 bbPress (r2868) * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects * * @param int $parent_id Parent id. * @param string $post_type Post type. Defaults to 'post'. * @return int The last active post_id */ function bbp_get_public_child_last_id( $parent_id = 0, $post_type = 'post' ) { // Bail if nothing passed if ( empty( $parent_id ) ) { return false; } // Which statuses switch ( $post_type ) { // Forum case bbp_get_forum_post_type() : $post_status = bbp_get_public_forum_statuses(); break; // Topic case bbp_get_topic_post_type() : $post_status = bbp_get_public_topic_statuses(); break; // Reply case bbp_get_reply_post_type() : default : $post_status = bbp_get_public_reply_statuses(); break; } $query = new WP_Query( array( 'fields' => 'ids', 'post_parent' => $parent_id, 'post_status' => $post_status, 'post_type' => $post_type, 'posts_per_page' => 1, 'orderby' => array( 'post_date' => 'DESC', 'ID' => 'DESC' ), // Performance 'suppress_filters' => true, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'ignore_sticky_posts' => true, 'no_found_rows' => true ) ); $child_id = array_shift( $query->posts ); unset( $query ); // Filter & return return (int) apply_filters( 'bbp_get_public_child_last_id', $child_id, $parent_id, $post_type ); } /** * Query the database for child counts, grouped by type & status * * @since 2.6.0 bbPress (r6826) * * @param int $parent_id */ function bbp_get_child_counts( $parent_id = 0 ) { // Create cache key $parent_id = absint( $parent_id ); $key = md5( serialize( array( 'parent_id' => $parent_id, 'post_type' => bbp_get_post_types() ) ) ); $last_changed = wp_cache_get_last_changed( 'bbpress_posts' ); $cache_key = "bbp_child_counts:{$key}:{$last_changed}"; // Check for cache and set if needed $retval = wp_cache_get( $cache_key, 'bbpress_posts' ); if ( false === $retval ) { // Setup the DB & query $bbp_db = bbp_db(); $sql = "SELECT p.post_type AS type, p.post_status AS status, COUNT( * ) AS count FROM {$bbp_db->posts} AS p LEFT JOIN {$bbp_db->postmeta} AS pm ON p.ID = pm.post_id AND pm.meta_key = %s WHERE pm.meta_value = %s GROUP BY p.post_status, p.post_type"; // Get prepare vars $post_type = get_post_type( $parent_id ); $meta_key = "_bbp_{$post_type}_id"; // Prepare & get results $query = $bbp_db->prepare( $sql, $meta_key, $parent_id ); $results = $bbp_db->get_results( $query, ARRAY_A ); // Setup return value $retval = wp_list_pluck( $results, 'type', 'type' ); $statuses = get_post_stati(); // Loop through results foreach ( $results as $row ) { // Setup empties if ( ! is_array( $retval[ $row['type'] ] ) ) { $retval[ $row['type'] ] = array_fill_keys( $statuses, 0 ); } // Set statuses $retval[ $row['type'] ][ $row['status'] ] = bbp_number_not_negative( $row['count'] ); } // Always cache the results wp_cache_set( $cache_key, $retval, 'bbpress_posts' ); } // Make sure results are INTs return (array) apply_filters( 'bbp_get_child_counts', $retval, $parent_id ); } /** * Filter a list of child counts, from `bbp_get_child_counts()` * * @since 2.6.0 bbPress (r6826) * * @param int $parent_id ID of post to get child counts from * @param array $types Optional. An array of post types to filter by * @param array $statuses Optional. An array of post statuses to filter by * * @return array A list of objects or object fields. */ function bbp_filter_child_counts_list( $parent_id = 0, $types = array( 'post' ), $statuses = array() ) { // Setup local vars $retval = array(); $types = array_flip( (array) $types ); $statuses = array_flip( (array) $statuses ); $counts = bbp_get_child_counts( $parent_id ); // Loop through counts by type foreach ( $counts as $type => $type_counts ) { // Skip if not this type if ( ! isset( $types[ $type ] ) ) { continue; } // Maybe filter statuses if ( ! empty( $statuses ) ) { $type_counts = array_intersect_key( $type_counts, $statuses ); } // Add type counts to return array $retval[ $type ] = $type_counts; } // Filter & return return (array) apply_filters( 'bbp_filter_child_counts_list', $retval, $parent_id, $types, $statuses ); } /** * Query the DB and get a count of public children * * @since 2.0.0 bbPress (r2868) * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects * * @param int $parent_id Parent id. * @param string $post_type Post type. Defaults to 'post'. * @return int The number of children */ function bbp_get_public_child_count( $parent_id = 0, $post_type = 'post' ) { // Bail if nothing passed if ( empty( $post_type ) ) { return false; } // Which statuses switch ( $post_type ) { // Forum case bbp_get_forum_post_type() : $post_status = bbp_get_public_forum_statuses(); break; // Topic case bbp_get_topic_post_type() : $post_status = bbp_get_public_topic_statuses(); break; // Reply case bbp_get_reply_post_type() : default : $post_status = bbp_get_public_reply_statuses(); break; } // Get counts $counts = bbp_filter_child_counts_list( $parent_id, $post_type, $post_status ); $child_count = isset( $counts[ $post_type ] ) ? bbp_number_not_negative( array_sum( array_values( $counts[ $post_type ] ) ) ) : 0; // Filter & return return (int) apply_filters( 'bbp_get_public_child_count', $child_count, $parent_id, $post_type ); } /** * Query the DB and get a count of public children * * @since 2.0.0 bbPress (r2868) * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects * * @param int $parent_id Parent id. * @param string $post_type Post type. Defaults to 'post'. * @return int The number of children */ function bbp_get_non_public_child_count( $parent_id = 0, $post_type = 'post' ) { // Bail if nothing passed if ( empty( $parent_id ) || empty( $post_type ) ) { return false; } // Which statuses switch ( $post_type ) { // Forum case bbp_get_forum_post_type() : $post_status = bbp_get_non_public_forum_statuses(); break; // Topic case bbp_get_topic_post_type() : $post_status = bbp_get_non_public_topic_statuses(); break; // Reply case bbp_get_reply_post_type() : $post_status = bbp_get_non_public_reply_statuses(); break; // Any default : $post_status = bbp_get_public_status_id(); break; } // Get counts $counts = bbp_filter_child_counts_list( $parent_id, $post_type, $post_status ); $child_count = isset( $counts[ $post_type ] ) ? bbp_number_not_negative( array_sum( array_values( $counts[ $post_type ] ) ) ) : 0; // Filter & return return (int) apply_filters( 'bbp_get_non_public_child_count', $child_count, $parent_id, $post_type ); } /** * Query the DB and get the child id's of public children * * @since 2.0.0 bbPress (r2868) * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects * * @param int $parent_id Parent id. * @param string $post_type Post type. Defaults to 'post'. * * @return array The array of children */ function bbp_get_public_child_ids( $parent_id = 0, $post_type = 'post' ) { // Bail if nothing passed if ( empty( $parent_id ) || empty( $post_type ) ) { return array(); } // Which statuses switch ( $post_type ) { // Forum case bbp_get_forum_post_type() : $post_status = bbp_get_public_forum_statuses(); break; // Topic case bbp_get_topic_post_type() : $post_status = bbp_get_public_topic_statuses(); break; // Reply case bbp_get_reply_post_type() : default : $post_status = bbp_get_public_reply_statuses(); break; } $query = new WP_Query( array( 'fields' => 'ids', 'post_parent' => $parent_id, 'post_status' => $post_status, 'post_type' => $post_type, 'posts_per_page' => -1, 'orderby' => array( 'post_date' => 'DESC', 'ID' => 'DESC' ), // Performance 'nopaging' => true, 'suppress_filters' => true, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'ignore_sticky_posts' => true, 'no_found_rows' => true ) ); $child_ids = ! empty( $query->posts ) ? $query->posts : array(); unset( $query ); // Filter & return return (array) apply_filters( 'bbp_get_public_child_ids', $child_ids, $parent_id, $post_type ); } /** * Query the DB and get the child id's of all children * * @since 2.0.0 bbPress (r3325) * * @param int $parent_id Parent id * @param string $post_type Post type. Defaults to 'post' * * @return array The array of children */ function bbp_get_all_child_ids( $parent_id = 0, $post_type = 'post' ) { // Bail if nothing passed if ( empty( $parent_id ) || empty( $post_type ) ) { return array(); } // Make cache key $not_in = array( 'draft', 'future' ); $key = md5( serialize( array( 'parent_id' => $parent_id, 'post_type' => $post_type, 'post_status' => $not_in ) ) ); // Check last changed $last_changed = wp_cache_get_last_changed( 'bbpress_posts' ); $cache_key = "bbp_child_ids:{$key}:{$last_changed}"; // Check for cache and set if needed $child_ids = wp_cache_get( $cache_key, 'bbpress_posts' ); // Not already cached if ( false === $child_ids ) { // Join post statuses to specifically exclude together $post_status = "'" . implode( "', '", $not_in ) . "'"; $bbp_db = bbp_db(); // Note that we can't use WP_Query here thanks to post_status assumptions $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 ); $child_ids = (array) $bbp_db->get_col( $query ); // Always cache the results wp_cache_set( $cache_key, $child_ids, 'bbpress_posts' ); } // Make sure results are INTs $child_ids = wp_parse_id_list( $child_ids ); // Filter & return return (array) apply_filters( 'bbp_get_all_child_ids', $child_ids, $parent_id, $post_type ); } /** * Prime familial post caches. * * This function uses _prime_post_caches() to prepare the object cache for * imminent requests to post objects that aren't naturally cached by the primary * WP_Query calls themselves. Post author caches are also primed. * * This is triggered when a `update_post_family_cache` argument is set to true. * * Also see: bbp_update_post_author_caches() * * @since 2.6.0 bbPress (r6699) * * @param array $objects Array of objects, fresh from a query * * @return bool True if some IDs were cached */ function bbp_update_post_family_caches( $objects = array() ) { // Bail if no posts if ( empty( $objects ) ) { return false; } // Default value $post_ids = array(); // Filter the types of IDs to prime $ids = apply_filters( 'bbp_update_post_family_caches', array( '_bbp_last_active_id', '_bbp_last_reply_id', '_bbp_last_topic_id', '_bbp_reply_to' ), $objects ); // Get the last active IDs foreach ( $objects as $object ) { $object = get_post( $object ); // Skip if post ID is empty. if ( empty( $object->ID ) ) { continue; } // Meta IDs foreach ( $ids as $key ) { $post_ids[] = get_post_meta( $object->ID, $key, true ); } // This post ID is already cached, but the post author may not be $post_ids[] = $object->ID; } // Unique, non-zero values $post_ids = bbp_get_unique_array_values( $post_ids ); // Bail if no IDs to prime if ( empty( $post_ids ) ) { return false; } // Prime post caches _prime_post_caches( $post_ids, true, true ); // Prime post author caches bbp_update_post_author_caches( $post_ids ); // Return return true; } /** * Prime post author caches. * * This function uses cache_users() to prepare the object cache for * imminent requests to user objects that aren't naturally cached by the primary * WP_Query calls themselves. * * This is triggered when a `update_post_author_cache` argument is set to true. * * @since 2.6.0 bbPress (r6699) * * @param array $objects Array of objects, fresh from a query * * @return bool True if some IDs were cached */ function bbp_update_post_author_caches( $objects = array() ) { // Bail if no posts if ( empty( $objects ) ) { return false; } // Default value $user_ids = array(); // Get the user IDs (could use wp_list_pluck() if this is ever a bottleneck) foreach ( $objects as $object ) { $object = get_post( $object ); // Skip if post does not have an author ID. if ( empty( $object->post_author ) ) { continue; } // If post exists, add post author to the array. $user_ids[] = (int) $object->post_author; } // Unique, non-zero values $user_ids = bbp_get_unique_array_values( $user_ids ); // Bail if no IDs to prime if ( empty( $user_ids ) ) { return false; } // Try to prime user caches cache_users( $user_ids ); // Return return true; } /** Globals *******************************************************************/ /** * Get the unfiltered value of a global $post's key * * Used most frequently when editing a forum/topic/reply * * @since 2.1.0 bbPress (r3694) * * @param string $field Name of the key * @param string $context How to sanitize - raw|edit|db|display|attribute|js * @return string Field value */ function bbp_get_global_post_field( $field = 'ID', $context = 'edit' ) { // Get the post, and maybe get a field from it $post = get_post(); $retval = isset( $post->{$field} ) ? sanitize_post_field( $field, $post->{$field}, $post->ID, $context ) : ''; // Filter & return return apply_filters( 'bbp_get_global_post_field', $retval, $post, $field, $context ); } /** Nonces ********************************************************************/ /** * Makes sure the user requested an action from another page on this site. * * To avoid security exploits within the theme. * * @since 2.1.0 bbPress (r4022) * * @param string $action Action nonce * @param string $query_arg where to look for nonce in $_REQUEST */ function bbp_verify_nonce_request( $action = '', $query_arg = '_wpnonce' ) { /** Home URL **************************************************************/ // Parse home_url() into pieces to remove query-strings, strange characters, // and other funny things that plugins might to do to it. $parsed_home = parse_url( home_url( '/', ( is_ssl() ? 'https' : 'http' ) ) ); // Maybe include the port, if it's included if ( isset( $parsed_home['port'] ) ) { $parsed_host = $parsed_home['host'] . ':' . $parsed_home['port']; } else { $parsed_host = $parsed_home['host']; } // Set the home URL for use in comparisons $home_url = trim( strtolower( $parsed_home['scheme'] . '://' . $parsed_host . $parsed_home['path'] ), '/' ); /** Requested URL *********************************************************/ // Maybe include the port, if it's included in home_url() if ( isset( $parsed_home['port'] ) && false === strpos( $_SERVER['HTTP_HOST'], ':' ) ) { $request_host = $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT']; } else { $request_host = $_SERVER['HTTP_HOST']; } // Build the currently requested URL $scheme = bbp_get_url_scheme(); $requested_url = strtolower( $scheme . $request_host . $_SERVER['REQUEST_URI'] ); /** Look for match ********************************************************/ /** * Filters the requested URL being nonce-verified. * * Useful for configurations like reverse proxying. * * @since 2.2.0 bbPress (r4361) * * @param string $requested_url The requested URL. */ $matched_url = apply_filters( 'bbp_verify_nonce_request_url', $requested_url ); // Check the nonce $result = isset( $_REQUEST[ $query_arg ] ) ? wp_verify_nonce( $_REQUEST[ $query_arg ], $action ) : false; // Nonce check failed if ( empty( $result ) || empty( $action ) || ( strpos( $matched_url, $home_url ) !== 0 ) ) { $result = false; } /** * Fires at the end of the nonce verification check. * * @since 2.1.0 bbPress (r4023) * * @param string $action Action nonce. * @param bool $result Boolean result of nonce verification. */ do_action( 'bbp_verify_nonce_request', $action, $result ); return $result; } /** Feeds *********************************************************************/ /** * This function is hooked into the WordPress 'request' action and is * responsible for sniffing out the query vars and serving up RSS2 feeds if * the stars align and the user has requested a feed of any bbPress type. * * @since 2.0.0 bbPress (r3171) * * @param array $query_vars * @return array */ function bbp_request_feed_trap( $query_vars = array() ) { // Looking at a feed if ( isset( $query_vars['feed'] ) ) { // Forum/Topic/Reply Feed if ( isset( $query_vars['post_type'] ) ) { // Matched post type $post_type = false; // Post types to check $post_types = array( bbp_get_forum_post_type(), bbp_get_topic_post_type(), bbp_get_reply_post_type() ); // Cast query vars as array outside of foreach loop $qv_array = (array) $query_vars['post_type']; // Check if this query is for a bbPress post type foreach ( $post_types as $bbp_pt ) { if ( in_array( $bbp_pt, $qv_array, true ) ) { $post_type = $bbp_pt; break; } } // Looking at a bbPress post type if ( ! empty( $post_type ) ) { // Supported select query vars $select_query_vars = array( 'p' => false, 'name' => false, $post_type => false, ); // Setup matched variables to select foreach ( $query_vars as $key => $value ) { if ( isset( $select_query_vars[ $key ] ) ) { $select_query_vars[ $key ] = $value; } } // Remove any empties $select_query_vars = array_filter( $select_query_vars ); // What bbPress post type are we looking for feeds on? switch ( $post_type ) { // Forum case bbp_get_forum_post_type() : // Define local variable(s) $meta_query = array(); // Single forum if ( ! empty( $select_query_vars ) ) { // Load up our own query query_posts( array_merge( array( 'post_type' => bbp_get_forum_post_type(), 'feed' => true ), $select_query_vars ) ); // Restrict to specific forum ID $meta_query = array( array( 'key' => '_bbp_forum_id', 'value' => bbp_get_forum_id(), 'type' => 'NUMERIC', 'compare' => '=' ) ); } // Only forum replies if ( ! empty( $_GET['type'] ) && ( bbp_get_reply_post_type() === $_GET['type'] ) ) { // The query $the_query = array( 'author' => 0, 'feed' => true, 'post_type' => bbp_get_reply_post_type(), 'post_parent' => 'any', 'post_status' => bbp_get_public_reply_statuses(), 'posts_per_page' => bbp_get_replies_per_rss_page(), 'order' => 'DESC', 'meta_query' => $meta_query ); // Output the feed bbp_display_replies_feed_rss2( $the_query ); // Only forum topics } elseif ( ! empty( $_GET['type'] ) && ( bbp_get_topic_post_type() === $_GET['type'] ) ) { // The query $the_query = array( 'author' => 0, 'feed' => true, 'post_type' => bbp_get_topic_post_type(), 'post_parent' => bbp_get_forum_id(), 'post_status' => bbp_get_public_topic_statuses(), 'posts_per_page' => bbp_get_topics_per_rss_page(), 'order' => 'DESC' ); // Output the feed bbp_display_topics_feed_rss2( $the_query ); // All forum topics and replies } else { // Exclude private/hidden forums if not looking at single if ( empty( $select_query_vars ) ) { $meta_query = array( bbp_exclude_forum_ids( 'meta_query' ) ); } // The query $the_query = array( 'author' => 0, 'feed' => true, 'post_type' => array( bbp_get_reply_post_type(), bbp_get_topic_post_type() ), 'post_parent' => 'any', 'post_status' => bbp_get_public_topic_statuses(), 'posts_per_page' => bbp_get_replies_per_rss_page(), 'order' => 'DESC', 'meta_query' => $meta_query ); // Output the feed bbp_display_replies_feed_rss2( $the_query ); } break; // Topic feed - Show replies case bbp_get_topic_post_type() : // Single topic if ( ! empty( $select_query_vars ) ) { // Load up our own query query_posts( array_merge( array( 'post_type' => bbp_get_topic_post_type(), 'feed' => true ), $select_query_vars ) ); // Output the feed bbp_display_replies_feed_rss2( array( 'feed' => true ) ); // All topics } else { // The query $the_query = array( 'author' => 0, 'feed' => true, 'post_parent' => 'any', 'posts_per_page' => bbp_get_topics_per_rss_page(), 'show_stickies' => false ); // Output the feed bbp_display_topics_feed_rss2( $the_query ); } break; // Replies case bbp_get_reply_post_type() : // The query $the_query = array( 'posts_per_page' => bbp_get_replies_per_rss_page(), 'meta_query' => array( array() ), 'feed' => true ); // All replies if ( empty( $select_query_vars ) ) { bbp_display_replies_feed_rss2( $the_query ); } break; } } // Single Topic Vview } elseif ( isset( $query_vars[ bbp_get_view_rewrite_id() ] ) ) { // Get the view $view = $query_vars[ bbp_get_view_rewrite_id() ]; // We have a view to display a feed if ( ! empty( $view ) ) { // Get the view query $the_query = bbp_get_view_query_args( $view ); // Output the feed bbp_display_topics_feed_rss2( $the_query ); } } // @todo User profile feeds } // No feed so continue on return $query_vars; } /** Templates ******************************************************************/ /** * Used to guess if page exists at requested path * * @since 2.0.0 bbPress (r3304) * * @param string $path * @return mixed False if no page, Page object if true */ function bbp_get_page_by_path( $path = '' ) { // Default to false $retval = false; // Path is not empty if ( ! empty( $path ) ) { // Pretty permalinks are on so path might exist if ( get_option( 'permalink_structure' ) ) { $retval = get_page_by_path( $path ); } } // Filter & return return apply_filters( 'bbp_get_page_by_path', $retval, $path ); } /** * Sets the 404 status. * * Used primarily with topics/replies inside hidden forums. * * @since 2.0.0 bbPress (r3051) * @since 2.6.0 bbPress (r6583) Use status_header() & nocache_headers() * * @param WP_Query $query The query being checked * * @return bool Always returns true */ function bbp_set_404( $query = null ) { // Global fallback if ( empty( $query ) ) { $query = bbp_get_wp_query(); } // Setup environment $query->set_404(); // Setup request status_header( 404 ); nocache_headers(); } /** * Sets the 200 status header. * * @since 2.6.0 bbPress (r6583) */ function bbp_set_200() { status_header( 200 ); } /** * Maybe handle the default 404 handling for some bbPress conditions * * Some conditions (like private/hidden forums and edits) have their own checks * on `bbp_template_redirect` and are not currently 404s. * * @since 2.6.0 bbPress (r6555) * * @param bool $override Whether to override the default handler * @param WP_Query $wp_query The posts query being referenced * * @return bool False to leave alone, true to override */ function bbp_pre_handle_404( $override = false, $wp_query = false ) { // Handle a bbPress 404 condition if ( isset( $wp_query->bbp_is_404 ) ) { // Either force a 404 when 200, or a 200 when 404 $override = ( true === $wp_query->bbp_is_404 ) ? bbp_set_404( $wp_query ) : bbp_set_200(); } // Return, maybe overridden return $override; } /** * Maybe pre-assign the posts that are returned from a WP_Query. * * This effectively short-circuits the default query for posts, which is * currently only used to avoid calling the main query when it's not necessary. * * @since 2.6.0 bbPress (r6580) * * @param mixed $posts Default null. Array of posts (possibly empty) * @param WP_Query $wp_query * * @return mixed Null if no override. Array if overridden. */ function bbp_posts_pre_query( $posts = null, $wp_query = false ) { // Custom 404 handler is set, so set posts to empty array to avoid 2 queries if ( ! empty( $wp_query->bbp_is_404 ) ) { $posts = array(); } // Return, maybe overridden return $posts; } /** * Get scheme for a URL based on is_ssl() results. * * @since 2.6.0 bbPress (r6759) * * @return string https:// if is_ssl(), otherwise http:// */ function bbp_get_url_scheme() { return is_ssl() ? 'https://' : 'http://'; } /** Titles ********************************************************************/ /** * Is a title longer that the maximum title length? * * Uses mb_strlen() in `8bit` mode to treat strings as raw. This matches the * behavior present in Comments, PHPMailer, RandomCompat, and others. * * @since 2.6.0 bbPress (r6783) * * @param string $title * @return bool */ function bbp_is_title_too_long( $title = '' ) { $max = bbp_get_title_max_length(); $len = mb_strlen( $title, '8bit' ); $result = ( $len > $max ); // Filter & return return (bool) apply_filters( 'bbp_is_title_too_long', $result, $title, $max, $len ); }