Make WordPress Core

Opened 5 weeks ago

Last modified 4 days ago

#65053 assigned defect (bug)

Media upload from the post edit screen shows an inaccurate number when uploading multiple media files

Reported by: dharm1025's profile Dharm1025 Owned by: adamsilverstein's profile adamsilverstein
Milestone: 7.1 Priority: normal
Severity: normal Version: 6.3
Component: Upload Keywords: has-patch has-test-info has-unit-tests commit
Focuses: ui, javascript Cc:

Description

When uploading multiple media files from the Edit Post screen via “Set featured image”, it displays incorrect numbers.

Steps to reproduce:

  1. Create a new post.
  2. Click on “Set featured image.”
  3. Go to “Upload files” and upload multiple files.
  4. Notice that the numbers are inaccurate (you may need to repeat step 3 multiple times to reproduce the issue).

https://cldup.com/YXkXrw9K0z.png

Attachments (4)

Screenshot 2026-04-09 at 3.52.52 PM.png (381.7 KB) - added by Dharm1025 5 weeks ago.
Screenshot
uploading.png (342.0 KB) - added by adamsilverstein 5 weeks ago.
65053-before-PR-11585.png (534.9 KB) - added by ozgursar 4 weeks ago.
Before patch 11585
65053-after-PR-11585.png (454.7 KB) - added by ozgursar 4 weeks ago.
After patch 11585

Download all attachments as: .zip

Change History (22)

#1 @adamsilverstein
5 weeks ago

Thanks for the bug report @Dharm1025 - I was able to reproduce this immediately in the editor. I cleared my media library, then dragging 10 files onto the editor the display read "Showing 10 of 11".

I was not able to reproduce in the media library itself, these seems specific to the editor. I was also not able to reproduce it using the gallery inserter or by dragging a group of images onto the editor. It seems tied to invoking the uploader from the "Set Featured Image" button as you described.

It feels a bit buggy that you can select more than one image at all from this upload dialog, only one image can be the featured image. So maybe the bug here is the inserter should only allow single image selection (similar to the image block inserter). Then you would never see the mismatched count your report identified.

This may need a Gutenberg PR (and matching issue) to fix, depending on the exact issue.

Last edited 5 weeks ago by adamsilverstein (previous) (diff)

#2 @adamsilverstein
5 weeks ago

@Dharm1025 if possible, can you verify that https://github.com/WordPress/gutenberg/pull/77213 fixes your issue?

#3 @adamsilverstein
5 weeks ago

I tested the PR manually and verified I can only select a single image from the featured image button. Are you seeing this problem elsewhere?

#4 @wildworks
5 weeks ago

My investigation on Playground revealed that this issue might have been present since at least WordPress 6.3. For some reason, the block editor and the core media modal may not be interacting correctly.

#5 @jadavsanjay
4 weeks ago

Reproduction Report

Description

This report validates whether the issue can be reproduced.

Environment

  • WordPress: 6.9.4
  • PHP: 8.3.30
  • Server: PHP.wasm
  • Database: WP_SQLite_Driver (Server: 8.0.38 / Client: 3.51.0)
  • Browser: Chrome 147.0.0.0
  • OS: Windows 10/11
  • Theme: Twenty Twenty-Five 1.4
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.1

Actual Results

  1. ✅ Error condition occurs (reproduced): The media library counter (e.g., "Showing X of Y") does not sync correctly with the actual number of uploaded files during the batch process..

Additional Notes

  • The issue specifically triggers when the media modal is opened via the "Set Featured Image" button in the post editor.

The mismatch in counting persists even after the upload process is completed until the modal is refreshed.

Supplemental Artifacts

https://postimg.cc/qNmVQspF

This ticket was mentioned in PR #11585 on WordPress/wordpress-develop by @hbhalodia.


4 weeks ago
#6

  • Keywords has-patch added

Trac ticket: https://core.trac.wordpress.org/ticket/65053

## Use of AI Tools

AI assistance: Yes
Tool(s): GitHub Copilot, Claude
Model(s): Claude Sonnet 4.6
Used for: Identifying root cause and for a fix. Have reviwed the final implementation and raised the PR.

# Detailed Claude Analysis and fix

## Root Cause Analysis: "Showing X of Y media items" shows incorrect total count

Trac ticket: https://core.trac.wordpress.org/ticket/65053
Related PR: https://github.com/WordPress/gutenberg/pull/77213
Affected files: src/js/media/models/attachments.js

---

### The Symptom

When opening the media library modal (e.g. via "Set featured image" or an Image block), the footer media count displayed an incorrect total count:

Showing 8 of 7 media items

The displayed count (8) was correct, but the total (7) was one short of the real total.

---

### Background: How the count works

The media library uses a Backbone.js collection architecture with three key classes:

