Skip to content

Fix #1236: keep the companion files WP 7.1 records on an attachment - #1241

Merged
Miraeld merged 1 commit into
developfrom
fix/1236-preserve-companion-metadata
Aug 18, 2026
Merged

Fix #1236: keep the companion files WP 7.1 records on an attachment#1241
Miraeld merged 1 commit into
developfrom
fix/1236-preserve-companion-metadata

Conversation

@Miraeld

@Miraeld Miraeld commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #1236

WordPress 7.1 can keep the file an upload started from next to the one it serves: the HEIC a photo was uploaded as, beside the JPEG the site delivers, or the original GIF beside the video it was turned into. The file name lives in the attachment metadata and nowhere else.

Regenerating thumbnails replaced that metadata wholesale, and wp_generate_attachment_metadata() never produces those keys, so restoring a media dropped them. wp_delete_attachment_files() reads them to clean up, so the files became invisible to WordPress and stayed on disk for good, even once the attachment was deleted.

Nothing is visible to the user either way, which is exactly why it needed fixing: the only symptom is disk usage that grows and is never reclaimed on sites where people upload from phones.

Type of change

  • New feature (non-breaking change which adds functionality).
  • Bug fix (non-breaking change which fixes an issue).
  • Enhancement (non-breaking change which improves an existing functionality).
  • Breaking change (fix or feature that would cause existing functionality to not work as before).
  • Sub-task of #(issue number)
  • Chore
  • Release

Detailed scenario

What was tested

Everything below was done by hand on a real WordPress 7.1-RC3-63235 build, in Chrome 151, with Imagify connected and backup on. A small mu-plugin was used during testing to read the attachment metadata back and to invoke the restore. It is not part of this PR.

Reproduced the bug first, from a real HEIC upload. On an unpatched build, uploaded a HEIC file through the block editor. The browser converted it to JPEG and WordPress kept the HEIC beside it, recording source_image: test-source.heic in the metadata. Clicked "Restore Original" in the Media Library. The key was gone, and the 184KB HEIC was still sitting in the uploads folder with nothing referencing it. That is the bug: the file can no longer be found, so it will never be cleaned up.

Verified the fix, again from a real HEIC upload. On this branch, uploaded a HEIC through the block editor. The browser converted it and sideloaded the source file, and WordPress wrote source_image: iphone-shot-1.heic through its own finalize endpoint. Then ran a restore on that media:

Before After
source_image after restore gone iphone-shot-1.heic
Restore completed yes yes
Thumbnails regenerated yes yes, 4 sizes

The restore genuinely ran in both cases, confirmed by the thumbnails being rebuilt from zero to four sizes and the optimization data being cleared.

One honest limitation, worth knowing for QA. On this machine the browser side pipeline never reaches its final step for HEIC files. All eight sideload requests return 200 and the converted JPEG and its sub sizes land on disk, but the finalize request is never sent, so the metadata is left empty. It happens the same way whether the file is dropped or picked through the file input, and it does not happen for JPEG or PNG uploads. Nothing in this PR affects it, and it happens on an unpatched build too.

To get past it, the finalize request the browser should have sent was issued by hand against the real REST endpoint, with the source file the browser had genuinely sideloaded. WordPress then wrote source_image through its own code. The endpoint rejects a file name that was not really sideloaded for that attachment, so this could not have been faked. Everything after that point, the restore and the metadata write, is the untouched plugin code path.

Also checked the three keys together. WordPress 7.1 records source_image, animated_video and animated_video_poster the same way, and none of them are produced by wp_generate_attachment_metadata(). Seeded all three on an optimized media exactly as finalize_item() stores them, ran a restore from the Media Library link, and confirmed all three survived while the sizes were correctly rebuilt.

Automated checks on top of the manual work: 5 new unit tests (composer test-unit -- --group MediaWP), full suite green at 495, composer run-stan clean, PHPCS clean on both changed files.

How to test

Setup:

  1. Install WordPress 7.1 and serve it over HTTPS, or on localhost. Client side processing needs a secure context, and it is enabled by default there in any browser.
  2. Activate Imagify on this branch and connect it with a valid API key.
  3. Go to Settings, Imagify. Turn "Auto optimize images on upload" on and turn "Backup original images" on. Save.
  4. Get a HEIC file. Any photo straight off an iPhone works. On a Mac you can also make one with sips -s format heic photo.jpg --out photo.heic.

Upload it:

  1. Create a new post: Posts, Add New.
  2. Insert an Image block, by typing /image and pressing Enter.
  3. Upload the HEIC through that block. It has to be the block editor, since the conversion happens in the browser.
  4. Wait for the image to appear. WordPress converts it to JPEG and keeps the HEIC beside it in the uploads folder.

