Skip to:
Content

bbPress.org

Changeset 7467


Ignore:
Timestamp:
09/10/2026 12:35:43 AM (12 days ago)
Author:
johnjamesjacoby
Message:

Counts: synchronize metadata across concurrent writes.

Introduce bbp_bump_count_meta() to update existing numeric metadata with bounded compare-and-swap retries while preserving WordPress metadata filters, actions, sanitization, and cache invalidation. Apply atomic differences to forum, topic, reply, ancestor, and user contribution counts.

Reconcile counts, engagements, and voices across status transitions, permanent deletion, author changes and reassignment, moderator move/merge/split operations, and repair recounts. Correct recursive forum totals and reply visibility, and preserve term-backed favorites and subscriptions during engagement rebuilds.

Document the count-hook compatibility changes and new helper arguments, and add regression coverage for single-site and multisite workflows.

In trunk, for 2.7.

Props alex-ye.
Fixes #2233.
Fixes #3678.

Location:
trunk
Files:
1 added
22 edited

Legend:

Unmodified
Added
Removed
  • trunk/CHANGELOG.md

    r7465 r7467  
    6464- Kept public, hidden, and author contribution counts accurate when topics and
    6565  replies change moderation status or are permanently deleted.
    66 - Kept subforum counts accurate across public, private, hidden, trashed, moved,
    67   restored, and permanently deleted forums.
     66- Prevented simultaneous topic and reply submissions from overwriting each
     67  other's existing stored count changes.
     68- Kept forum, ancestor, contribution, engagement, and voice counts synchronized
     69  when moderators move replies or merge and split topics.
     70- Reconciled replacement-user contribution, engagement, and voice counts when
     71  a deleted user's posts are reassigned.
     72- Excluded replies beneath non-public topics from forum reply counts during
     73  moderation and repair recounts.
     74- Kept subforum counts and recursive forum aggregates accurate across public,
     75  private, hidden, trashed, moved, restored, and permanently deleted forums.
    6876- Corrected count repair tools to clear stale metadata caches, exclude anonymous
    6977  authors from voice counts, honor reply-status filters, rebuild private and
    7078  hidden forums, and preserve similarly named metadata on other post types.
     79- Made topic engagement and voice recounts honor filtered public reply statuses.
     80- Preserved term-backed favorites and subscriptions during engagement recounts.
    7181
    7282- Cache a distinct count of current-site forum-role holders for forum statistics,
  • trunk/src/bbpress.php

    r7445 r7467  
    863863                // Forum
    864864                $count['object_subtype'] = bbp_get_forum_post_type();
    865                 register_meta( 'post', '_bbp_topic_count',           $count );
    866                 register_meta( 'post', '_bbp_reply_count',           $count );
    867                 register_meta( 'post', '_bbp_total_topic_count',     $count );
    868                 register_meta( 'post', '_bbp_total_reply_count',     $count );
    869                 register_meta( 'post', '_bbp_topic_count_hidden',    $count );
    870                 register_meta( 'post', '_bbp_reply_count_hidden',    $count );
    871                 register_meta( 'post', '_bbp_forum_subforum_count',  $count );
     865                register_meta( 'post', '_bbp_topic_count',              $count );
     866                register_meta( 'post', '_bbp_reply_count',              $count );
     867                register_meta( 'post', '_bbp_total_topic_count',        $count );
     868                register_meta( 'post', '_bbp_total_reply_count',        $count );
     869                register_meta( 'post', '_bbp_topic_count_hidden',       $count );
     870                register_meta( 'post', '_bbp_reply_count_hidden',       $count );
     871                register_meta( 'post', '_bbp_total_topic_count_hidden', $count );
     872                register_meta( 'post', '_bbp_total_reply_count_hidden', $count );
     873                register_meta( 'post', '_bbp_forum_subforum_count',     $count );
    872874
    873875                // Topic
  • trunk/src/includes/admin/tools/repair.php

    r7453 r7467  
    444444                                                LEFT JOIN `{$bbp_db->posts}` AS `posts` ON `posts`.`ID` = `postmeta`.`post_id`
    445445                                                WHERE `posts`.`post_type` = '{$fpt}'
    446                                                 AND `postmeta`.`meta_key` IN ( '_bbp_topic_count', '_bbp_total_topic_count', '_bbp_topic_count_hidden' )";
     446                                                AND `postmeta`.`meta_key` IN ( '_bbp_topic_count', '_bbp_total_topic_count', '_bbp_topic_count_hidden', '_bbp_total_topic_count_hidden' )";
    447447        if ( false === $bbp_db->query( $sql_delete ) ) {
    448448                return array( 1, sprintf( $statement, $result ) );
  • trunk/src/includes/common/engagements.php

    r7380 r7467  
    430430         *
    431431         * @since 2.6.0 bbPress (r6737)
     432         * @since 2.6.16 Limit removal to the requested relationship taxonomy.
    432433         *
    433434         * @param int    $object_id The object id.
     
    439440         */
    440441        public function remove_object_from_all_users( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {
    441                 return wp_delete_object_term_relationships( $object_id, get_object_taxonomies( 'bbp_user' ) );
     442                $tax_key = "{$meta_key}_{$meta_type}";
     443                $this->jit_taxonomy( $tax_key );
     444
     445                return wp_delete_object_term_relationships( $object_id, $tax_key );
    442446        }
    443447
  • trunk/src/includes/common/functions.php

    r7452 r7467  
    276276                $was_public        = in_array( $old_status, bbp_get_public_topic_statuses(), true );
    277277                $is_public         = in_array( $new_status, bbp_get_public_topic_statuses(), true );
     278                $was_hidden        = in_array( $old_status, bbp_get_non_public_topic_statuses(), true );
     279                $is_hidden         = in_array( $new_status, bbp_get_non_public_topic_statuses(), true );
    278280                $public_difference = (int) $is_public - (int) $was_public;
    279                 $hidden_difference = $is_new
    280                         ? (int) ! $is_public
    281                         : - $public_difference;
    282 
    283                 // A new topic or public boundary crossing changes at least one count
     281                $hidden_difference = (int) $is_hidden - (int) $was_hidden;
     282
     283                // A new topic or count boundary crossing changes at least one count
    284284                if ( ! empty( $public_difference ) || ! empty( $hidden_difference ) ) {
    285285                        $forum_id = $is_new
     
    302302                        }
    303303
    304                         // Topic approval does not change its replies' statuses
    305                         if ( ! $is_new && in_array( bbp_get_pending_status_id(), array( $old_status, $new_status ), true ) ) {
    306                                 $reply_count = bbp_get_public_child_count( $post->ID, bbp_get_reply_post_type() );
    307                                 bbp_bump_forum_reply_count( $forum_id, $reply_count * $public_difference );
     304                        // Recount replies after their parent topic crosses the public boundary
     305                        if ( ! $is_new && ! empty( $forum_id ) && ! empty( $public_difference ) ) {
     306                                bbp_update_forum_reply_count( $forum_id, true );
    308307                        }
    309308                }
     
    313312                $was_public        = in_array( $old_status, bbp_get_public_reply_statuses(), true );
    314313                $is_public         = in_array( $new_status, bbp_get_public_reply_statuses(), true );
     314                $was_hidden        = in_array( $old_status, bbp_get_non_public_reply_statuses(), true );
     315                $is_hidden         = in_array( $new_status, bbp_get_non_public_reply_statuses(), true );
    315316                $public_difference = (int) $is_public - (int) $was_public;
    316                 $hidden_difference = $is_new
    317                         ? (int) ! $is_public
    318                         : - $public_difference;
    319 
    320                 // A new reply or public boundary crossing changes at least one count
     317                $hidden_difference = (int) $is_hidden - (int) $was_hidden;
     318
     319                // A new reply or count boundary crossing changes at least one count
    321320                if ( ! empty( $public_difference ) || ! empty( $hidden_difference ) ) {
    322321                        $topic_id = $is_new
     
    326325                                ? bbp_get_topic_forum_id( $topic_id )
    327326                                : bbp_get_reply_forum_id( $post->ID );
     327                        $forum_public_difference = bbp_is_topic_public( $topic_id )
     328                                ? $public_difference
     329                                : 0;
    328330
    329331                        // Update the topic's public reply count
     
    338340
    339341                        // Update the forum's public reply count
    340                         if ( ! empty( $forum_id ) && ! empty( $public_difference ) ) {
    341                                 bbp_bump_forum_reply_count( $forum_id, $public_difference );
     342                        if ( ! empty( $forum_id ) && ! empty( $forum_public_difference ) ) {
     343                                bbp_bump_forum_reply_count( $forum_id, $forum_public_difference );
    342344                        }
    343345
  • trunk/src/includes/core/abstraction.php

    r7447 r7467  
    147147function bbp_db() {
    148148        return bbp_get_global_object( 'wpdb', 'WPDB' );
     149}
     150
     151/**
     152 * Atomically bump a numeric metadata value using compare-and-swap retries.
     153 *
     154 * Count updates normally require reading a value, changing it in PHP, and
     155 * writing it back. Two requests can read the same value and overwrite one
     156 * another's changes. This function avoids that lost update by making the write
     157 * conditional on the value that was read. If another request changes the value
     158 * first, the condition matches no rows, so the current value is read directly
     159 * from the database and the calculation is retried.
     160 *
     161 * The first read uses the WordPress metadata API and its cache. Missing
     162 * metadata is added through add_metadata() so its standard lifecycle continues
     163 * to run. Existing metadata uses a conditional database update while preserving
     164 * the standard update metadata short-circuit filter and before/after actions.
     165 * Before actions run for every conditional attempt, while after actions run
     166 * only after a successful write. Metadata caches are cleared between attempts
     167 * and after successful writes. Values are sanitized through sanitize_meta(),
     168 * cast to integers, and prevented from falling below zero.
     169 *
     170 * Retries are bounded and filterable. This function does not lock rows or hold
     171 * a database transaction open, and returns false when there is no change, a
     172 * database operation fails, or all attempts lose to concurrent writes. It is
     173 * intended for uniquely keyed numeric count metadata; WordPress metadata tables
     174 * do not enforce uniqueness during simultaneous first-time inserts.
     175 *
     176 * @since 2.6.16
     177 *
     178 * @see https://bbpress.trac.wordpress.org/ticket/3678
     179 *
     180 * @param string $meta_type  Type of object metadata is for.
     181 * @param int    $object_id  ID of the object metadata is for.
     182 * @param string $meta_key   Metadata key.
     183 * @param int    $difference Amount to add to the stored value.
     184 * @param int    $default    Existing value to use when metadata is missing.
     185 * @return bool True on success, false on failure or no change.
     186 */
     187function bbp_bump_count_meta( $meta_type = '', $object_id = 0, $meta_key = '', $difference = 1, $default = 0 ) {
     188
     189        $object_id  = (int) $object_id;
     190        $difference = (int) $difference;
     191        $default    = (int) $default;
     192
     193        // Bail if required values are missing
     194        if ( empty( $object_id ) || empty( $meta_key ) || empty( $difference ) ) {
     195                return false;
     196        }
     197
     198        /**
     199         * Short-circuits bumping numeric metadata.
     200         *
     201         * Returning a non-null value prevents the normal metadata update.
     202         *
     203         * @since 2.6.16
     204         *
     205         * @param null|bool $check      Whether to short-circuit the metadata update.
     206         * @param string    $meta_type  Type of object metadata is for.
     207         * @param int       $object_id  ID of the object metadata is for.
     208         * @param string    $meta_key   Metadata key.
     209         * @param int       $difference Amount to add to the stored value.
     210         * @param int       $default    Existing value to use when metadata is missing.
     211         */
     212        $check = apply_filters( 'bbp_pre_bump_count_meta', null, $meta_type, $object_id, $meta_key, $difference, $default );
     213        if ( null !== $check ) {
     214                return (bool) $check;
     215        }
     216
     217        /**
     218         * Filters the metadata types that support atomic count updates.
     219         *
     220         * @since 2.6.16
     221         *
     222         * @param array  $meta_types Supported metadata types.
     223         * @param string $meta_type  Requested metadata type.
     224         * @param int    $object_id  ID of the object metadata is for.
     225         * @param string $meta_key   Metadata key.
     226         */
     227        $meta_types = (array) apply_filters( 'bbp_bump_count_meta_types', array( 'post', 'user', 'term', 'comment' ), $meta_type, $object_id, $meta_key );
     228
     229        // Bail if the metadata type is unsupported
     230        if ( ! in_array( $meta_type, $meta_types, true ) ) {
     231                return false;
     232        }
     233
     234        $bbp_db     = bbp_db();
     235        $table_name = sanitize_key( $meta_type . 'meta' );
     236        $table      = isset( $bbp_db->{$table_name} ) ? $bbp_db->{$table_name} : '';
     237        $column     = sanitize_key( $meta_type . '_id' );
     238        $id_column  = ( 'user' === $meta_type ) ? 'umeta_id' : 'meta_id';
     239
     240        // Bail if the metadata table does not exist
     241        if ( empty( $table ) ) {
     242                return false;
     243        }
     244
     245        /**
     246         * Filters the maximum number of conditional metadata write attempts.
     247         *
     248         * @since 2.6.16
     249         *
     250         * @param int    $max_attempts Maximum number of attempts.
     251         * @param string $meta_type    Type of object metadata is for.
     252         * @param int    $object_id    ID of the object metadata is for.
     253         * @param string $meta_key     Metadata key.
     254         * @param int    $difference   Amount to add to the stored value.
     255         * @param int    $default      Existing value to use when metadata is missing.
     256         */
     257        $max_attempts = (int) apply_filters( 'bbp_bump_count_meta_max_attempts', 5, $meta_type, $object_id, $meta_key, $difference, $default );
     258        $max_attempts = max( 1, $max_attempts );
     259        $checked      = false;
     260        $subtype      = get_object_subtype( $meta_type, $object_id );
     261        $count_query  = $bbp_db->prepare( "SELECT meta_value FROM {$table} WHERE meta_key = %s AND {$column} = %d LIMIT 1", $meta_key, $object_id );
     262
     263        // Retry when another request updates the same value first
     264        for ( $attempt = 0; $attempt < $max_attempts; $attempt++ ) {
     265                if ( empty( $attempt ) ) {
     266                        $exists = metadata_exists( $meta_type, $object_id, $meta_key );
     267                        $count  = $exists
     268                                ? (int) get_metadata( $meta_type, $object_id, $meta_key, true )
     269                                : $default;
     270                } else {
     271                        $stored = $bbp_db->get_var( $count_query );
     272
     273                        // Bail on a database error
     274                        if ( ! empty( $bbp_db->last_error ) ) {
     275                                return false;
     276                        }
     277
     278                        $exists = null !== $stored;
     279                        $count  = $exists ? (int) $stored : $default;
     280                }
     281
     282                $new_count = sanitize_meta( $meta_key, bbp_number_not_negative( $count + $difference ), $meta_type, $subtype );
     283                $new_count = (int) $new_count;
     284
     285                // Allow metadata updates to be short-circuited as usual
     286                if ( ! $checked ) {
     287                        $checked = true;
     288                        $check   = apply_filters( "update_{$meta_type}_metadata", null, $object_id, $meta_key, $new_count, '' );
     289
     290                        if ( null !== $check ) {
     291                                return (bool) $check;
     292                        }
     293                }
     294
     295                // Bail if the count is already at its lower bound
     296                if ( $new_count === $count ) {
     297                        if ( ! empty( $attempt ) ) {
     298                                return false;
     299                        }
     300
     301                        $stored = $bbp_db->get_var( $count_query );
     302
     303                        // Bail on a database error
     304                        if ( ! empty( $bbp_db->last_error ) ) {
     305                                return false;
     306                        }
     307
     308                        $current_exists = null !== $stored;
     309                        $current_count  = $current_exists ? (int) $stored : $default;
     310
     311                        if ( ( $current_exists === $exists ) && ( $current_count === $count ) ) {
     312                                return false;
     313                        }
     314
     315                        wp_cache_delete( $object_id, $meta_type . '_meta' );
     316                        continue;
     317                }
     318
     319                // Add missing metadata using the standard WordPress lifecycle
     320                if ( ! $exists ) {
     321                        if ( ! empty( add_metadata( $meta_type, $object_id, $meta_key, $new_count, true ) ) ) {
     322                                return true;
     323                        }
     324
     325                        $stored = $bbp_db->get_var( $count_query );
     326
     327                        // Bail if the add failed without a concurrent insert or on a database error
     328                        if ( ! empty( $bbp_db->last_error ) || ( null === $stored ) ) {
     329                                return false;
     330                        }
     331
     332                        wp_cache_delete( $object_id, $meta_type . '_meta' );
     333                        continue;
     334                }
     335
     336                $meta_ids = $bbp_db->get_col( $bbp_db->prepare( "SELECT {$id_column} FROM {$table} WHERE meta_key = %s AND {$column} = %d", $meta_key, $object_id ) );
     337
     338                // Bail on a database error
     339                if ( ! empty( $bbp_db->last_error ) ) {
     340                        return false;
     341                }
     342
     343                // Retry if metadata was removed after the cached existence check
     344                if ( empty( $meta_ids ) ) {
     345                        wp_cache_delete( $object_id, $meta_type . '_meta' );
     346                        continue;
     347                }
     348
     349                // Run the standard actions immediately before the conditional update
     350                foreach ( $meta_ids as $meta_id ) {
     351                        do_action( "update_{$meta_type}_meta", $meta_id, $object_id, $meta_key, $new_count );
     352
     353                        if ( 'post' === $meta_type ) {
     354                                do_action( 'update_postmeta', $meta_id, $object_id, $meta_key, $new_count );
     355                        }
     356                }
     357
     358                // Compare count metadata numerically to normalize stored numeric strings
     359                $updated = $bbp_db->update(
     360                        $table,
     361                        array( 'meta_value' => $new_count ),
     362                        array(
     363                                $column      => $object_id,
     364                                'meta_key'   => $meta_key,
     365                                'meta_value' => $count
     366                        ),
     367                        array( '%d' ),
     368                        array( '%d', '%s', '%d' )
     369                );
     370
     371                // Bail on a database error
     372                if ( false === $updated ) {
     373                        return false;
     374                }
     375
     376                wp_cache_delete( $object_id, $meta_type . '_meta' );
     377
     378                // Retry when another request updated the count first
     379                if ( empty( $updated ) ) {
     380                        continue;
     381                }
     382
     383                // Run the standard actions immediately after the conditional update
     384                foreach ( $meta_ids as $meta_id ) {
     385                        do_action( "updated_{$meta_type}_meta", $meta_id, $object_id, $meta_key, $new_count );
     386
     387                        if ( 'post' === $meta_type ) {
     388                                do_action( 'updated_postmeta', $meta_id, $object_id, $meta_key, $new_count );
     389                        }
     390                }
     391
     392                return true;
     393        }
     394
     395        return false;
    149396}
    150397
  • trunk/src/includes/core/actions.php

    r7453 r7467  
    288288add_action( 'bbp_deleted_topic', 'bbp_decrease_user_topic_count' );
    289289add_action( 'bbp_deleted_reply', 'bbp_decrease_user_reply_count' );
     290add_action( 'bbp_post_updated',  'bbp_update_counts_on_post_author_change', 10, 3 );
     291add_action( 'bbp_post_updated',  'bbp_recalculate_engagements_on_post_author_change', 20, 3 );
     292
     293// Update counts and engagements after WordPress reassigns a deleted user's posts
     294add_action( 'delete_user',  'bbp_update_counts_on_user_reassignment', 10, 2 );
     295add_action( 'deleted_user', 'bbp_update_counts_on_user_reassignment', 10, 2 );
    290296
    291297// Topic status transition helpers for replies
  • trunk/src/includes/forums/functions.php

    r7455 r7467  
    11111111 *
    11121112 * @since 2.1.0 bbPress (r3825)
     1113 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    11131114 *
    11141115 * @param int $forum_id Optional. Forum id.
     
    11321133
    11331134        // Update this forum id
    1134         update_post_meta( $forum_id, '_bbp_topic_count',       (int) ( $topic_count + $difference ) );
    1135         update_post_meta( $forum_id, '_bbp_total_topic_count', (int) ( $total_topic_count + $difference ) );
     1135        bbp_bump_count_meta( 'post', $forum_id, '_bbp_topic_count',       $difference, $topic_count      );
     1136        bbp_bump_count_meta( 'post', $forum_id, '_bbp_total_topic_count', $difference, $total_topic_count );
    11361137
    11371138        // Check for ancestors
     
    11501151
    11511152                                        $parent_topic_count = bbp_get_forum_topic_count( $parent_forum_id, false, true );
    1152                                         update_post_meta( $parent_forum_id, '_bbp_topic_count', (int) ( $parent_topic_count + $difference ) );
     1153                                        bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_topic_count', $difference, $parent_topic_count );
    11531154                                }
    11541155
    11551156                                // Update the total topic count.
    11561157                                $parent_total_topic_count = bbp_get_forum_topic_count( $parent_forum_id, true,  true );
    1157                                 update_post_meta( $parent_forum_id, '_bbp_total_topic_count', (int) ( $parent_total_topic_count + $difference ) );
     1158                                bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_total_topic_count', $difference, $parent_total_topic_count );
    11581159                        }
    11591160                }
    11601161        }
    11611162
    1162         $forum_topic_count = (int) ( $total_topic_count + $difference );
     1163        $forum_topic_count = bbp_number_not_negative( $total_topic_count + $difference );
    11631164
    11641165        // Filter & return
     
    12301231 *
    12311232 * @since 2.1.0 bbPress (r3825)
     1233 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    12321234 *
    12331235 * @param int $forum_id Optional. Forum id.
     
    12511253
    12521254        // Update this forum id
    1253         update_post_meta( $forum_id, '_bbp_topic_count_hidden',       (int) ( $reply_count + $difference ) );
    1254         update_post_meta( $forum_id, '_bbp_total_topic_count_hidden', (int) ( $total_topic_count + $difference ) );
     1255        bbp_bump_count_meta( 'post', $forum_id, '_bbp_topic_count_hidden',       $difference, $reply_count      );
     1256        bbp_bump_count_meta( 'post', $forum_id, '_bbp_total_topic_count_hidden', $difference, $total_topic_count );
    12551257
    12561258        // Check for ancestors
     
    12691271
    12701272                                        $parent_topic_count = bbp_get_forum_topic_count_hidden( $parent_forum_id, false, true );
    1271                                         update_post_meta( $parent_forum_id, '_bbp_topic_count_hidden', (int) ( $parent_topic_count + $difference ) );
     1273                                        bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_topic_count_hidden', $difference, $parent_topic_count );
    12721274                                }
    12731275
    12741276                                // Update the total topic count.
    12751277                                $parent_total_topic_count = bbp_get_forum_topic_count_hidden( $parent_forum_id, true, true );
    1276                                 update_post_meta( $parent_forum_id, '_bbp_total_topic_count_hidden', (int) ( $parent_total_topic_count + $difference ) );
     1278                                bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_total_topic_count_hidden', $difference, $parent_total_topic_count );
    12771279                        }
    12781280                }
    12791281        }
    12801282
    1281         $forum_topic_count = (int) ( $total_topic_count + $difference );
     1283        $forum_topic_count = bbp_number_not_negative( $total_topic_count + $difference );
    12821284
    12831285        // Filter & return
     
    13491351 *
    13501352 * @since 2.1.0 bbPress (r3825)
     1353 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    13511354 *
    13521355 * @param int $forum_id Optional. Forum id.
     
    13701373
    13711374        // Update this forum id
    1372         update_post_meta( $forum_id, '_bbp_reply_count',       (int) ( $reply_count + $difference ) );
    1373         update_post_meta( $forum_id, '_bbp_total_reply_count', (int) ( $total_reply_count + $difference ) );
     1375        bbp_bump_count_meta( 'post', $forum_id, '_bbp_reply_count',       $difference, $reply_count      );
     1376        bbp_bump_count_meta( 'post', $forum_id, '_bbp_total_reply_count', $difference, $total_reply_count );
    13741377
    13751378        // Check for ancestors
     
    13881391
    13891392                                        $parent_reply_count = bbp_get_forum_reply_count( $parent_forum_id, false, true );
    1390                                         update_post_meta( $parent_forum_id, '_bbp_reply_count', (int) ( $parent_reply_count + $difference ) );
     1393                                        bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_reply_count', $difference, $parent_reply_count );
    13911394                                }
    13921395
    13931396                                // Update the total reply count.
    13941397                                $parent_total_reply_count = bbp_get_forum_reply_count( $parent_forum_id, true,  true );
    1395                                 update_post_meta( $parent_forum_id, '_bbp_total_reply_count', (int) ( $parent_total_reply_count + $difference ) );
     1398                                bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_total_reply_count', $difference, $parent_total_reply_count );
    13961399                        }
    13971400                }
    13981401        }
    13991402
    1400         $forum_reply_count = (int) ( $total_reply_count + $difference );
     1403        $forum_reply_count = bbp_number_not_negative( $total_reply_count + $difference );
    14011404
    14021405        // Filter & return
     
    14081411 *
    14091412 * @since 2.6.0 bbPress (r6922)
     1413 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    14101414 *
    14111415 * @param int $forum_id Optional. Forum id.
     
    14291433
    14301434        // Update this forum id
    1431         update_post_meta( $forum_id, '_bbp_reply_count_hidden',       (int) ( $reply_count + $difference ) );
    1432         update_post_meta( $forum_id, '_bbp_total_reply_count_hidden', (int) ( $total_reply_count + $difference ) );
     1435        bbp_bump_count_meta( 'post', $forum_id, '_bbp_reply_count_hidden',       $difference, $reply_count      );
     1436        bbp_bump_count_meta( 'post', $forum_id, '_bbp_total_reply_count_hidden', $difference, $total_reply_count );
    14331437
    14341438        // Check for ancestors
     
    14471451
    14481452                                        $parent_reply_count = bbp_get_forum_reply_count_hidden( $parent_forum_id, false, true );
    1449                                         update_post_meta( $parent_forum_id, '_bbp_reply_count_hidden', (int) ( $parent_reply_count + $difference ) );
     1453                                        bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_reply_count_hidden', $difference, $parent_reply_count );
    14501454                                }
    14511455
    14521456                                // Update the total reply count.
    14531457                                $parent_total_reply_count = bbp_get_forum_reply_count_hidden( $parent_forum_id, true,  true );
    1454                                 update_post_meta( $parent_forum_id, '_bbp_total_reply_count_hidden', (int) ( $parent_total_reply_count + $difference ) );
     1458                                bbp_bump_count_meta( 'post', $parent_forum_id, '_bbp_total_reply_count_hidden', $difference, $parent_total_reply_count );
    14551459                        }
    14561460                }
    14571461        }
    14581462
    1459         $forum_reply_count = (int) ( $total_reply_count + $difference );
     1463        $forum_reply_count = bbp_number_not_negative( $total_reply_count + $difference );
    14601464
    14611465        // Filter & return
    14621466        return (int) apply_filters( 'bbp_bump_forum_reply_count_hidden', $forum_reply_count, $forum_id, $difference, $update_ancestors );
     1467}
     1468
     1469/**
     1470 * Bump one total count through a forum's ancestors.
     1471 *
     1472 * This is used after recounting a forum that may be nested as a subforum. The
     1473 * starting forum's total has already been updated, so only its parent forums
     1474 * receive the difference between the old and new totals. The supplied metadata
     1475 * key identifies which total topic or reply count is propagated.
     1476 *
     1477 * Forum hierarchy is stored in `post_parent`, making get_post_ancestors() the
     1478 * canonical, cache-aware way to walk from a subforum toward its root forum.
     1479 * Only parent forums are traversed: topics and replies are never ancestors in
     1480 * a valid forum hierarchy, even when the metadata key stores their totals. The
     1481 * walk stops if a malformed parent relationship leaves the forum post type.
     1482 *
     1483 * @since 2.6.16
     1484 *
     1485 * @param int    $forum_id   Starting forum ID. Its own count is not changed.
     1486 * @param string $meta_key   Topic or reply total count metadata key.
     1487 * @param int    $difference Amount to add to the stored value.
     1488 * @return bool True when all ancestor counts were updated, false otherwise.
     1489 */
     1490function bbp_bump_forum_ancestor_count( $forum_id = 0, $meta_key = '', $difference = 0 ) {
     1491        $forum_id   = bbp_get_forum_id( $forum_id );
     1492        $difference = (int) $difference;
     1493
     1494        // Bail if nothing can change
     1495        if ( empty( $forum_id ) || empty( $meta_key ) || empty( $difference ) ) {
     1496                return false;
     1497        }
     1498
     1499        $updated      = true;
     1500        $ancestor_ids = get_post_ancestors( $forum_id );
     1501
     1502        // Return if this forum has no ancestors
     1503        if ( empty( $ancestor_ids ) ) {
     1504                return $updated;
     1505        }
     1506
     1507        // Update only total counts on ancestor forums
     1508        foreach ( $ancestor_ids as $ancestor_id ) {
     1509
     1510                // Stop if malformed data leaves the subforum hierarchy
     1511                if ( ! bbp_is_forum( $ancestor_id ) ) {
     1512                        break;
     1513                }
     1514
     1515                $count = (int) get_post_meta( $ancestor_id, $meta_key, true );
     1516
     1517                if ( ! bbp_bump_count_meta( 'post', $ancestor_id, $meta_key, $difference, $count ) ) {
     1518                        $updated = false;
     1519                }
     1520        }
     1521
     1522        return $updated;
    14631523}
    14641524
     
    19782038 *
    19792039 * @since 2.0.0 bbPress (r2464)
     2040 * @since 2.6.16 Optionally update ancestor forum totals.
    19802041 *
    19812042 * @param int $forum_id Optional. Forum id or topic id. It is checked whether it
    19822043 *                       is a topic or a forum. If it's a topic, its parent,
    19832044 *                       i.e. the forum is automatically retrieved.
    1984  * @param bool $total_count Optional. To return the total count or normal count.
     2045 * @param bool $update_ancestors Optional. Whether to update ancestor totals.
    19852046 * @return int Forum topic count.
    19862047 */
    1987 function bbp_update_forum_topic_count( $forum_id = 0 ) {
    1988         $forum_id = bbp_get_forum_id( $forum_id );
     2048function bbp_update_forum_topic_count( $forum_id = 0, $update_ancestors = false ) {
     2049        $forum_id             = bbp_get_forum_id( $forum_id );
     2050        $old_total_topics     = ( true === $update_ancestors )
     2051                ? (int) get_post_meta( $forum_id, '_bbp_total_topic_count', true )
     2052                : 0;
    19892053        $children_topic_count = 0;
     2054        $total_topics         = 0;
    19902055
    19912056        // Loop through subforums and add together forum topic counts
     
    20072072        update_post_meta( $forum_id, '_bbp_total_topic_count', $total_topics );
    20082073
     2074        // Update ancestor total counts by the persisted difference
     2075        if ( true === $update_ancestors ) {
     2076                bbp_bump_forum_ancestor_count( $forum_id, '_bbp_total_topic_count', $total_topics - $old_total_topics );
     2077        }
     2078
    20092079        // Filter & return
    20102080        return (int) apply_filters( 'bbp_update_forum_topic_count', $total_topics, $forum_id );
     
    20172087 * @since 2.0.0 bbPress (r2888)
    20182088 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects
     2089 * @since 2.6.16 Optionally update ancestor forum totals.
    20192090 *
    20202091 * @param int $forum_id Optional. Topic id to update.
    20212092 * @param int $topic_count Optional. Set the topic count manually.
     2093 * @param bool $update_ancestors Optional. Whether to update ancestor totals.
    20222094 *
    20232095 * @return int Topic hidden topic count.
    20242096 */
    2025 function bbp_update_forum_topic_count_hidden( $forum_id = 0, $topic_count = false ) {
     2097function bbp_update_forum_topic_count_hidden( $forum_id = 0, $topic_count = false, $update_ancestors = false ) {
    20262098
    20272099        // If topic_id was passed as $forum_id, then get its forum
     
    20352107        }
    20362108
     2109        $children_topic_count = 0;
     2110        $total_topics         = 0;
     2111        $old_total_topics     = ( true === $update_ancestors )
     2112                ? (int) get_post_meta( $forum_id, '_bbp_total_topic_count_hidden', true )
     2113                : 0;
     2114
    20372115        // Can't update what isn't there
    20382116        if ( ! empty( $forum_id ) ) {
     2117
     2118                // Loop through children and add together hidden topic counts
     2119                $children = bbp_forum_query_subforum_ids( $forum_id );
     2120                if ( ! empty( $children ) ) {
     2121                        foreach ( (array) $children as $child ) {
     2122                                bbp_update_forum_topic_count_hidden( $child );
     2123                                $children_topic_count += bbp_get_forum_topic_count_hidden( $child, true, true );
     2124                        }
     2125                }
    20392126
    20402127                // Get topics of forum
     
    20632150                }
    20642151
    2065                 $topic_count = (int) $topic_count;
    2066 
    2067                 // Update the count
    2068                 update_post_meta( $forum_id, '_bbp_topic_count_hidden', $topic_count );
     2152                $topic_count  = (int) $topic_count;
     2153                $total_topics = (int) ( $topic_count + $children_topic_count );
     2154
     2155                // Update the counts
     2156                update_post_meta( $forum_id, '_bbp_topic_count_hidden',       $topic_count  );
     2157                update_post_meta( $forum_id, '_bbp_total_topic_count_hidden', $total_topics );
     2158
     2159                // Update ancestor total counts by the persisted difference
     2160                if ( true === $update_ancestors ) {
     2161                        bbp_bump_forum_ancestor_count( $forum_id, '_bbp_total_topic_count_hidden', $total_topics - $old_total_topics );
     2162                }
    20692163        }
    20702164
     
    20782172 * @since 2.0.0 bbPress (r2464)
    20792173 * @since 2.6.0 bbPress (r5954) Replace direct queries with WP_Query() objects.
     2174 * @since 2.6.16 Count replies only when their parent topics are public.
     2175 * @since 2.6.16 Optionally update ancestor forum totals.
    20802176 *
    20812177 * @param int  $forum_id Optional. Forum id or topic id. It is checked whether it
    20822178 *                       is a topic or a forum. If it's a topic, its parent,
    20832179 *                       i.e. the forum is automatically retrieved.
     2180 * @param bool $update_ancestors Optional. Whether to update ancestor totals.
    20842181 *
    20852182 * @return int Forum reply count.
    20862183 */
    2087 function bbp_update_forum_reply_count( $forum_id = 0 ) {
    2088 
    2089         $forum_id = bbp_get_forum_id( $forum_id );
     2184function bbp_update_forum_reply_count( $forum_id = 0, $update_ancestors = false ) {
     2185
     2186        $forum_id             = bbp_get_forum_id( $forum_id );
     2187        $old_total_replies    = ( true === $update_ancestors )
     2188                ? (int) get_post_meta( $forum_id, '_bbp_total_reply_count', true )
     2189                : 0;
    20902190        $children_reply_count = 0;
    20912191
     
    20992199
    21002200        // Don't count replies if the forum is a category
    2101         $reply_count = ! bbp_is_forum_category( $forum_id )
    2102                 ? bbp_get_public_child_count( $forum_id, bbp_get_reply_post_type() )
    2103                 : 0;
     2201        if ( bbp_is_forum_category( $forum_id ) ) {
     2202                $reply_count = 0;
     2203
     2204        // Count public replies whose parent topics are also public
     2205        } else {
     2206                $bbp_db         = bbp_db();
     2207                $reply_statuses = bbp_get_public_reply_statuses();
     2208                $topic_statuses = bbp_get_public_topic_statuses();
     2209
     2210                if ( empty( $reply_statuses ) || empty( $topic_statuses ) ) {
     2211                        $reply_count = 0;
     2212                } else {
     2213                        $reply_placeholders = implode( ', ', array_fill( 0, count( $reply_statuses ), '%s' ) );
     2214                        $topic_placeholders = implode( ', ', array_fill( 0, count( $topic_statuses ), '%s' ) );
     2215                        $sql                = "SELECT COUNT(*) FROM {$bbp_db->posts} AS replies
     2216                                INNER JOIN {$bbp_db->posts} AS topics ON replies.post_parent = topics.ID
     2217                                WHERE topics.post_parent = %d
     2218                                        AND topics.post_type = %s
     2219                                        AND topics.post_status IN ({$topic_placeholders})
     2220                                        AND replies.post_type = %s
     2221                                        AND replies.post_status IN ({$reply_placeholders})";
     2222                        $query              = $bbp_db->prepare(
     2223                                $sql,
     2224                                array_merge(
     2225                                        array( $forum_id, bbp_get_topic_post_type() ),
     2226                                        $topic_statuses,
     2227                                        array( bbp_get_reply_post_type() ),
     2228                                        $reply_statuses
     2229                                )
     2230                        );
     2231                        $reply_count = bbp_number_not_negative( $bbp_db->get_var( $query ) );
     2232                }
     2233        }
    21042234
    21052235        // Calculate total replies in this forum
     
    21102240        update_post_meta( $forum_id, '_bbp_total_reply_count', $total_replies );
    21112241
     2242        // Update ancestor total counts by the persisted difference
     2243        if ( true === $update_ancestors ) {
     2244                bbp_bump_forum_ancestor_count( $forum_id, '_bbp_total_reply_count', $total_replies - $old_total_replies );
     2245        }
     2246
    21122247        // Filter & return
    21132248        return (int) apply_filters( 'bbp_update_forum_reply_count', $total_replies, $forum_id );
     
    21182253 *
    21192254 * @since 2.6.0 bbPress (r6922)
     2255 * @since 2.6.16 Optionally update ancestor forum totals.
    21202256 *
    21212257 * @param int  $forum_id Optional. Forum id or topic id. It is checked whether it
    21222258 *                       is a topic or a forum. If it's a topic, its parent,
    21232259 *                       i.e. the forum is automatically retrieved.
     2260 * @param bool $update_ancestors Optional. Whether to update ancestor totals.
    21242261 *
    21252262 * @return int Forum reply count.
    21262263 */
    2127 function bbp_update_forum_reply_count_hidden( $forum_id = 0 ) {
    2128 
    2129         $forum_id = bbp_get_forum_id( $forum_id );
     2264function bbp_update_forum_reply_count_hidden( $forum_id = 0, $update_ancestors = false ) {
     2265
     2266        $forum_id             = bbp_get_forum_id( $forum_id );
     2267        $old_total_replies    = ( true === $update_ancestors )
     2268                ? (int) get_post_meta( $forum_id, '_bbp_total_reply_count_hidden', true )
     2269                : 0;
    21302270        $children_reply_count = 0;
    21312271
     
    21492289        update_post_meta( $forum_id, '_bbp_reply_count_hidden',       $reply_count   );
    21502290        update_post_meta( $forum_id, '_bbp_total_reply_count_hidden', $total_replies );
     2291
     2292        // Update ancestor total counts by the persisted difference
     2293        if ( true === $update_ancestors ) {
     2294                bbp_bump_forum_ancestor_count( $forum_id, '_bbp_total_reply_count_hidden', $total_replies - $old_total_replies );
     2295        }
    21512296
    21522297        // Filter & return
     
    27392884 * Returns the forum's subforum ids.
    27402885 *
    2741  * Only forums with published status are returned.
     2886 * Only forums with countable statuses are returned.
    27422887 *
    27432888 * @since 2.0.0 bbPress (r2908)
     2889 * @since 2.6.16 Exclude forums with uncountable statuses.
    27442890 *
    27452891 * @param int $forum_id Forum id.
    27462892 */
    27472893function bbp_forum_query_subforum_ids( $forum_id ) {
    2748         $subforum_ids = bbp_get_all_child_ids( $forum_id, bbp_get_forum_post_type() );
     2894        $forum_id     = bbp_get_forum_id( $forum_id );
     2895        $statuses     = bbp_get_countable_forum_statuses();
     2896        $subforum_ids = array();
     2897
     2898        // Query and cache countable subforums. The public child-ID helper excludes
     2899        // private and hidden forums, while the all-child helper includes trash, so
     2900        // neither existing helper represents the statuses counted here.
     2901        if ( ! empty( $forum_id ) && ! empty( $statuses ) ) {
     2902                $key = md5(
     2903                        serialize(
     2904                                array(
     2905                                        'parent_id'   => $forum_id,
     2906                                        'post_type'   => bbp_get_forum_post_type(),
     2907                                        'post_status' => $statuses
     2908                                )
     2909                        )
     2910                );
     2911                $cache_key    = "bbp_child_ids:{$key}:" . wp_cache_get_last_changed( 'bbpress_posts' );
     2912                $subforum_ids = wp_cache_get( $cache_key, 'bbpress_posts' );
     2913
     2914                if ( false === $subforum_ids ) {
     2915                        $bbp_db       = bbp_db();
     2916                        $placeholders = implode( ', ', array_fill( 0, count( $statuses ), '%s' ) );
     2917                        $query        = $bbp_db->prepare(
     2918                                "SELECT ID FROM {$bbp_db->posts} WHERE post_parent = %d AND post_type = %s AND post_status IN ({$placeholders}) ORDER BY ID DESC",
     2919                                array_merge( array( $forum_id, bbp_get_forum_post_type() ), $statuses )
     2920                        );
     2921                        $subforum_ids = (array) $bbp_db->get_col( $query );
     2922
     2923                        wp_cache_set( $cache_key, $subforum_ids, 'bbpress_posts' );
     2924                }
     2925        }
    27492926
    27502927        // Filter & return
  • trunk/src/includes/replies/functions.php

    r7449 r7467  
    15261526 *
    15271527 * @since 2.3.0 bbPress (r4521)
     1528 * @since 2.6.16 Recount both forums and topic engagements.
    15281529 *
    15291530 * @param int $move_reply_id Move reply id.
     
    15321533 */
    15331534function bbp_move_reply_count( $move_reply_id, $source_topic_id, $destination_topic_id ) {
    1534 
    1535         // Forum topic counts
    1536         bbp_update_forum_topic_count( bbp_get_topic_forum_id( $destination_topic_id ) );
    1537 
    1538         // Forum reply counts
    1539         bbp_update_forum_reply_count( bbp_get_topic_forum_id( $destination_topic_id ) );
     1535        $source_forum_id      = bbp_get_topic_forum_id( $source_topic_id );
     1536        $destination_forum_id = bbp_get_topic_forum_id( $destination_topic_id );
     1537
     1538        // A reply converted into a topic changes its forum's topic counts
     1539        if ( bbp_is_topic( $move_reply_id ) ) {
     1540                bbp_update_forum_topic_count( $destination_forum_id, true );
     1541                bbp_update_forum_topic_count_hidden( $destination_forum_id, false, true );
     1542
     1543                // Transfer the public contribution between count types
     1544                if ( bbp_is_topic_published( $move_reply_id ) ) {
     1545                        $user_id = bbp_get_topic_author_id( $move_reply_id );
     1546                        bbp_bump_user_reply_count( $user_id, -1 );
     1547                        bbp_bump_user_topic_count( $user_id, 1 );
     1548                }
     1549        }
     1550
     1551        // Recount replies in both forums
     1552        foreach ( bbp_get_unique_array_values( array( $source_forum_id, $destination_forum_id ) ) as $forum_id ) {
     1553                bbp_update_forum_reply_count( $forum_id, true );
     1554                bbp_update_forum_reply_count_hidden( $forum_id, true );
     1555        }
    15401556
    15411557        // Topic reply counts
     
    15471563        bbp_update_topic_reply_count_hidden( $destination_topic_id );
    15481564
    1549         // Topic voice counts
     1565        // Topic engagement and voice counts
     1566        bbp_recalculate_topic_engagements( $source_topic_id      );
     1567        bbp_recalculate_topic_engagements( $destination_topic_id );
    15501568        bbp_update_topic_voice_count( $source_topic_id      );
    15511569        bbp_update_topic_voice_count( $destination_topic_id );
  • trunk/src/includes/topics/functions.php

    r7455 r7467  
    13681368 *
    13691369 * @since 2.0.0 bbPress (r2756)
     1370 * @since 2.6.16 Recount both forums and topic engagements.
    13701371 *
    13711372 * @param int $destination_topic_id Destination topic id.
     
    13741375 */
    13751376function bbp_merge_topic_count( $destination_topic_id, $source_topic_id, $source_topic_forum_id ) {
     1377        $destination_forum_id = bbp_get_topic_forum_id( $destination_topic_id );
    13761378
    13771379        /** Source Topic **********************************************************/
    13781380
    13791381        // Forum Topic Counts
    1380         bbp_update_forum_topic_count( $source_topic_forum_id );
     1382        bbp_update_forum_topic_count( $source_topic_forum_id, true );
     1383        bbp_update_forum_topic_count_hidden( $source_topic_forum_id, false, true );
    13811384
    13821385        // Forum Reply Counts
    1383         bbp_update_forum_reply_count( $source_topic_forum_id );
     1386        foreach ( bbp_get_unique_array_values( array( $source_topic_forum_id, $destination_forum_id ) ) as $forum_id ) {
     1387                bbp_update_forum_reply_count( $forum_id, true );
     1388                bbp_update_forum_reply_count_hidden( $forum_id, true );
     1389        }
    13841390
    13851391        /** Destination Topic *****************************************************/
     
    13911397        bbp_update_topic_reply_count_hidden( $destination_topic_id );
    13921398
    1393         // Topic Voice Counts
     1399        // Topic Engagement and Voice Counts
     1400        bbp_recalculate_topic_engagements( $destination_topic_id );
    13941401        bbp_update_topic_voice_count( $destination_topic_id );
     1402
     1403        // Transfer the converted source topic author's contribution to replies
     1404        if ( bbp_is_reply_published( $source_topic_id ) ) {
     1405                $user_id = bbp_get_reply_author_id( $source_topic_id );
     1406                bbp_bump_user_topic_count( $user_id, -1 );
     1407                bbp_bump_user_reply_count( $user_id, 1 );
     1408        }
    13951409
    13961410        do_action( 'bbp_merge_topic_count', $destination_topic_id, $source_topic_id, $source_topic_forum_id );
     
    17181732 *
    17191733 * @since 2.0.0 bbPress (r2756)
     1734 * @since 2.6.16 Recount both forums and topic engagements.
    17201735 *
    17211736 * @param int $from_reply_id From reply id.
     
    17241739 */
    17251740function bbp_split_topic_count( $from_reply_id, $source_topic_id, $destination_topic_id ) {
    1726 
    1727         // Forum Topic Counts
    1728         bbp_update_forum_topic_count( bbp_get_topic_forum_id( $destination_topic_id ) );
    1729 
    1730         // Forum Reply Counts
    1731         bbp_update_forum_reply_count( bbp_get_topic_forum_id( $destination_topic_id ) );
     1741        $source_forum_id      = bbp_get_topic_forum_id( $source_topic_id );
     1742        $destination_forum_id = bbp_get_topic_forum_id( $destination_topic_id );
     1743
     1744        // A reply converted into a topic changes its forum's topic counts
     1745        if ( bbp_is_topic( $from_reply_id ) ) {
     1746                bbp_update_forum_topic_count( $destination_forum_id, true );
     1747                bbp_update_forum_topic_count_hidden( $destination_forum_id, false, true );
     1748
     1749                // Transfer the public contribution between count types
     1750                if ( bbp_is_topic_published( $from_reply_id ) ) {
     1751                        $user_id = bbp_get_topic_author_id( $from_reply_id );
     1752                        bbp_bump_user_reply_count( $user_id, -1 );
     1753                        bbp_bump_user_topic_count( $user_id, 1 );
     1754                }
     1755        }
     1756
     1757        // Recount replies in both forums
     1758        foreach ( bbp_get_unique_array_values( array( $source_forum_id, $destination_forum_id ) ) as $forum_id ) {
     1759                bbp_update_forum_reply_count( $forum_id, true );
     1760                bbp_update_forum_reply_count_hidden( $forum_id, true );
     1761        }
    17321762
    17331763        // Topic Reply Counts
     
    17391769        bbp_update_topic_reply_count_hidden( $destination_topic_id );
    17401770
    1741         // Topic Voice Counts
     1771        // Topic Engagement and Voice Counts
     1772        bbp_recalculate_topic_engagements( $source_topic_id      );
     1773        bbp_recalculate_topic_engagements( $destination_topic_id );
    17421774        bbp_update_topic_voice_count( $source_topic_id      );
    17431775        bbp_update_topic_voice_count( $destination_topic_id );
     
    24052437 *
    24062438 * @since 2.1.0 bbPress (r3825)
     2439 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    24072440 *
    24082441 * @param int $topic_id   Optional. Topic id.
     
    24222455        $reply_count = bbp_get_topic_reply_count( $topic_id, true );
    24232456        $difference  = (int) $difference;
    2424         $new_count   = (int) ( $reply_count + $difference );
     2457        $new_count   = bbp_number_not_negative( $reply_count + $difference );
    24252458
    24262459        // Update this topic id's reply count
    2427         update_post_meta( $topic_id, '_bbp_reply_count', $new_count );
     2460        bbp_bump_count_meta( 'post', $topic_id, '_bbp_reply_count', $difference, $reply_count );
    24282461
    24292462        // Filter & return
     
    24952528 *
    24962529 * @since 2.1.0 bbPress (r3825)
     2530 * @since 2.6.16 Use atomic metadata writes and non-negative counts.
    24972531 *
    24982532 * @param int $topic_id   Optional. Topic id.
     
    25122546        $reply_count = bbp_get_topic_reply_count_hidden( $topic_id, true );
    25132547        $difference  = (int) $difference;
    2514         $new_count   = (int) ( $reply_count + $difference );
     2548        $new_count   = bbp_number_not_negative( $reply_count + $difference );
    25152549
    25162550        // Update this topic id's hidden reply count
    2517         update_post_meta( $topic_id, '_bbp_reply_count_hidden', $new_count );
     2551        bbp_bump_count_meta( 'post', $topic_id, '_bbp_reply_count_hidden', $difference, $reply_count );
    25182552
    25192553        // Filter & return
  • trunk/src/includes/users/engagements.php

    r7380 r7467  
    195195 *
    196196 * @since 2.6.0 bbPress (r6522)
     197 * @since 2.6.16 Honor filtered public reply statuses.
    197198 *
    198199 * @param int $topic_id Optional. Topic id.
     
    203204
    204205        // Default variables
    205         $topic_id = bbp_get_topic_id( $topic_id );
    206         $bbp_db   = bbp_db();
    207         $statii   = "'" . implode( "', '", bbp_get_public_topic_statuses() ) . "'";
    208 
    209         // A cool UNION query!
    210         $sql = "
    211 SELECT DISTINCT( post_author ) FROM (
    212         SELECT post_author FROM {$bbp_db->posts}
    213                 WHERE ( ID = %d AND post_status IN ({$statii}) AND post_type = %s )
    214 UNION
    215         SELECT post_author FROM {$bbp_db->posts}
    216                 WHERE ( post_parent = %d AND post_status = %s AND post_type = %s )
    217 ) as u1";
    218 
    219         // Prepare & get results
    220         $query   = $bbp_db->prepare( $sql, $topic_id, bbp_get_topic_post_type(), $topic_id, bbp_get_public_status_id(), bbp_get_reply_post_type() );
    221         $results = $bbp_db->get_col( $query );
     206        $topic_id       = bbp_get_topic_id( $topic_id );
     207        $bbp_db         = bbp_db();
     208        $topic_type     = bbp_get_topic_post_type();
     209        $reply_type     = bbp_get_reply_post_type();
     210        $topic_statuses = array_unique( array_filter( bbp_get_public_topic_statuses() ) );
     211        $reply_statuses = array_unique( array_filter( bbp_get_public_reply_statuses() ) );
     212        $sql            = array();
     213        $values         = array();
     214
     215        // Add the topic author for countable topic statuses
     216        if ( ! empty( $topic_statuses ) ) {
     217                $placeholders = implode( ', ', array_fill( 0, count( $topic_statuses ), '%s' ) );
     218                $sql[]        = "SELECT post_author FROM {$bbp_db->posts} WHERE ID = %d AND post_status IN ( {$placeholders} ) AND post_type = %s";
     219                $values       = array_merge( $values, array( $topic_id ), $topic_statuses, array( $topic_type ) );
     220        }
     221
     222        // Add reply authors for countable reply statuses
     223        if ( ! empty( $reply_statuses ) ) {
     224                $placeholders = implode( ', ', array_fill( 0, count( $reply_statuses ), '%s' ) );
     225                $sql[]        = "SELECT post_author FROM {$bbp_db->posts} WHERE post_parent = %d AND post_status IN ( {$placeholders} ) AND post_type = %s";
     226                $values       = array_merge( $values, array( $topic_id ), $reply_statuses, array( $reply_type ) );
     227        }
     228
     229        // Query unique topic and reply authors
     230        $results = ! empty( $sql )
     231                ? $bbp_db->get_col( $bbp_db->prepare( implode( ' UNION ', $sql ), $values ) )
     232                : array();
    222233
    223234        // Parse results into voices
    224         $engagements = ! is_wp_error( $results )
    225                 ? wp_parse_id_list( array_filter( $results ) )
    226                 : array();
     235        $engagements = wp_parse_id_list( array_filter( $results ) );
    227236
    228237        // Filter & return
  • trunk/src/includes/users/functions.php

    r7452 r7467  
    720720                : bbp_get_user_topic_count( $user_id, true );
    721721
    722         $user_topic_count = bbp_number_not_negative( $count + $difference );
     722        $user_topic_count = (int) bbp_number_not_negative( $count + $difference );
    723723
    724724        // Add them up and filter them
    725725        $new_count = (int) apply_filters( 'bbp_bump_user_topic_count', $user_topic_count, $user_id, $difference, $count );
    726726
    727         return bbp_update_user_topic_count( $user_id, $new_count );
     727        // Preserve absolute count filters before using the atomic difference
     728        $difference = ( $new_count === $user_topic_count )
     729                ? $new_count - $count
     730                : false;
     731
     732        return bbp_update_user_topic_count( $user_id, $new_count, $difference );
    728733}
    729734
     
    756761                : bbp_get_user_reply_count( $user_id, true );
    757762
    758         $user_reply_count = bbp_number_not_negative( $count + $difference );
     763        $user_reply_count = (int) bbp_number_not_negative( $count + $difference );
    759764
    760765        // Add them up and filter them
    761766        $new_count = (int) apply_filters( 'bbp_bump_user_reply_count', $user_reply_count, $user_id, $difference, $count );
    762767
    763         return bbp_update_user_reply_count( $user_id, $new_count );
     768        // Preserve absolute count filters before using the atomic difference
     769        $difference = ( $new_count === $user_reply_count )
     770                ? $new_count - $count
     771                : false;
     772
     773        return bbp_update_user_reply_count( $user_id, $new_count, $difference );
     774}
     775
     776/**
     777 * Update user counts when a topic or reply changes authors.
     778 *
     779 * @since 2.6.16
     780 *
     781 * @param int     $post_id     Post ID.
     782 * @param WP_Post $post_after  Post object following the update.
     783 * @param WP_Post $post_before Post object before the update.
     784 */
     785function bbp_update_counts_on_post_author_change( $post_id = 0, $post_after = false, $post_before = false ) {
     786
     787        // Bail if the author or post type did not change as expected
     788        if ( ( $post_after->post_author === $post_before->post_author ) || ( $post_after->post_type !== $post_before->post_type ) ) {
     789                return;
     790        }
     791
     792        // Set topic public membership
     793        if ( bbp_get_topic_post_type() === $post_after->post_type ) {
     794                $public_statuses = bbp_get_public_topic_statuses();
     795                $was_public      = in_array( $post_before->post_status, $public_statuses, true );
     796                $is_public       = in_array( $post_after->post_status,  $public_statuses, true );
     797                $is_topic        = true;
     798
     799        // Set reply public membership
     800        } elseif ( bbp_get_reply_post_type() === $post_after->post_type ) {
     801                $public_statuses = bbp_get_public_reply_statuses();
     802                $was_public      = in_array( $post_before->post_status, $public_statuses, true );
     803                $is_public       = in_array( $post_after->post_status,  $public_statuses, true );
     804                $is_topic        = false;
     805
     806        // Bail if this is not a topic or reply
     807        } else {
     808                return;
     809        }
     810
     811        // The transition callback already handles posts that were not public
     812        if ( ! $was_public ) {
     813                return;
     814        }
     815
     816        // Transfer a public contribution between authors
     817        if ( $is_public ) {
     818                if ( $is_topic ) {
     819                        bbp_bump_user_topic_count( $post_before->post_author, -1 );
     820                        bbp_bump_user_topic_count( $post_after->post_author,   1 );
     821                } else {
     822                        bbp_bump_user_reply_count( $post_before->post_author, -1 );
     823                        bbp_bump_user_reply_count( $post_after->post_author,   1 );
     824                }
     825
     826        // Repair both authors after the transition callback targeted the new author
     827        } else {
     828                foreach ( bbp_get_unique_array_values( array( $post_before->post_author, $post_after->post_author ) ) as $user_id ) {
     829                        if ( $is_topic ) {
     830                                bbp_update_user_topic_count( $user_id, bbp_get_user_topic_count_raw( $user_id ) );
     831                        } else {
     832                                bbp_update_user_reply_count( $user_id, bbp_get_user_reply_count_raw( $user_id ) );
     833                        }
     834                }
     835        }
     836}
     837
     838/**
     839 * Update topic engagements when a topic or reply changes authors.
     840 *
     841 * @since 2.6.16
     842 *
     843 * @param int     $post_id     Post ID.
     844 * @param WP_Post $post_after  Post object following the update.
     845 * @param WP_Post $post_before Post object before the update.
     846 */
     847function bbp_recalculate_engagements_on_post_author_change( $post_id = 0, $post_after = false, $post_before = false ) {
     848
     849        // Bail if the author did not change
     850        if ( $post_after->post_author === $post_before->post_author ) {
     851                return;
     852        }
     853
     854        // Get the topic ID from a topic or reply
     855        if ( bbp_get_topic_post_type() === $post_after->post_type ) {
     856                $topic_id = $post_id;
     857        } elseif ( bbp_get_reply_post_type() === $post_after->post_type ) {
     858                $topic_id = bbp_get_reply_topic_id( $post_id );
     859        } else {
     860                return;
     861        }
     862
     863        // Recalculate engagements and their count
     864        bbp_recalculate_topic_engagements( $topic_id );
     865        bbp_update_topic_voice_count( $topic_id );
     866}
     867
     868/**
     869 * Update counts and engagements when a deleted user's posts are reassigned.
     870 *
     871 * WordPress reassigns post authors directly in the database, bypassing the
     872 * normal post update actions. Record affected topics before that write, then
     873 * repair the replacement user's counts and those topics after it completes.
     874 *
     875 * @since 2.6.16
     876 *
     877 * @param int      $user_id  ID of the user being deleted.
     878 * @param int|null $reassign ID of the user receiving the posts.
     879 */
     880function bbp_update_counts_on_user_reassignment( $user_id = 0, $reassign = null ) {
     881        static $topic_ids = array();
     882
     883        $user_id = (int) $user_id;
     884        $reassign = (int) $reassign;
     885
     886        // Bail if posts are not being reassigned to another user
     887        if ( empty( $user_id ) || empty( $reassign ) || ( $user_id === $reassign ) ) {
     888                return;
     889        }
     890
     891        $key = get_current_blog_id() . ':' . $user_id . ':' . $reassign;
     892
     893        // Record affected topics before WordPress changes their authors directly
     894        if ( 'delete_user' === current_filter() ) {
     895                $bbp_db     = bbp_db();
     896                $topic_type = bbp_get_topic_post_type();
     897                $reply_type = bbp_get_reply_post_type();
     898                $query      = $bbp_db->prepare(
     899                        "SELECT DISTINCT CASE WHEN post_type = %s THEN ID ELSE post_parent END FROM {$bbp_db->posts} WHERE post_author = %d AND post_type IN ( %s, %s )",
     900                        $topic_type,
     901                        $user_id,
     902                        $topic_type,
     903                        $reply_type
     904                );
     905
     906                $topic_ids[ $key ] = wp_parse_id_list( array_filter( $bbp_db->get_col( $query ) ) );
     907                return;
     908        }
     909
     910        // Bail unless WordPress completed the reassignment recorded above
     911        if ( ( 'deleted_user' !== current_filter() ) || ! isset( $topic_ids[ $key ] ) ) {
     912                return;
     913        }
     914
     915        $affected_topic_ids = $topic_ids[ $key ];
     916        unset( $topic_ids[ $key ] );
     917
     918        // Bail if the deleted user did not author any topics or replies
     919        if ( empty( $affected_topic_ids ) ) {
     920                return;
     921        }
     922
     923        // Recount contributions for the replacement user
     924        bbp_update_user_topic_count( $reassign, bbp_get_user_topic_count_raw( $reassign ) );
     925        bbp_update_user_reply_count( $reassign, bbp_get_user_reply_count_raw( $reassign ) );
     926
     927        // Rebuild engagements and voices for each affected topic
     928        foreach ( $affected_topic_ids as $topic_id ) {
     929                bbp_recalculate_topic_engagements( $topic_id, true );
     930                bbp_update_topic_voice_count( $topic_id );
     931        }
    764932}
    765933
  • trunk/src/includes/users/options.php

    r7380 r7467  
    124124 *
    125125 * @since 2.6.0 bbPress (r5309)
    126  *
    127  * @param int $user_id
    128  * @param mixed $count
     126 * @since 2.6.16 Support atomic count differences.
     127 *
     128 * @param int       $user_id    User ID.
     129 * @param mixed     $count      New topic count.
     130 * @param int|false $difference Optional. Difference from the previous count.
    129131 * @return boolean
    130132 */
    131 function bbp_update_user_topic_count( $user_id = 0, $count = false ) {
     133function bbp_update_user_topic_count( $user_id = 0, $count = false, $difference = false ) {
    132134
    133135        // Validate user id
     
    138140
    139141        // Just in time filtering of the user's topic count
     142        $unfiltered_count = $count;
    140143        $count = apply_filters( 'bbp_update_user_topic_count', $count, $user_id );
    141144
     
    145148        }
    146149
     150        // Atomically bump an unfiltered user option
     151        if ( ( false !== $difference ) && ( $count === $unfiltered_count ) ) {
     152                $default  = (int) $unfiltered_count - (int) $difference;
     153                $meta_key = bbp_db()->get_blog_prefix() . '_bbp_topic_count';
     154                $result   = bbp_bump_count_meta( 'user', $user_id, $meta_key, $difference, $default );
     155
     156                return $result;
     157        }
     158
    147159        // Return the updated user option
    148160        return update_user_option( $user_id, '_bbp_topic_count', $count );
     
    153165 *
    154166 * @since 2.6.0 bbPress (r5309)
    155  *
    156  * @param int $user_id User id.
    157  * @param mixed $count
     167 * @since 2.6.16 Support atomic count differences.
     168 *
     169 * @param int       $user_id    User ID.
     170 * @param mixed     $count      New reply count.
     171 * @param int|false $difference Optional. Difference from the previous count.
    158172 * @return boolean
    159173 */
    160 function bbp_update_user_reply_count( $user_id = 0, $count = false ) {
     174function bbp_update_user_reply_count( $user_id = 0, $count = false, $difference = false ) {
    161175
    162176        // Validate user id
     
    167181
    168182        // Just in time filtering of the user's reply count
     183        $unfiltered_count = $count;
    169184        $count = apply_filters( 'bbp_update_user_reply_count', $count, $user_id );
    170185
     
    172187        if ( false === $count ) {
    173188                return false;
     189        }
     190
     191        // Atomically bump an unfiltered user option
     192        if ( ( false !== $difference ) && ( $count === $unfiltered_count ) ) {
     193                $default  = (int) $unfiltered_count - (int) $difference;
     194                $meta_key = bbp_db()->get_blog_prefix() . '_bbp_reply_count';
     195                $result   = bbp_bump_count_meta( 'user', $user_id, $meta_key, $difference, $default );
     196
     197                return $result;
    174198        }
    175199
  • trunk/src/readme.txt

    r7453 r7467  
    5656Permanent deletion does not produce a post-status transition. Its count maintenance continues through the existing `bbp_deleted_topic` and `bbp_deleted_reply` actions.
    5757
     58Count bump functions now use conditional metadata writes and bounded retries so simultaneous requests do not overwrite each other's existing count changes. Existing bbPress count filters and the standard WordPress metadata filters and actions continue to run. WordPress metadata tables do not enforce unique object-and-key pairs, so simultaneous first-time inserts retain the same limitation as the core metadata API. The `bbp_pre_bump_count_meta` filter can short-circuit an update, the `bbp_bump_count_meta_max_attempts` filter controls the default limit of five write attempts, and the `bbp_bump_count_meta_types` filter controls the post, user, term, and comment metadata types supported by default. `bbp_update_user_topic_count()` and `bbp_update_user_reply_count()` accept an optional third `$difference` argument for this internal bump lifecycle; existing calls remain compatible.
     59
     60Post-author changes and user deletion with post reassignment now reconcile affected user contribution, topic engagement, and voice counts. Moderator move, merge, and split operations also reconcile source, destination, and ancestor forum counts. Forum count updater functions accept an optional final argument for propagating a recount's difference through ancestor totals; existing calls remain compatible. Forum reply recounts include public replies only when their parent topics are also public. A public reply beneath a non-public topic is excluded from the public forum total without being included in the pending, spammed, and trashed reply count. Topic engagement recounts honor filtered public topic and reply statuses, and preserve other term-backed relationships.
     61
    5862= Subforum counts in 2.6.16 =
    5963
    60 bbPress now maintains subforum counts when forums are trashed, restored, permanently deleted, or moved between parents. The new `bbp_post_updated` action receives the post ID, the updated `WP_Post` object, and the previous `WP_Post` object after any bbPress post type is updated.
     64bbPress now maintains subforum counts when forums are trashed, restored, permanently deleted, or moved between parents. Recursive forum counts include public, private, and hidden subforums while excluding subforums with uncountable statuses. The new `bbp_post_updated` action receives the post ID, the updated `WP_Post` object, and the previous `WP_Post` object after any bbPress post type is updated.
    6165
    6266== Screenshots ==
  • trunk/tests/phpunit/testcases/admin/tools.php

    r7453 r7467  
    371371                $this->assertSame( 0, $count );
    372372
     373                // Category total topic count hidden.
     374                $count = bbp_get_forum_topic_count_hidden( $c, true, true );
     375                $this->assertSame( 2, $count );
     376
    373377                // Forum topic count.
    374378                $count = bbp_get_forum_topic_count( $f, false, true );
     
    383387                $this->assertSame( 2, $count );
    384388
    385                 // Delete the _bbp_total_topic_count meta key.
     389                // Delete the _bbp_topic_count_hidden meta key.
    386390                $this->assertTrue( delete_post_meta_by_key( '_bbp_topic_count_hidden' ) );
     391
     392                // Delete the _bbp_total_topic_count_hidden meta key.
     393                $this->assertTrue( delete_post_meta_by_key( '_bbp_total_topic_count_hidden' ) );
    387394
    388395                // Delete the _bbp_total_topic_count meta key.
     
    404411                $this->assertSame( 0, $count );
    405412
     413                // Category total topic count hidden.
     414                $count = bbp_get_forum_topic_count_hidden( $c, true, true );
     415                $this->assertSame( 0, $count );
     416
    406417                // Forum topic count.
    407418                $count = bbp_get_forum_topic_count( $f, false, true );
     
    431442                $this->assertSame( 0, $count );
    432443
     444                // Category total topic count hidden.
     445                $count = bbp_get_forum_topic_count_hidden( $c, true, true );
     446                $this->assertSame( 2, $count );
     447
    433448                // Forum topic count.
    434449                $count = bbp_get_forum_topic_count( $f, false, true );
     
    442457                $count = bbp_get_forum_topic_count_hidden( $f, true, true );
    443458                $this->assertSame( 2, $count );
     459        }
     460
     461        /**
     462         * @covers ::bbp_admin_repair_forum_hidden_reply_count
     463         */
     464        public function test_bbp_admin_repair_forum_hidden_reply_count() {
     465                $c = $this->factory->forum->create( array(
     466                        'forum_meta' => array( 'forum_type' => 'category' ),
     467                ) );
     468                $f = $this->factory->forum->create( array(
     469                        'post_parent' => $c,
     470                        'forum_meta'  => array( 'forum_id' => $c ),
     471                ) );
     472                $t = $this->factory->topic->create( array(
     473                        'post_parent' => $f,
     474                        'topic_meta'  => array( 'forum_id' => $f ),
     475                ) );
     476
     477                $this->factory->reply->create( array(
     478                        'post_parent' => $t,
     479                        'post_status' => bbp_get_pending_status_id(),
     480                        'reply_meta'  => array(
     481                                'forum_id' => $f,
     482                                'topic_id' => $t,
     483                        ),
     484                ) );
     485
     486                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $c, false, true ) );
     487                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $c, true, true ) );
     488                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $f, false, true ) );
     489                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $f, true, true ) );
     490
     491                $this->assertTrue( delete_post_meta_by_key( '_bbp_reply_count_hidden' ) );
     492                $this->assertTrue( delete_post_meta_by_key( '_bbp_total_reply_count_hidden' ) );
     493
     494                bbp_admin_repair_forum_hidden_reply_count();
     495
     496                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $c, false, true ) );
     497                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $c, true, true ) );
     498                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $f, false, true ) );
     499                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $f, true, true ) );
    444500        }
    445501
  • trunk/tests/phpunit/testcases/core/abstraction.php

    r5898 r7467  
    3232
    3333        /**
     34         * @covers ::bbp_bump_count_meta
     35         */
     36        public function test_bbp_bump_count_meta_handles_missing_and_non_negative_values() {
     37                $post_id = self::factory()->post->create();
     38
     39                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', 2, 3 ) );
     40                $this->assertSame( 5, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     41                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -10 ) );
     42                $this->assertSame( 0, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     43                $this->assertFalse( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 ) );
     44        }
     45
     46        /**
     47         * @covers ::bbp_bump_count_meta
     48         */
     49        public function test_bbp_bump_count_meta_validates_required_values_before_filters() {
     50                $filtered = 0;
     51                $callback = function( $check ) use ( &$filtered ) {
     52                        $filtered++;
     53                        return $check;
     54                };
     55
     56                add_filter( 'bbp_pre_bump_count_meta', $callback );
     57                $this->assertFalse( bbp_bump_count_meta( 'post', 0, '_bbp_test_count' ) );
     58                $this->assertFalse( bbp_bump_count_meta( 'post', 1, '' ) );
     59                $this->assertFalse( bbp_bump_count_meta( 'post', 1, '_bbp_test_count', 0 ) );
     60                remove_filter( 'bbp_pre_bump_count_meta', $callback, 10 );
     61
     62                $this->assertSame( 0, $filtered );
     63        }
     64
     65        /**
     66         * @covers ::bbp_bump_count_meta
     67         */
     68        public function test_bbp_bump_count_meta_can_be_short_circuited_before_type_validation() {
     69                $meta_types_filtered = 0;
     70                $received            = array();
     71                $pre_callback        = function( $check, $meta_type, $object_id, $meta_key, $difference, $default ) use ( &$received ) {
     72                        $received = func_get_args();
     73                        return false;
     74                };
     75                $types_callback = function( $meta_types ) use ( &$meta_types_filtered ) {
     76                        $meta_types_filtered++;
     77                        return $meta_types;
     78                };
     79
     80                add_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10, 6 );
     81                add_filter( 'bbp_bump_count_meta_types', $types_callback );
     82                $result = bbp_bump_count_meta( 'invalid', 1, '_bbp_test_count' );
     83                remove_filter( 'bbp_bump_count_meta_types', $types_callback, 10 );
     84                remove_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10 );
     85
     86                $this->assertFalse( $result );
     87                $this->assertSame( array( null, 'invalid', 1, '_bbp_test_count', 1, 0 ), $received );
     88                $this->assertSame( 0, $meta_types_filtered );
     89
     90                $pre_callback = function() {
     91                        return true;
     92                };
     93
     94                add_filter( 'bbp_pre_bump_count_meta', $pre_callback );
     95                $result = bbp_bump_count_meta( 'invalid', 1, '_bbp_test_count' );
     96                remove_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10 );
     97
     98                $this->assertTrue( $result );
     99        }
     100
     101        /**
     102         * @covers ::bbp_bump_count_meta
     103         */
     104        public function test_bbp_bump_count_meta_supports_user_term_and_comment_metadata() {
     105                $post_id    = self::factory()->post->create();
     106                $user_id    = self::factory()->user->create();
     107                $term_id    = self::factory()->term->create();
     108                $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => $post_id ) );
     109
     110                $this->assertTrue( bbp_bump_count_meta( 'user', $user_id, '_bbp_test_count', 1 ) );
     111                $this->assertSame( 1, (int) get_user_meta( $user_id, '_bbp_test_count', true ) );
     112                $this->assertTrue( bbp_bump_count_meta( 'term', $term_id, '_bbp_test_count', 2 ) );
     113                $this->assertSame( 2, (int) get_term_meta( $term_id, '_bbp_test_count', true ) );
     114                $this->assertTrue( bbp_bump_count_meta( 'comment', $comment_id, '_bbp_test_count', 3 ) );
     115                $this->assertSame( 3, (int) get_comment_meta( $comment_id, '_bbp_test_count', true ) );
     116        }
     117
     118        /**
     119         * @covers ::bbp_bump_count_meta
     120         */
     121        public function test_bbp_bump_count_meta_types_are_filterable() {
     122                $term_id  = self::factory()->term->create();
     123                $callback = function() {
     124                        return array( 'post' );
     125                };
     126
     127                add_filter( 'bbp_bump_count_meta_types', $callback );
     128                $result = bbp_bump_count_meta( 'term', $term_id, '_bbp_test_count' );
     129                remove_filter( 'bbp_bump_count_meta_types', $callback, 10 );
     130
     131                $this->assertFalse( $result );
     132                $this->assertSame( '', get_term_meta( $term_id, '_bbp_test_count', true ) );
     133        }
     134
     135        /**
     136         * @covers ::bbp_bump_count_meta
     137         */
     138        public function test_bbp_bump_count_meta_does_not_retry_a_filtered_update() {
     139                $post_id = self::factory()->post->create();
     140                $updates = 0;
     141                $callback = function() use ( &$updates ) {
     142                        $updates++;
     143                        return false;
     144                };
     145
     146                update_post_meta( $post_id, '_bbp_test_count', 5 );
     147                add_filter( 'update_post_metadata', $callback );
     148                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     149                remove_filter( 'update_post_metadata', $callback, 10 );
     150
     151                $this->assertFalse( $result );
     152                $this->assertSame( 1, $updates );
     153                $this->assertSame( 5, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     154        }
     155
     156        /**
     157         * @covers ::bbp_bump_count_meta
     158         */
     159        public function test_bbp_bump_count_meta_filters_an_update_before_adding_metadata() {
     160                $post_id  = self::factory()->post->create();
     161                $previous = null;
     162                $callback = function( $check, $filtered_post_id, $meta_key, $meta_value, $prev_value ) use ( &$previous ) {
     163                        $previous = $prev_value;
     164                        return false;
     165                };
     166
     167                add_filter( 'update_post_metadata', $callback, 10, 5 );
     168                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     169                remove_filter( 'update_post_metadata', $callback, 10 );
     170
     171                $this->assertFalse( $result );
     172                $this->assertSame( '', $previous );
     173                $this->assertSame( '', get_post_meta( $post_id, '_bbp_test_count', true ) );
     174        }
     175
     176        /**
     177         * @covers ::bbp_bump_count_meta
     178         * @ticket BBP3678
     179         */
     180        public function test_bbp_bump_count_meta_uses_the_database_after_a_filtered_read() {
     181                $post_id = self::factory()->post->create();
     182                $callback = function( $value, $filtered_post_id, $meta_key ) use ( $post_id ) {
     183                        return ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) )
     184                                ? 100
     185                                : $value;
     186                };
     187
     188                update_post_meta( $post_id, '_bbp_test_count', 5 );
     189                add_filter( 'get_post_metadata', $callback, 10, 3 );
     190                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     191                remove_filter( 'get_post_metadata', $callback, 10 );
     192
     193                $this->assertTrue( $result );
     194                $this->assertSame( 6, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     195        }
     196
     197        /**
     198         * @covers ::bbp_bump_count_meta
     199         */
     200        public function test_bbp_bump_count_meta_returns_false_when_adding_metadata_fails() {
     201                $post_id = self::factory()->post->create();
     202                $callback = function() {
     203                        return false;
     204                };
     205
     206                add_filter( 'add_post_metadata', $callback );
     207                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     208                remove_filter( 'add_post_metadata', $callback, 10 );
     209
     210                $this->assertFalse( $result );
     211                $this->assertSame( '', get_post_meta( $post_id, '_bbp_test_count', true ) );
     212        }
     213
     214        /**
     215         * @covers ::bbp_bump_count_meta
     216         * @ticket BBP3678
     217         */
     218        public function test_bbp_bump_count_meta_preserves_an_interleaved_update_from_zero() {
     219                $post_id     = self::factory()->post->create();
     220                $interleaved = false;
     221                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     222                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     223                                $interleaved = true;
     224                                bbp_bump_count_meta( 'post', $post_id, $meta_key );
     225                        }
     226
     227                        return $check;
     228                };
     229
     230                update_post_meta( $post_id, '_bbp_test_count', 0 );
     231                add_filter( 'update_post_metadata', $callback, 10, 3 );
     232                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     233                remove_filter( 'update_post_metadata', $callback, 10 );
     234
     235                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     236        }
     237
     238        /**
     239         * @covers ::bbp_bump_count_meta
     240         * @ticket BBP3678
     241         */
     242        public function test_bbp_bump_count_meta_preserves_interleaved_decrements() {
     243                $post_id      = self::factory()->post->create();
     244                $interleaved  = false;
     245                $filter_calls = 0;
     246                $callback     = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved, &$filter_calls ) {
     247                        $filter_calls++;
     248
     249                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     250                                $interleaved = true;
     251                                bbp_bump_count_meta( 'post', $post_id, $meta_key, -1 );
     252                        }
     253
     254                        return $check;
     255                };
     256
     257                update_post_meta( $post_id, '_bbp_test_count', 3 );
     258                add_filter( 'update_post_metadata', $callback, 10, 3 );
     259                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 );
     260                remove_filter( 'update_post_metadata', $callback, 10 );
     261
     262                $this->assertSame( 1, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     263                $this->assertSame( 2, $filter_calls );
     264        }
     265
     266        /**
     267         * @covers ::bbp_bump_count_meta
     268         * @ticket BBP3678
     269         */
     270        public function test_bbp_bump_count_meta_refreshes_a_stale_zero_before_decrementing() {
     271                global $wpdb;
     272
     273                $post_id = self::factory()->post->create();
     274
     275                update_post_meta( $post_id, '_bbp_test_count', 0 );
     276                get_post_meta( $post_id, '_bbp_test_count', true );
     277
     278                $wpdb->update(
     279                        $wpdb->postmeta,
     280                        array( 'meta_value' => 1 ),
     281                        array(
     282                                'post_id'  => $post_id,
     283                                'meta_key' => '_bbp_test_count'
     284                        ),
     285                        array( '%d' ),
     286                        array( '%d', '%s' )
     287                );
     288
     289                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 ) );
     290                $this->assertSame( 0, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     291        }
     292
     293        /**
     294         * @covers ::bbp_bump_count_meta
     295         * @ticket BBP3678
     296         */
     297        public function test_bbp_bump_count_meta_max_attempts_is_filterable() {
     298                $post_id     = self::factory()->post->create();
     299                $interleaved = false;
     300                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     301                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     302                                $interleaved = true;
     303                                bbp_bump_count_meta( 'post', $post_id, $meta_key, -1 );
     304                        }
     305
     306                        return $check;
     307                };
     308                $max_attempts = function() {
     309                        return 1;
     310                };
     311
     312                update_post_meta( $post_id, '_bbp_test_count', 3 );
     313                add_filter( 'update_post_metadata', $callback, 10, 3 );
     314                add_filter( 'bbp_bump_count_meta_max_attempts', $max_attempts );
     315                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 );
     316                remove_filter( 'bbp_bump_count_meta_max_attempts', $max_attempts, 10 );
     317                remove_filter( 'update_post_metadata', $callback, 10 );
     318
     319                $this->assertFalse( $result );
     320                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     321        }
     322
     323        /**
     324         * @covers ::bbp_bump_count_meta
     325         * @ticket BBP3678
     326         */
     327        public function test_bbp_bump_count_meta_preserves_an_interleaved_add() {
     328                $post_id     = self::factory()->post->create();
     329                $interleaved = false;
     330                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     331                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     332                                $interleaved = true;
     333                                bbp_bump_count_meta( 'post', $post_id, $meta_key );
     334                        }
     335
     336                        return $check;
     337                };
     338
     339                add_filter( 'add_post_metadata', $callback, 10, 3 );
     340                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     341                remove_filter( 'add_post_metadata', $callback, 10 );
     342
     343                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     344        }
     345
     346        /**
     347         * @covers ::bbp_bump_count_meta
     348         */
     349        public function test_bbp_bump_count_meta_runs_standard_metadata_actions() {
     350                $post_id        = self::factory()->post->create();
     351                $updated        = 0;
     352                $legacy_updated = 0;
     353                $callback = function( $meta_id, $updated_post_id, $meta_key, $meta_value ) use ( $post_id, &$updated ) {
     354                        if ( ( $post_id === $updated_post_id ) && ( '_bbp_test_count' === $meta_key ) && ( 2 === $meta_value ) ) {
     355                                $updated++;
     356                        }
     357                };
     358                $legacy_callback = function( $meta_id, $updated_post_id, $meta_key, $meta_value ) use ( $post_id, &$legacy_updated ) {
     359                        if ( ( $post_id === $updated_post_id ) && ( '_bbp_test_count' === $meta_key ) && ( 2 === $meta_value ) ) {
     360                                $legacy_updated++;
     361                        }
     362                };
     363
     364                update_post_meta( $post_id, '_bbp_test_count', 1 );
     365                add_action( 'updated_post_meta', $callback, 10, 4 );
     366                add_action( 'updated_postmeta', $legacy_callback, 10, 4 );
     367                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     368                remove_action( 'updated_post_meta', $callback, 10 );
     369                remove_action( 'updated_postmeta', $legacy_callback, 10 );
     370
     371                $this->assertSame( 1, $updated );
     372                $this->assertSame( 1, $legacy_updated );
     373        }
     374
     375        /**
    34376         * @covers ::bbp_rewrite
    35377         * @todo   Implement test_bbp_rewrite().
  • trunk/tests/phpunit/testcases/forums/functions/counts.php

    r7453 r7467  
    1111
    1212        /**
     13         * @covers BBPress::register_meta
     14         */
     15        public function test_hidden_total_count_meta_is_registered() {
     16                bbpress()->register_meta();
     17
     18                $registered = get_registered_meta_keys( 'post', bbp_get_forum_post_type() );
     19
     20                $this->assertArrayHasKey( '_bbp_total_topic_count_hidden', $registered );
     21                $this->assertArrayHasKey( '_bbp_total_reply_count_hidden', $registered );
     22        }
     23
     24        /**
    1325         * @covers ::bbp_get_countable_forum_statuses
    1426         */
     
    3446
    3547        /**
     48         * @covers ::bbp_update_forum_topic_count_hidden
     49         * @covers ::bbp_forum_query_subforum_ids
     50         * @ticket BBP3678
     51         */
     52        public function test_hidden_topic_counts_exclude_trashed_subforums() {
     53                $parent_id = $this->factory->forum->create( array(
     54                        'forum_meta' => array( 'forum_type' => 'category' ),
     55                ) );
     56                $forum_id = $this->factory->forum->create( array(
     57                        'post_parent' => $parent_id,
     58                        'forum_meta'  => array( 'forum_id' => $parent_id ),
     59                ) );
     60                $this->factory->topic->create( array(
     61                        'post_parent' => $forum_id,
     62                        'post_status' => bbp_get_spam_status_id(),
     63                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     64                ) );
     65
     66                $this->assertSame( 0, bbp_update_forum_topic_count_hidden( $parent_id ) );
     67                $this->assertSame( 1, bbp_get_forum_topic_count_hidden( $parent_id, true, true ) );
     68
     69                wp_trash_post( $forum_id );
     70
     71                $this->assertSame( 0, bbp_update_forum_topic_count_hidden( $parent_id ) );
     72        }
     73
     74        /**
     75         * @covers ::bbp_bump_forum_ancestor_count
     76         * @ticket BBP3678
     77         */
     78        public function test_bbp_bump_forum_ancestor_count_stops_outside_forum_hierarchy() {
     79                $forum_id = $this->factory->forum->create();
     80                $topic_id = $this->factory->topic->create( array(
     81                        'post_parent' => $forum_id,
     82                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     83                ) );
     84                $subforum_id = $this->factory->forum->create( array(
     85                        'post_parent' => $topic_id,
     86                        'forum_meta'  => array( 'forum_id' => $topic_id ),
     87                ) );
     88
     89                update_post_meta( $forum_id, '_bbp_total_topic_count', 2 );
     90                update_post_meta( $topic_id, '_bbp_total_topic_count', 3 );
     91
     92                bbp_bump_forum_ancestor_count( $subforum_id, '_bbp_total_topic_count', 1 );
     93
     94                $this->assertSame( 2, (int) get_post_meta( $forum_id, '_bbp_total_topic_count', true ) );
     95                $this->assertSame( 3, (int) get_post_meta( $topic_id, '_bbp_total_topic_count', true ) );
     96        }
     97
     98        /**
    3699         * Generic function to test the forum counts with a new topic
    37100         *
     
    90153
    91154        /**
     155         * @covers ::bbp_update_counts_on_transition_post_status
     156         */
     157        public function test_bbp_forum_draft_topic_is_not_counted_as_hidden() {
     158                $user_id  = $this->factory->user->create();
     159                $forum_id = $this->factory->forum->create();
     160                $topic_id = $this->factory->topic->create( array(
     161                        'post_author' => $user_id,
     162                        'post_parent' => $forum_id,
     163                        'post_status' => 'draft',
     164                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     165                ) );
     166
     167                $this->assertSame( 0, bbp_get_forum_topic_count( $forum_id, false, true ) );
     168                $this->assertSame( 0, bbp_get_forum_topic_count_hidden( $forum_id, false, true ) );
     169
     170                wp_update_post( array(
     171                        'ID'          => $topic_id,
     172                        'post_status' => bbp_get_pending_status_id(),
     173                ) );
     174                $this->assertSame( 1, bbp_get_forum_topic_count_hidden( $forum_id, false, true ) );
     175
     176                wp_update_post( array(
     177                        'ID'          => $topic_id,
     178                        'post_status' => 'draft',
     179                ) );
     180                $this->assertSame( 0, bbp_get_forum_topic_count_hidden( $forum_id, false, true ) );
     181        }
     182
     183        /**
    92184         * Generic function to test the forum counts on a trashed/untrashed topic
    93185         */
     
    266358
    267359        /**
     360         * @covers ::bbp_update_counts_on_transition_post_status
     361         */
     362        public function test_bbp_forum_reply_count_stays_excluded_when_pending_topic_is_trashed_and_restored() {
     363                $forum_id = $this->factory->forum->create();
     364                $topic_id = $this->factory->topic->create( array(
     365                        'post_parent' => $forum_id,
     366                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     367                ) );
     368
     369                $this->factory->reply->create( array(
     370                        'post_parent' => $topic_id,
     371                        'post_status' => bbp_get_public_status_id(),
     372                        'reply_meta'  => array(
     373                                'forum_id' => $forum_id,
     374                                'topic_id' => $topic_id,
     375                        ),
     376                ) );
     377
     378                bbp_unapprove_topic( $topic_id );
     379                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     380
     381                wp_trash_post( $topic_id );
     382                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     383
     384                wp_untrash_post( $topic_id );
     385                $this->assertSame( bbp_get_pending_status_id(), bbp_get_topic_status( $topic_id ) );
     386                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     387        }
     388
     389        /**
     390         * @covers ::bbp_update_counts_on_transition_post_status
     391         * @ticket BBP3678
     392         */
     393        public function test_topic_status_transition_updates_ancestor_forum_reply_counts() {
     394                $parent_id = $this->factory->forum->create( array(
     395                        'forum_meta' => array( 'forum_type' => 'category' ),
     396                ) );
     397                $forum_id = $this->factory->forum->create( array(
     398                        'post_parent' => $parent_id,
     399                        'forum_meta'  => array( 'forum_id' => $parent_id ),
     400                ) );
     401                $topic_id = $this->factory->topic->create( array(
     402                        'post_parent' => $forum_id,
     403                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     404                ) );
     405                $this->factory->reply->create( array(
     406                        'post_parent' => $topic_id,
     407                        'reply_meta'  => array(
     408                                'forum_id' => $forum_id,
     409                                'topic_id' => $topic_id,
     410                        ),
     411                ) );
     412
     413                $this->assertSame( 1, bbp_get_forum_reply_count( $parent_id, true, true ) );
     414
     415                bbp_unapprove_topic( $topic_id );
     416                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     417                $this->assertSame( 0, bbp_get_forum_reply_count( $parent_id, true, true ) );
     418
     419                bbp_approve_topic( $topic_id );
     420                $this->assertSame( 1, bbp_get_forum_reply_count( $forum_id, true, true ) );
     421                $this->assertSame( 1, bbp_get_forum_reply_count( $parent_id, true, true ) );
     422        }
     423
     424        /**
     425         * @covers ::bbp_update_counts_on_transition_post_status
     426         */
     427        public function test_bbp_forum_reply_count_excludes_new_public_reply_in_pending_topic() {
     428                $forum_id = $this->factory->forum->create();
     429                $topic_id = $this->factory->topic->create( array(
     430                        'post_parent' => $forum_id,
     431                        'post_status' => bbp_get_pending_status_id(),
     432                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     433                ) );
     434
     435                $this->factory->reply->create( array(
     436                        'post_parent' => $topic_id,
     437                        'post_status' => bbp_get_public_status_id(),
     438                        'reply_meta'  => array(
     439                                'forum_id' => $forum_id,
     440                                'topic_id' => $topic_id,
     441                        ),
     442                ) );
     443
     444                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     445                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $forum_id, true, true ) );
     446                $this->assertSame( 1, bbp_get_topic_reply_count( $topic_id, true ) );
     447                $this->assertSame( 0, bbp_update_forum_reply_count( $forum_id ) );
     448        }
     449
     450        /**
    268451         * @covers ::bbp_bump_forum_topic_count
    269452         */
     
    278461                $count = bbp_get_forum_topic_count( $f );
    279462                $this->assertSame( '1', $count );
     463        }
     464
     465        /**
     466         * @covers ::bbp_bump_forum_topic_count
     467         * @ticket BBP3678
     468         */
     469        public function test_bbp_bump_forum_topic_count_preserves_an_interleaved_update() {
     470                $forum_id   = $this->factory->forum->create();
     471                $interleaved = false;
     472                $callback    = function( $count, $filtered_forum_id ) use ( &$interleaved ) {
     473                        if ( ! $interleaved ) {
     474                                $interleaved = true;
     475                                bbp_bump_forum_topic_count( $filtered_forum_id );
     476                        }
     477
     478                        return $count;
     479                };
     480
     481                add_filter( 'bbp_get_forum_topic_count_int', $callback, 10, 2 );
     482                bbp_bump_forum_topic_count( $forum_id );
     483                remove_filter( 'bbp_get_forum_topic_count_int', $callback, 10 );
     484
     485                $this->assertSame( 2, bbp_get_forum_topic_count( $forum_id, false, true ) );
     486                $this->assertSame( 2, bbp_get_forum_topic_count( $forum_id, true, true ) );
    280487        }
    281488
     
    702909
    703910        /**
     911         * @covers ::bbp_update_forum_topic_count_hidden
     912         */
     913        public function test_bbp_update_forum_topic_count_hidden_rebuilds_total_counts() {
     914                $parent_id = $this->factory->forum->create();
     915                $child_id  = $this->factory->forum->create( array( 'post_parent' => $parent_id ) );
     916
     917                $this->factory->topic->create( array(
     918                        'post_parent' => $child_id,
     919                        'post_status' => bbp_get_pending_status_id(),
     920                        'topic_meta'  => array( 'forum_id' => $child_id ),
     921                ) );
     922
     923                update_post_meta( $parent_id, '_bbp_total_topic_count_hidden', 99 );
     924                update_post_meta( $child_id, '_bbp_total_topic_count_hidden', 99 );
     925
     926                $this->assertSame( 0, bbp_update_forum_topic_count_hidden( $parent_id ) );
     927                $this->assertSame( 0, bbp_get_forum_topic_count_hidden( $parent_id, false, true ) );
     928                $this->assertSame( 1, bbp_get_forum_topic_count_hidden( $parent_id, true, true ) );
     929                $this->assertSame( 1, bbp_get_forum_topic_count_hidden( $child_id, true, true ) );
     930        }
     931
     932        /**
    704933         * @covers ::bbp_update_forum_reply_count
    705934         */
  • trunk/tests/phpunit/testcases/forums/functions/query.php

    r6077 r7467  
    6969
    7070        /**
     71         * @covers ::bbp_forum_query_subforum_ids
     72         * @ticket BBP3678
     73         */
     74        public function test_bbp_forum_query_subforum_ids_excludes_uncountable_statuses() {
     75                $this->assertSame( array(), bbp_forum_query_subforum_ids( 0 ) );
     76
     77                $parent_id = $this->factory->forum->create();
     78                $public_id = $this->factory->forum->create( array(
     79                        'post_parent' => $parent_id,
     80                ) );
     81                $hidden_id = $this->factory->forum->create( array(
     82                        'post_parent' => $parent_id,
     83                        'post_status' => bbp_get_hidden_status_id(),
     84                ) );
     85                $private_id = $this->factory->forum->create( array(
     86                        'post_parent' => $parent_id,
     87                        'post_status' => bbp_get_private_status_id(),
     88                ) );
     89                $trash_id = $this->factory->forum->create( array(
     90                        'post_parent' => $parent_id,
     91                ) );
     92                wp_update_post( array(
     93                        'ID'          => $trash_id,
     94                        'post_status' => bbp_get_trash_status_id(),
     95                ) );
     96
     97                $this->assertEqualSets( array( $public_id, $private_id, $hidden_id ), bbp_forum_query_subforum_ids( $parent_id ) );
     98        }
     99
     100        /**
    71101         * @covers ::bbp_forum_query_last_reply_id
    72102         */
  • trunk/tests/phpunit/testcases/replies/functions/reply.php

    r7455 r7467  
    250250        /**
    251251         * @covers ::bbp_move_reply_count
    252          * @todo   Implement test_bbp_move_reply_count().
    253252         */
    254253        public function test_bbp_move_reply_count() {
    255                 // Remove the following lines when you implement this test.
    256                 $this->markTestIncomplete(
    257                         'This test has not been implemented yet.'
    258                 );
     254                $topic_author_id       = $this->factory->user->create();
     255                $reply_author_id       = $this->factory->user->create();
     256                $destination_author_id = $this->factory->user->create();
     257                $source_parent_id      = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     258                $destination_parent_id = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     259                $source_forum_id       = $this->factory->forum->create( array( 'post_parent' => $source_parent_id ) );
     260                $destination_forum_id  = $this->factory->forum->create( array( 'post_parent' => $destination_parent_id ) );
     261                $source_topic_id      = $this->factory->topic->create( array(
     262                        'post_author' => $topic_author_id,
     263                        'post_parent' => $source_forum_id,
     264                        'topic_meta'  => array( 'forum_id' => $source_forum_id ),
     265                ) );
     266                $destination_topic_id = $this->factory->topic->create( array(
     267                        'post_author' => $destination_author_id,
     268                        'post_parent' => $destination_forum_id,
     269                        'topic_meta'  => array( 'forum_id' => $destination_forum_id ),
     270                ) );
     271                $reply_id = $this->factory->reply->create( array(
     272                        'post_author' => $reply_author_id,
     273                        'post_parent' => $source_topic_id,
     274                        'reply_meta'  => array(
     275                                'forum_id' => $source_forum_id,
     276                                'topic_id' => $source_topic_id,
     277                        ),
     278                ) );
     279
     280                wp_update_post( array(
     281                        'ID'          => $reply_id,
     282                        'post_parent' => $destination_topic_id,
     283                ) );
     284                bbp_update_reply_topic_id( $reply_id, $destination_topic_id );
     285                bbp_update_reply_forum_id( $reply_id, $destination_forum_id );
     286                bbp_move_reply_count( $reply_id, $source_topic_id, $destination_topic_id );
     287
     288                $this->assertSame( 0, bbp_get_forum_reply_count( $source_forum_id, true, true ) );
     289                $this->assertSame( 1, bbp_get_forum_reply_count( $destination_forum_id, true, true ) );
     290                $this->assertSame( 0, bbp_get_forum_reply_count( $source_parent_id, true, true ) );
     291                $this->assertSame( 1, bbp_get_forum_reply_count( $destination_parent_id, true, true ) );
     292                $this->assertSame( 0, bbp_get_topic_reply_count( $source_topic_id, true ) );
     293                $this->assertSame( 1, bbp_get_topic_reply_count( $destination_topic_id, true ) );
     294                $this->assertEqualSets( array( $topic_author_id ), bbp_get_topic_engagements( $source_topic_id ) );
     295                $this->assertEqualSets( array( $reply_author_id, $destination_author_id ), bbp_get_topic_engagements( $destination_topic_id ) );
     296                $this->assertSame( 1, bbp_get_topic_voice_count( $source_topic_id, true ) );
     297                $this->assertSame( 2, bbp_get_topic_voice_count( $destination_topic_id, true ) );
     298        }
     299
     300        /**
     301         * @covers ::bbp_move_reply_count
     302         */
     303        public function test_bbp_move_reply_count_updates_hidden_counts() {
     304                $source_parent_id      = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     305                $destination_parent_id = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     306                $source_forum_id       = $this->factory->forum->create( array( 'post_parent' => $source_parent_id ) );
     307                $destination_forum_id  = $this->factory->forum->create( array( 'post_parent' => $destination_parent_id ) );
     308                $source_topic_id      = $this->factory->topic->create( array(
     309                        'post_parent' => $source_forum_id,
     310                        'topic_meta'  => array( 'forum_id' => $source_forum_id ),
     311                ) );
     312                $destination_topic_id = $this->factory->topic->create( array(
     313                        'post_parent' => $destination_forum_id,
     314                        'topic_meta'  => array( 'forum_id' => $destination_forum_id ),
     315                ) );
     316                $reply_id = $this->factory->reply->create( array(
     317                        'post_parent' => $source_topic_id,
     318                        'post_status' => bbp_get_pending_status_id(),
     319                        'reply_meta'  => array(
     320                                'forum_id' => $source_forum_id,
     321                                'topic_id' => $source_topic_id,
     322                        ),
     323                ) );
     324
     325                wp_update_post( array(
     326                        'ID'          => $reply_id,
     327                        'post_parent' => $destination_topic_id,
     328                ) );
     329                bbp_update_reply_topic_id( $reply_id, $destination_topic_id );
     330                bbp_update_reply_forum_id( $reply_id, $destination_forum_id );
     331                bbp_move_reply_count( $reply_id, $source_topic_id, $destination_topic_id );
     332
     333                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $source_forum_id, true, true ) );
     334                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $destination_forum_id, true, true ) );
     335                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $source_parent_id, true, true ) );
     336                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $destination_parent_id, true, true ) );
     337                $this->assertSame( 0, bbp_get_topic_reply_count_hidden( $source_topic_id, true ) );
     338                $this->assertSame( 1, bbp_get_topic_reply_count_hidden( $destination_topic_id, true ) );
     339        }
     340
     341        /**
     342         * @covers ::bbp_move_reply_count
     343         */
     344        public function test_bbp_move_reply_count_transfers_contribution_when_converted_to_topic() {
     345                $user_id  = $this->factory->user->create();
     346                $forum_id = $this->factory->forum->create();
     347                $topic_id = $this->factory->topic->create( array(
     348                        'post_parent' => $forum_id,
     349                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     350                ) );
     351                $reply_id = $this->factory->reply->create( array(
     352                        'post_author' => $user_id,
     353                        'post_parent' => $topic_id,
     354                        'reply_meta'  => array(
     355                                'forum_id' => $forum_id,
     356                                'topic_id' => $topic_id,
     357                        ),
     358                ) );
     359
     360                wp_update_post( array(
     361                        'ID'          => $reply_id,
     362                        'post_parent' => $forum_id,
     363                        'post_type'   => bbp_get_topic_post_type(),
     364                ) );
     365                bbp_update_topic_forum_id( $reply_id, $forum_id );
     366                bbp_update_topic_topic_id( $reply_id );
     367                bbp_move_reply_count( $reply_id, $topic_id, $reply_id );
     368
     369                $this->assertSame( 0, bbp_get_user_reply_count( $user_id, true ) );
     370                $this->assertSame( 1, bbp_get_user_topic_count( $user_id, true ) );
     371                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     372                $this->assertSame( 2, bbp_get_forum_topic_count( $forum_id, true, true ) );
    259373        }
    260374
  • trunk/tests/phpunit/testcases/topics/functions/counts.php

    r7449 r7467  
    9292
    9393        /**
     94         * @covers ::bbp_update_counts_on_transition_post_status
     95         */
     96        public function test_bbp_topic_draft_reply_is_not_counted_as_hidden() {
     97                $forum_id = $this->factory->forum->create();
     98                $topic_id = $this->factory->topic->create( array(
     99                        'post_parent' => $forum_id,
     100                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     101                ) );
     102                $reply_id = $this->factory->reply->create( array(
     103                        'post_parent' => $topic_id,
     104                        'post_status' => 'draft',
     105                        'reply_meta'  => array(
     106                                'forum_id' => $forum_id,
     107                                'topic_id' => $topic_id,
     108                        ),
     109                ) );
     110
     111                $this->assertSame( 0, bbp_get_topic_reply_count_hidden( $topic_id, true ) );
     112                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $forum_id, false, true ) );
     113
     114                wp_update_post( array(
     115                        'ID'          => $reply_id,
     116                        'post_status' => bbp_get_pending_status_id(),
     117                ) );
     118                $this->assertSame( 1, bbp_get_topic_reply_count_hidden( $topic_id, true ) );
     119                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $forum_id, false, true ) );
     120
     121                wp_update_post( array(
     122                        'ID'          => $reply_id,
     123                        'post_status' => 'draft',
     124                ) );
     125                $this->assertSame( 0, bbp_get_topic_reply_count_hidden( $topic_id, true ) );
     126                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $forum_id, false, true ) );
     127        }
     128
     129        /**
    94130         * Generic function to test the topic counts on a deleted reply
    95131         */
     
    380416
    381417        /**
     418         * @covers ::bbp_bump_topic_reply_count
     419         * @ticket BBP3678
     420         */
     421        public function test_bbp_bump_topic_reply_count_preserves_an_interleaved_update() {
     422                $topic_id   = $this->factory->topic->create();
     423                $interleaved = false;
     424                $callback    = function( $count, $filtered_topic_id ) use ( &$interleaved ) {
     425                        if ( ! $interleaved ) {
     426                                $interleaved = true;
     427                                bbp_bump_topic_reply_count( $filtered_topic_id );
     428                        }
     429
     430                        return $count;
     431                };
     432
     433                add_filter( 'bbp_get_topic_reply_count_int', $callback, 10, 2 );
     434                bbp_bump_topic_reply_count( $topic_id );
     435                remove_filter( 'bbp_get_topic_reply_count_int', $callback, 10 );
     436
     437                $this->assertSame( 2, bbp_get_topic_reply_count( $topic_id, true ) );
     438        }
     439
     440        /**
    382441         * @covers ::bbp_decrease_topic_reply_count
    383442         */
     
    589648                $count = bbp_get_topic_voice_count( $t );
    590649                $this->assertSame( '2', $count );
     650        }
     651
     652        /**
     653         * @covers ::bbp_get_topic_engagements_raw
     654         * @covers ::bbp_recalculate_topic_engagements
     655         * @covers ::bbp_update_topic_voice_count
     656         * @ticket BBP3678
     657         */
     658        public function test_topic_voice_count_honors_filtered_public_reply_statuses() {
     659                $topic_author_id = $this->factory->user->create();
     660                $reply_author_id = $this->factory->user->create();
     661                $forum_id        = $this->factory->forum->create();
     662                $topic_id        = $this->factory->topic->create( array(
     663                        'post_author' => $topic_author_id,
     664                        'post_parent' => $forum_id,
     665                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     666                ) );
     667                $public_statuses = function( $statuses ) {
     668                        $statuses[] = 'private';
     669                        return $statuses;
     670                };
     671
     672                add_filter( 'bbp_get_public_reply_statuses', $public_statuses );
     673
     674                try {
     675                        $this->factory->reply->create( array(
     676                                'post_author' => $reply_author_id,
     677                                'post_parent' => $topic_id,
     678                                'post_status' => 'private',
     679                                'reply_meta'  => array(
     680                                        'forum_id' => $forum_id,
     681                                        'topic_id' => $topic_id,
     682                                ),
     683                        ) );
     684
     685                        bbp_recalculate_topic_engagements( $topic_id, true );
     686                        bbp_update_topic_voice_count( $topic_id );
     687                } finally {
     688                        remove_filter( 'bbp_get_public_reply_statuses', $public_statuses, 10 );
     689                }
     690
     691                $this->assertSame( array( $topic_author_id, $reply_author_id ), bbp_get_topic_engagements( $topic_id ) );
     692                $this->assertSame( 2, bbp_get_topic_voice_count( $topic_id, true ) );
    591693        }
    592694
  • trunk/tests/phpunit/testcases/topics/functions/topic.php

    r7455 r7467  
    274274        /**
    275275         * @covers ::bbp_merge_topic_count
    276          * @todo   Implement test_bbp_merge_topic_count().
    277276         */
    278277        public function test_bbp_merge_topic_count() {
    279                 // Remove the following lines when you implement this test.
    280                 $this->markTestIncomplete(
    281                         'This test has not been implemented yet.'
    282                 );
     278                $source_author_id      = $this->factory->user->create();
     279                $reply_author_id       = $this->factory->user->create();
     280                $destination_author_id = $this->factory->user->create();
     281                $source_parent_id      = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     282                $destination_parent_id = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     283                $source_forum_id       = $this->factory->forum->create( array( 'post_parent' => $source_parent_id ) );
     284                $destination_forum_id  = $this->factory->forum->create( array( 'post_parent' => $destination_parent_id ) );
     285                $source_topic_id      = $this->factory->topic->create( array(
     286                        'post_author' => $source_author_id,
     287                        'post_parent' => $source_forum_id,
     288                        'topic_meta'  => array( 'forum_id' => $source_forum_id ),
     289                ) );
     290                $destination_topic_id = $this->factory->topic->create( array(
     291                        'post_author' => $destination_author_id,
     292                        'post_parent' => $destination_forum_id,
     293                        'topic_meta'  => array( 'forum_id' => $destination_forum_id ),
     294                ) );
     295                $reply_id = $this->factory->reply->create( array(
     296                        'post_author' => $reply_author_id,
     297                        'post_parent' => $source_topic_id,
     298                        'reply_meta'  => array(
     299                                'forum_id' => $source_forum_id,
     300                                'topic_id' => $source_topic_id,
     301                        ),
     302                ) );
     303
     304                wp_update_post( array(
     305                        'ID'          => $reply_id,
     306                        'post_parent' => $destination_topic_id,
     307                ) );
     308                bbp_update_reply_topic_id( $reply_id, $destination_topic_id );
     309                bbp_update_reply_forum_id( $reply_id, $destination_forum_id );
     310                wp_update_post( array(
     311                        'ID'          => $source_topic_id,
     312                        'post_parent' => $destination_topic_id,
     313                        'post_type'   => bbp_get_reply_post_type(),
     314                ) );
     315                bbp_update_reply_topic_id( $source_topic_id, $destination_topic_id );
     316                bbp_update_reply_forum_id( $source_topic_id, $destination_forum_id );
     317                bbp_merge_topic_count( $destination_topic_id, $source_topic_id, $source_forum_id );
     318
     319                $this->assertSame( 0, bbp_get_forum_topic_count( $source_forum_id, true, true ) );
     320                $this->assertSame( 0, bbp_get_forum_reply_count( $source_forum_id, true, true ) );
     321                $this->assertSame( 2, bbp_get_forum_reply_count( $destination_forum_id, true, true ) );
     322                $this->assertSame( 0, bbp_get_forum_topic_count( $source_parent_id, true, true ) );
     323                $this->assertSame( 0, bbp_get_forum_reply_count( $source_parent_id, true, true ) );
     324                $this->assertSame( 2, bbp_get_forum_reply_count( $destination_parent_id, true, true ) );
     325                $this->assertSame( 2, bbp_get_topic_reply_count( $destination_topic_id, true ) );
     326                $this->assertSame( 0, bbp_get_user_topic_count( $source_author_id, true ) );
     327                $this->assertSame( 1, bbp_get_user_reply_count( $source_author_id, true ) );
     328                $this->assertEqualSets( array( $source_author_id, $reply_author_id, $destination_author_id ), bbp_get_topic_engagements( $destination_topic_id ) );
     329                $this->assertSame( 3, bbp_get_topic_voice_count( $destination_topic_id, true ) );
    283330        }
    284331
     
    296343        /**
    297344         * @covers ::bbp_split_topic_count
    298          * @todo   Implement test_bbp_split_topic_count().
    299345         */
    300346        public function test_bbp_split_topic_count() {
    301                 // Remove the following lines when you implement this test.
    302                 $this->markTestIncomplete(
    303                         'This test has not been implemented yet.'
    304                 );
     347                $source_parent_id      = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     348                $destination_parent_id = $this->factory->forum->create( array( 'forum_meta' => array( 'forum_type' => 'category' ) ) );
     349                $source_forum_id       = $this->factory->forum->create( array( 'post_parent' => $source_parent_id ) );
     350                $destination_forum_id  = $this->factory->forum->create( array( 'post_parent' => $destination_parent_id ) );
     351                $source_topic_id      = $this->factory->topic->create( array(
     352                        'post_parent' => $source_forum_id,
     353                        'topic_meta'  => array( 'forum_id' => $source_forum_id ),
     354                ) );
     355                $destination_topic_id = $this->factory->topic->create( array(
     356                        'post_parent' => $destination_forum_id,
     357                        'topic_meta'  => array( 'forum_id' => $destination_forum_id ),
     358                ) );
     359                $reply_id = $this->factory->reply->create( array(
     360                        'post_parent' => $source_topic_id,
     361                        'reply_meta'  => array(
     362                                'forum_id' => $source_forum_id,
     363                                'topic_id' => $source_topic_id,
     364                        ),
     365                ) );
     366
     367                wp_update_post( array(
     368                        'ID'          => $reply_id,
     369                        'post_parent' => $destination_topic_id,
     370                ) );
     371                bbp_update_reply_topic_id( $reply_id, $destination_topic_id );
     372                bbp_update_reply_forum_id( $reply_id, $destination_forum_id );
     373                bbp_split_topic_count( $reply_id, $source_topic_id, $destination_topic_id );
     374
     375                $this->assertSame( 0, bbp_get_forum_reply_count( $source_forum_id, true, true ) );
     376                $this->assertSame( 1, bbp_get_forum_reply_count( $destination_forum_id, true, true ) );
     377                $this->assertSame( 0, bbp_get_forum_reply_count( $source_parent_id, true, true ) );
     378                $this->assertSame( 1, bbp_get_forum_reply_count( $destination_parent_id, true, true ) );
     379                $this->assertSame( 0, bbp_get_topic_reply_count( $source_topic_id, true ) );
     380                $this->assertSame( 1, bbp_get_topic_reply_count( $destination_topic_id, true ) );
     381        }
     382
     383        /**
     384         * @covers ::bbp_split_topic_count
     385         * @ticket BBP3678
     386         */
     387        public function test_bbp_split_topic_count_updates_converted_reply_counts_and_engagements() {
     388                $source_author_id = $this->factory->user->create();
     389                $reply_author_id  = $this->factory->user->create();
     390                $forum_id         = $this->factory->forum->create();
     391                $source_topic_id  = $this->factory->topic->create( array(
     392                        'post_author' => $source_author_id,
     393                        'post_parent' => $forum_id,
     394                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     395                ) );
     396                $from_reply_id = $this->factory->reply->create( array(
     397                        'post_author' => $reply_author_id,
     398                        'post_parent' => $source_topic_id,
     399                        'reply_meta'  => array(
     400                                'forum_id' => $forum_id,
     401                                'topic_id' => $source_topic_id,
     402                        ),
     403                ) );
     404
     405                wp_update_post( array(
     406                        'ID'          => $from_reply_id,
     407                        'post_parent' => $forum_id,
     408                        'post_type'   => bbp_get_topic_post_type(),
     409                ) );
     410                bbp_update_topic_topic_id( $from_reply_id );
     411                bbp_update_topic_forum_id( $from_reply_id, $forum_id );
     412
     413                bbp_split_topic_count( $from_reply_id, $source_topic_id, $from_reply_id );
     414
     415                $this->assertSame( 2, bbp_get_forum_topic_count( $forum_id, true, true ) );
     416                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, true, true ) );
     417                $this->assertSame( 1, bbp_get_user_topic_count( $source_author_id, true ) );
     418                $this->assertSame( 1, bbp_get_user_topic_count( $reply_author_id, true ) );
     419                $this->assertSame( 0, bbp_get_user_reply_count( $reply_author_id, true ) );
     420                $this->assertSame( array( $source_author_id ), bbp_get_topic_engagements( $source_topic_id ) );
     421                $this->assertSame( array( $reply_author_id ), bbp_get_topic_engagements( $from_reply_id ) );
     422                $this->assertSame( 1, bbp_get_topic_voice_count( $source_topic_id, true ) );
     423                $this->assertSame( 1, bbp_get_topic_voice_count( $from_reply_id, true ) );
    305424        }
    306425
  • trunk/tests/phpunit/testcases/users/functions/counts.php

    r7449 r7467  
    247247                $count = bbp_get_user_topic_count( $u, $integer );
    248248                $this->assertSame( $int_value + 1, $count );
     249        }
     250
     251        /**
     252         * @covers ::bbp_bump_user_topic_count
     253         * @covers ::bbp_update_user_topic_count
     254         * @ticket BBP3678
     255         */
     256        public function test_bbp_bump_user_topic_count_preserves_an_interleaved_update() {
     257                $user_id     = $this->factory->user->create();
     258                $interleaved = false;
     259                $callback    = function( $count, $filtered_user_id ) use ( &$interleaved ) {
     260                        if ( ! $interleaved ) {
     261                                $interleaved = true;
     262                                bbp_bump_user_topic_count( $filtered_user_id );
     263                        }
     264
     265                        return $count;
     266                };
     267
     268                bbp_update_user_topic_count( $user_id, 0 );
     269                add_filter( 'bbp_get_user_topic_count_int', $callback, 10, 2 );
     270                bbp_bump_user_topic_count( $user_id );
     271                remove_filter( 'bbp_get_user_topic_count_int', $callback, 10 );
     272
     273                $this->assertSame( 2, bbp_get_user_topic_count( $user_id, true ) );
     274        }
     275
     276        /**
     277         * @covers ::bbp_bump_user_topic_count
     278         * @covers ::bbp_update_user_topic_count
     279         */
     280        public function test_bbp_bump_user_topic_count_preserves_count_filters() {
     281                $user_id      = $this->factory->user->create();
     282                $atomic_calls = 0;
     283                $bump         = function( $count ) {
     284                        return $count + 4;
     285                };
     286                $update   = function( $count ) {
     287                        return $count + 3;
     288                };
     289                $atomic = function( $check ) use ( &$atomic_calls ) {
     290                        $atomic_calls++;
     291                        return $check;
     292                };
     293
     294                bbp_update_user_topic_count( $user_id, 5 );
     295                add_filter( 'bbp_bump_user_topic_count', $bump );
     296                add_filter( 'bbp_update_user_topic_count', $update );
     297                add_filter( 'bbp_pre_bump_count_meta', $atomic );
     298                bbp_bump_user_topic_count( $user_id );
     299                remove_filter( 'bbp_bump_user_topic_count', $bump, 10 );
     300                remove_filter( 'bbp_update_user_topic_count', $update, 10 );
     301                remove_filter( 'bbp_pre_bump_count_meta', $atomic, 10 );
     302
     303                $this->assertSame( 13, bbp_get_user_topic_count( $user_id, true ) );
     304                $this->assertSame( 0, $atomic_calls );
     305        }
     306
     307        /**
     308         * @covers ::bbp_bump_user_reply_count
     309         * @covers ::bbp_update_user_reply_count
     310         */
     311        public function test_bbp_bump_user_reply_count_preserves_an_absolute_update_filter() {
     312                $user_id      = $this->factory->user->create();
     313                $atomic_calls = 0;
     314                $update       = function() {
     315                        return 100;
     316                };
     317                $atomic = function( $check ) use ( &$atomic_calls ) {
     318                        $atomic_calls++;
     319                        return $check;
     320                };
     321
     322                bbp_update_user_reply_count( $user_id, 5 );
     323                add_filter( 'bbp_update_user_reply_count', $update );
     324                add_filter( 'bbp_pre_bump_count_meta', $atomic );
     325                bbp_bump_user_reply_count( $user_id );
     326                remove_filter( 'bbp_update_user_reply_count', $update, 10 );
     327                remove_filter( 'bbp_pre_bump_count_meta', $atomic, 10 );
     328
     329                $this->assertSame( 100, bbp_get_user_reply_count( $user_id, true ) );
     330                $this->assertSame( 0, $atomic_calls );
     331        }
     332
     333        /**
     334         * @covers ::bbp_bump_user_topic_count
     335         * @covers ::bbp_update_user_topic_count
     336         * @ticket BBP3678
     337         */
     338        public function test_bbp_bump_user_topic_count_uses_the_current_site_option() {
     339                if ( ! is_multisite() ) {
     340                        $this->markTestSkipped( 'Requires multisite.' );
     341                }
     342
     343                $user_id = $this->factory->user->create();
     344                $site_id = $this->factory->blog->create();
     345
     346                bbp_update_user_topic_count( $user_id, 5 );
     347                switch_to_blog( $site_id );
     348
     349                try {
     350                        bbp_update_user_topic_count( $user_id, 7 );
     351                        bbp_bump_user_topic_count( $user_id );
     352                        $this->assertSame( 8, bbp_get_user_topic_count( $user_id, true ) );
     353                        $this->assertSame( 8, (int) get_user_meta( $user_id, bbp_db()->get_blog_prefix() . '_bbp_topic_count', true ) );
     354                } finally {
     355                        restore_current_blog();
     356                }
     357
     358                $this->assertSame( 5, bbp_get_user_topic_count( $user_id, true ) );
    249359        }
    250360
     
    402512
    403513        /**
     514         * @covers ::bbp_update_counts_on_user_reassignment
     515         * @ticket BBP3678
     516         */
     517        public function test_deleting_user_with_reassignment_updates_counts_and_engagements() {
     518                $deleted_user_id  = $this->factory->user->create();
     519                $reassign_user_id = $this->factory->user->create();
     520                $forum_id         = $this->factory->forum->create();
     521                $topic_id         = $this->factory->topic->create( array(
     522                        'post_author' => $deleted_user_id,
     523                        'post_parent' => $forum_id,
     524                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     525                ) );
     526                $deleted_user_reply_id = $this->factory->reply->create( array(
     527                        'post_author' => $deleted_user_id,
     528                        'post_parent' => $topic_id,
     529                        'reply_meta'  => array(
     530                                'forum_id' => $forum_id,
     531                                'topic_id' => $topic_id,
     532                        ),
     533                ) );
     534                $this->factory->reply->create( array(
     535                        'post_author' => $reassign_user_id,
     536                        'post_parent' => $topic_id,
     537                        'reply_meta'  => array(
     538                                'forum_id' => $forum_id,
     539                                'topic_id' => $topic_id,
     540                        ),
     541                ) );
     542                $reply_only_topic_id = $this->factory->topic->create( array(
     543                        'post_author' => $reassign_user_id,
     544                        'post_parent' => $forum_id,
     545                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     546                ) );
     547                $this->factory->reply->create( array(
     548                        'post_author' => $deleted_user_id,
     549                        'post_parent' => $reply_only_topic_id,
     550                        'reply_meta'  => array(
     551                                'forum_id' => $forum_id,
     552                                'topic_id' => $reply_only_topic_id,
     553                        ),
     554                ) );
     555
     556                $this->assertSame( 1, bbp_get_user_topic_count( $deleted_user_id, true ) );
     557                $this->assertSame( 2, bbp_get_user_reply_count( $deleted_user_id, true ) );
     558                $this->assertSame( 1, bbp_get_user_topic_count( $reassign_user_id, true ) );
     559                $this->assertSame( 1, bbp_get_user_reply_count( $reassign_user_id, true ) );
     560                $this->assertSame( 2, bbp_get_topic_voice_count( $topic_id, true ) );
     561                $this->assertSame( 2, bbp_get_topic_voice_count( $reply_only_topic_id, true ) );
     562
     563                wp_delete_user( $deleted_user_id, $reassign_user_id );
     564
     565                $this->assertSame( $reassign_user_id, (int) get_post_field( 'post_author', $topic_id ) );
     566                $this->assertSame( $reassign_user_id, (int) get_post_field( 'post_author', $deleted_user_reply_id ) );
     567                $this->assertSame( 2, bbp_get_user_topic_count( $reassign_user_id, true ) );
     568                $this->assertSame( 3, bbp_get_user_reply_count( $reassign_user_id, true ) );
     569                $this->assertSame( array( $reassign_user_id ), bbp_get_topic_engagements( $topic_id ) );
     570                $this->assertSame( array( $reassign_user_id ), bbp_get_topic_engagements( $reply_only_topic_id ) );
     571                $this->assertSame( 1, bbp_get_topic_voice_count( $topic_id, true ) );
     572                $this->assertSame( 1, bbp_get_topic_voice_count( $reply_only_topic_id, true ) );
     573        }
     574
     575        /**
     576         * @covers ::bbp_update_counts_on_user_reassignment
     577         * @ticket BBP3678
     578         */
     579        public function test_deleting_user_with_reassignment_updates_current_multisite_counts() {
     580                if ( ! is_multisite() ) {
     581                        $this->markTestSkipped( 'Requires multisite.' );
     582                }
     583
     584                $deleted_user_id  = $this->factory->user->create();
     585                $reassign_user_id = $this->factory->user->create();
     586                $site_id          = $this->factory->blog->create();
     587
     588                add_user_to_blog( $site_id, $deleted_user_id, 'subscriber' );
     589                add_user_to_blog( $site_id, $reassign_user_id, 'subscriber' );
     590                switch_to_blog( $site_id );
     591
     592                try {
     593                        $forum_id = $this->factory->forum->create();
     594                        $this->factory->topic->create( array(
     595                                'post_author' => $deleted_user_id,
     596                                'post_parent' => $forum_id,
     597                                'topic_meta'  => array( 'forum_id' => $forum_id ),
     598                        ) );
     599
     600                        wp_delete_user( $deleted_user_id, $reassign_user_id );
     601
     602                        $meta_key = bbp_db()->get_blog_prefix() . '_bbp_topic_count';
     603                        $this->assertSame( 1, bbp_get_user_topic_count( $reassign_user_id, true ) );
     604                        $this->assertSame( 1, (int) get_user_meta( $reassign_user_id, $meta_key, true ) );
     605                } finally {
     606                        restore_current_blog();
     607                }
     608        }
     609
     610        /**
     611         * @covers ::bbp_make_spam_user
     612         * @covers ::bbp_make_ham_user
     613         * @covers ::bbp_update_counts_on_transition_post_status
     614         * @ticket BBP3678
     615         */
     616        public function test_bbp_make_spam_and_ham_user_updates_counts() {
     617                $user_id  = $this->factory->user->create();
     618                $forum_id = $this->factory->forum->create();
     619                $topic_id = $this->factory->topic->create( array(
     620                        'post_author' => $user_id,
     621                        'post_parent' => $forum_id,
     622                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     623                ) );
     624                $this->factory->reply->create( array(
     625                        'post_author' => $user_id,
     626                        'post_parent' => $topic_id,
     627                        'reply_meta'  => array(
     628                                'forum_id' => $forum_id,
     629                                'topic_id' => $topic_id,
     630                        ),
     631                ) );
     632
     633                $this->assertTrue( bbp_make_spam_user( $user_id ) );
     634                $this->assertSame( 0, bbp_get_user_topic_count( $user_id, true ) );
     635                $this->assertSame( 0, bbp_get_user_reply_count( $user_id, true ) );
     636                $this->assertSame( 0, bbp_get_forum_topic_count( $forum_id, false, true ) );
     637                $this->assertSame( 0, bbp_get_forum_reply_count( $forum_id, false, true ) );
     638                $this->assertSame( 1, bbp_get_forum_topic_count_hidden( $forum_id, false, true ) );
     639                $this->assertSame( 1, bbp_get_forum_reply_count_hidden( $forum_id, false, true ) );
     640
     641                $this->assertTrue( bbp_make_ham_user( $user_id ) );
     642                $this->assertSame( 1, bbp_get_user_topic_count( $user_id, true ) );
     643                $this->assertSame( 1, bbp_get_user_reply_count( $user_id, true ) );
     644                $this->assertSame( 1, bbp_get_forum_topic_count( $forum_id, false, true ) );
     645                $this->assertSame( 1, bbp_get_forum_reply_count( $forum_id, false, true ) );
     646                $this->assertSame( 0, bbp_get_forum_topic_count_hidden( $forum_id, false, true ) );
     647                $this->assertSame( 0, bbp_get_forum_reply_count_hidden( $forum_id, false, true ) );
     648        }
     649
     650        /**
     651         * @covers ::bbp_update_counts_on_post_author_change
     652         */
     653        public function test_changing_post_authors_updates_user_counts() {
     654                $old_user_id = $this->factory->user->create();
     655                $new_user_id = $this->factory->user->create();
     656                $forum_id    = $this->factory->forum->create();
     657                $topic_id    = $this->factory->topic->create( array(
     658                        'post_author' => $old_user_id,
     659                        'post_parent' => $forum_id,
     660                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     661                ) );
     662                $reply_id = $this->factory->reply->create( array(
     663                        'post_author' => $old_user_id,
     664                        'post_parent' => $topic_id,
     665                        'reply_meta'  => array(
     666                                'forum_id' => $forum_id,
     667                                'topic_id' => $topic_id,
     668                        ),
     669                ) );
     670
     671                wp_update_post( array( 'ID' => $topic_id, 'post_author' => $new_user_id ) );
     672                $this->assertSame( 2, bbp_get_topic_voice_count( $topic_id, true ) );
     673
     674                wp_update_post( array( 'ID' => $reply_id, 'post_author' => $new_user_id ) );
     675                $this->assertSame( 1, bbp_get_topic_voice_count( $topic_id, true ) );
     676
     677                $this->assertSame( 0, bbp_get_user_topic_count( $old_user_id, true ) );
     678                $this->assertSame( 0, bbp_get_user_reply_count( $old_user_id, true ) );
     679                $this->assertSame( 1, bbp_get_user_topic_count( $new_user_id, true ) );
     680                $this->assertSame( 1, bbp_get_user_reply_count( $new_user_id, true ) );
     681        }
     682
     683        /**
     684         * @covers ::bbp_update_counts_on_post_author_change
     685         */
     686        public function test_changing_post_author_and_status_rebuilds_user_counts() {
     687                $old_user_id = $this->factory->user->create();
     688                $new_user_id = $this->factory->user->create();
     689                $forum_id    = $this->factory->forum->create();
     690                $topic_id    = $this->factory->topic->create( array(
     691                        'post_author' => $old_user_id,
     692                        'post_parent' => $forum_id,
     693                        'topic_meta'  => array( 'forum_id' => $forum_id ),
     694                ) );
     695
     696                wp_update_post( array(
     697                        'ID'          => $topic_id,
     698                        'post_author' => $new_user_id,
     699                        'post_status' => bbp_get_pending_status_id(),
     700                ) );
     701
     702                $this->assertSame( 0, bbp_get_user_topic_count( $old_user_id, true ) );
     703                $this->assertSame( 0, bbp_get_user_topic_count( $new_user_id, true ) );
     704        }
     705
     706        /**
    404707         * @covers ::bbp_increase_user_topic_count
    405708         */
Note: See TracChangeset for help on using the changeset viewer.