Class Role
wp.media.model.Query Fetches paginated attachments from the server via AJAX. Stores totalAttachments parsed from the X-WP-Total response header.
wp.media.model.Attachments A client-side collection that can mirror a Query and/or observe additional collections (e.g. the selection).
wp.media.model.Selection Extends Attachments. Holds the currently selected items in the modal.

The "Showing X of Y" text in the Load More bar is rendered by AttachmentsBrowser.updateLoadMoreView():

sprintf(
    __( 'Showing %1$s of %2$s media items' ),
    this.collection.length,             // items currently displayed
    this.collection.getTotalAttachments() // total on the server
)

getTotalAttachments() reads directly from the mirrored Query collection's totalAttachments property:

getTotalAttachments: function() {
    return this.mirroring ? this.mirroring.totalAttachments : 0;
},

The totalAttachments on the Query is updated in real-time via two private methods bound as event listeners:

_addToTotalAttachments: function() {
    if ( this.mirroring ) {
        this.mirroring.totalAttachments = this.mirroring.totalAttachments + 1;
    }
},
_removeFromTotalAttachments: function() {
    if ( this.mirroring ) {
        this.mirroring.totalAttachments = this.mirroring.totalAttachments - 1;
    }
},

---

### The Root Cause

In wp.media.model.Attachments, the observe() method is responsible for watching another collection and replicating its add/remove/reset events. Before this fix, it also registered the total-count trackers on every observed collection:

// BEFORE (buggy):
observe: function( attachments ) {
    this.observers = this.observers || [];
    this.observers.push( attachments );

    attachments.on( 'add change remove', this._validateHandler, this );
    attachments.on( 'add',    this._addToTotalAttachments,      this ); // ← too broad
    attachments.on( 'remove', this._removeFromTotalAttachments, this ); // ← too broad
    attachments.on( 'reset',  this._validateAllHandler,         this );
    this.validateAll( attachments );
    return this;
},

The critical detail is that observe() can be called on more than just the Query collection. Both wp.media.controller.FeaturedImage and wp.media.controller.ReplaceImage call:

// featured-image.js (line 83) and replace-image.js (line 86):
library.observe( this.get('selection') );

This is intentional and correct behaviour for *display* purposes — it ensures that a pre-existing featured image (which might not be in the first page of query results) is always visible in the grid even if the server hasn't returned it yet.

However, it also meant that the total-count handlers were now bound to the Selection collection as well as the Query collection.

#### The event cascade that causes the wrong count

  1. User opens the "Set featured image" modal. A featured image is already set (ID: 42).
  2. library.observe(selection) is called. The selection contains 1 item (attachment 42).
  3. The server responds with 8 total attachments → query.totalAttachments = 8. ✅
  4. User clicks a different image (ID: 99) to set as the new featured image.
  5. Selection.add() is called for attachment 99. Because this.multiple === false, it first calls this.remove(this.models) — removing attachment 42 from the selection.
  6. The remove event fires on the Selection collection.
  7. _removeFromTotalAttachments is triggered (it was bound to the selection in step 2) → query.totalAttachments is decremented: 8 → 7. ❌
  8. Selection.add() completes, adding attachment 99, firing add_addToTotalAttachments → 7 → 8. ✅ (briefly correct again)
  9. But updateLoadMoreView is debounced (10ms). If it fires between steps 7 and 8 (or if the add event is never fired due to validation), the UI shows "Showing 8 of 7".

Additionally, in the general case, any add/remove on the selection (without a corresponding add/remove on the query) permanently corrupts totalAttachments for the lifetime of the modal session.

---

### The Fix

The total-count event bindings (_addToTotalAttachments / _removeFromTotalAttachments) belong exclusively on the primary query collection being mirrored — not on every additionally-observed collection.

The fix moves these bindings from observe() into mirror(), which is the method that establishes the relationship with the Query collection:

// AFTER (fixed):
observe: function( attachments ) {
    this.observers = this.observers || [];
    this.observers.push( attachments );

    attachments.on( 'add change remove', this._validateHandler, this );
    // _addToTotalAttachments / _removeFromTotalAttachments removed from here
    attachments.on( 'reset', this._validateAllHandler, this );
    this.validateAll( attachments );
    return this;
},

mirror: function( attachments ) {
    if ( this.mirroring && this.mirroring === attachments ) {
        return this;
    }

    this.unmirror();
    this.mirroring = attachments;

    this.reset( [], { silent: true } );
    this.observe( attachments );

    // Bind total attachment count tracking only to the mirrored query
    // collection, not to additional observed collections (e.g. selection).
    attachments.on( 'add',    this._addToTotalAttachments,      this );
    attachments.on( 'remove', this._removeFromTotalAttachments, this );

    this.trigger( 'attachments:received', this );
    return this;
},

#### Why this is safe

  • mirror() is only ever called with a wp.media.model.Query instance (via _requery()). The query accurately reflects server-side counts, so attaching the total-count trackers there is correct.
  • observe() is called for both the query (via mirror()) and supplemental collections like the selection. After this fix, observe() only handles item visibility/validation, which is its original purpose.
  • The unmirror() path is unaffected: it calls unobserve(this.mirroring) which calls .off(null, null, this) on the query, removing all handlers bound with this as the context — including the total-count ones added in mirror().
  • There are no other callers of observe() in core that expect the total-count side effect.