Confirm the starting state:

  1. Read the metadata: wp post meta get <ID> _wp_attachment_metadata --format=json.
  2. There must be a source_image key naming the HEIC file. If it is missing, the browser did not finish the upload, so do not continue: the case under test is not set up. See the limitation noted above.
  3. Confirm that HEIC file is present in wp-content/uploads/<year>/<month>/.

Restore, which is what used to lose the key:

  1. Go to Media, Library in list view and find the media.
  2. Click "Restore Original" in its Imagify column.
  3. Read the metadata again with the same command.
  4. source_image must still be there, and must still name the HEIC file that is on disk. Losing it is the bug.

Confirm the consequence is fixed:

  1. Delete the attachment permanently, from Media, Library.
  2. The HEIC file must be gone from the uploads folder. Before this fix it was left behind, because WordPress no longer knew about it.

Check nothing regressed on ordinary media:

  1. Take any optimized JPEG, click "Restore Original" on it, and confirm the thumbnails come back and the media can be optimized again as before.

Affected Features & Quality Assurance Scope

  1. Restore Original, for WP Media Library attachments, which is the path that regenerates metadata.
  2. Anything else that reaches Media\WP::generate_thumbnails(), so re-optimizing with a different level and the missing thumbnails action.
  3. Attachment deletion, indirectly, since it is what consumes these keys.
  4. Custom folders and NextGen Gallery have their own media classes and are untouched.

Technical description

Documentation

Media\WP::generate_thumbnails() writes the return value of wp_generate_attachment_metadata() straight into _wp_attachment_metadata. That is correct for everything that function produces, but WordPress 7.1 added three keys it does not produce, written only when the browser handled the upload:

  1. source_image, the file the upload started from, kept when the browser converted it.
  2. animated_video, the video an animated GIF was turned into.
  3. animated_video_poster, the still frame for that video.

All three name real files on disk, and all three are read back by wp_delete_attachment_files() (wp-includes/post.php:6980 and :7003) as the only record of what to clean up. Confirmed against core: wp-admin/includes/image.php does not mention any of them.

keep_companion_files() now carries them from the stored metadata onto the freshly generated array, before it is written. A key the new metadata already carries is left alone, so nothing is clobbered if a future WordPress starts producing them itself.

While in there, the metadata write is guarded with is_array(). WordPress 7.1 made wp_get_attachment_metadata() return false for non array metadata and coerce a non array sizes to an empty array (wp-includes/post.php:7051 and :7113-7141), so storing a non array value now reads back as false rather than as what was written. Nothing in the plugin was known to write one, but the write was unguarded.

The helper is protected rather than private so it can be exercised directly in the unit tests.

New dependencies

None.

Risks

The change only ever adds keys that were already stored, and only when the fresh metadata does not have them, so it cannot alter sizes, file names or dimensions. On any WordPress below 7.1 the keys never exist and the helper is a no op.

Metadata already lost to an earlier restore is not recovered. The names are gone, so the files cannot be identified any more. This stops the loss from happening again rather than repairing it, and any such file has to be found on disk by hand. A migration would have to guess which stray file belonged to which attachment, which is not something worth risking.

Mandatory Checklist

Code validation

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.

Unticked items justification

Nothing unticked.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.)
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

There is nothing to log: the outcome is observable directly in _wp_attachment_metadata, where the keys either survive a restore or do not. No filesystem or HTTP calls were added, and the helper only reads two arrays.

WordPress 7.1 can keep the file an upload started from next to the one it serves,
a HEIC beside its JPEG or a GIF beside its video, and records the name in the
attachment metadata. Regenerating thumbnails replaced that metadata wholesale, and
wp_generate_attachment_metadata() never produces those keys.

Restoring a media therefore dropped them, and wp_delete_attachment_files() could no
longer find the files: they stayed on disk for good, even once the attachment was
deleted. Carry them over, and guard the metadata write against a non array.
@codacy-production

codacy-production Bot commented Aug 14, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

🟢 Coverage 73.33% diff coverage

Metric Results
Coverage variation Report missing for cd27ae41
Diff coverage 73.33% diff coverage (50.00%)

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (cd27ae4) Report Missing Report Missing Report Missing
Head commit (65851c7) 20327 1551 7.63%

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#1241) 15 11 73.33%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Miraeld Miraeld self-assigned this Aug 14, 2026
@Miraeld
Miraeld added this pull request to the merge queue Aug 18, 2026
Merged via the queue into develop with commit d8aacf2 Aug 18, 2026
13 of 14 checks passed
@Miraeld
Miraeld deleted the fix/1236-preserve-companion-metadata branch August 18, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WP 7.1: restoring a media wipes the new source_image metadata key and orphans the source file

2 participants