---

### Files Changed

File Type Change
attachments.js Source Moved _addToTotalAttachments/_removeFromTotalAttachments bindings from observe() to mirror()

---

### Fix Location: Core, not Gutenberg

Although this bug is reproducible through the "Set featured image" panel in Gutenberg, the root cause is in the legacy media library JavaScript stack in WordPress Core. The Attachments, Selection, and Query Backbone models are part of wp.media which lives in Core (wp-includes/js/). Gutenberg's MediaUpload component is ultimately a wrapper that opens this same modal. Adding multiple={false} to MediaUpload (as proposed in the Gutenberg PR #77213) would address the UX problem of allowing multi-select but would not fix this count bug, which is triggered by any single-selection change in the modal, including image blocks and other media contexts.

#7 @hbhalodia
4 weeks ago

Hi @jadavsanjay @wildworks @adamsilverstein @Dharm1025, Can you please try to reproduce the issue again with the patch added in the PR - https://github.com/WordPress/wordpress-develop/pull/11585

I have asked help from AI (Claude Sonnet 4.6), it was pretty much good to find root cause and explain what was the real issue. After the fix, I tried replicating the issue but was not able to and also checked for regression but there was none.

Thanks,

@ozgursar
4 weeks ago

Before patch 11585

@ozgursar
4 weeks ago

After patch 11585

#8 @ozgursar
4 weeks ago

Patch Testing Report

Patch Tested: https://github.com/WordPress/wordpress-develop/pull/11585

Environment

  • WordPress: 7.1-alpha-62161-src
  • PHP: 8.2.29
  • Server: nginx/1.29.4
  • Database: mysqli (Server: 8.4.7 / Client: mysqlnd 8.2.29)
  • Browser: Opera
  • OS: macOS
  • Theme: Twenty Twenty-Five 1.4
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.1

Steps taken

  1. Create a new post
  2. Click Set featured image
  3. Go to Upload files and upload multiple files
  4. Observe incorrect image count (e.g. Showing 7 of 8 media items)
  5. Apply the patch
  6. Repeat steps 2-4
  7. Observe correct image count (e.g. Showing 7 of 7 media items)
  8. ✅ Patch is solving the problem

Expected result

  • While uploading we should see the correct number of images

Screenshots/Screencast with results

Before patch
https://core.trac.wordpress.org/raw-attachment/ticket/65053/65053-before-PR-11585.png

After patch
https://core.trac.wordpress.org/raw-attachment/ticket/65053/65053-after-PR-11585.png

Last edited 4 weeks ago by ozgursar (previous) (diff)

#9 @wildworks
2 weeks ago

#65149 was marked as a duplicate.

@adamsilverstein commented on PR #11585:


2 weeks ago
#10

I confirmed the bug, applied the patch on a branch, and added a QUnit test at the model layer that asserts Attachments#observe(selection) does not corrupt the mirrored query's totalAttachments (and that mirrored-query add/remove still drives the total as a regression guard). It fails on trunk with Actual: 9, Expected: 8 and passes once the bindings move into mirror(). Branch: https://github.com/adamsilverstein/wordpress-develop/tree/trac-65053-attachments-count — feel free to fold the test into this PR.

#11 @adamsilverstein
2 weeks ago

@hbhalodia - nice work.

The fix in https://github.com/WordPress/wordpress-develop/pull/11585 looks good! I added some tests in https://github.com/WordPress/wordpress-develop/compare/trunk...adamsilverstein:wordpress-develop:trac-65053-attachments-count if you want to add them to your pr. They fail before the pr changes.

#12 @adamsilverstein
2 weeks ago

  • Keywords needs-unit-tests has-test-info added

#13 @adamsilverstein
2 weeks ago

  • Milestone changed from Awaiting Review to 7.1
  • Version changed from trunk to 6.3

#14 @adamsilverstein
2 weeks ago

  • Owner set to adamsilverstein
  • Status changed from new to assigned

@hbhalodia commented on PR #11585:


12 days ago
#15

Hi @adamsilverstein, I have added the test case as you mentioned in the branch.

Thanks,

@adamsilverstein commented on PR #11585:


5 days ago
#16

Hi @adamsilverstein[[Image(chrome-extension://hgomfjikakokcbkjlfgodhklifiplmpg/images/wp-logo.png)]], I have added the test case as you mentioned in the branch.

Thanks,

Thanks @hbhalodia!

#17 @adamsilverstein
5 days ago

  • Keywords has-unit-tests commit added; needs-unit-tests removed

This fix looks ready to me, I will get it committed for 7.1.

#18 @hbhalodia
4 days ago

Thanks @adamsilverstein

Note: See TracTickets for help on using tickets.