| 开发者 | ltracy |
|---|---|
| 更新时间 | 2026年8月31日 22:53 |
| 捐献地址: | 去捐款 |
| PHP版本: | 7.4 及以上 |
| WordPress版本: | 7.1 |
| 版权: | GPLv2 or later |
| 版权网址: | 版权信息 |
_thumbnail_id) and the attachment's own post parent_ct_builder_shortcodes, _ct_builder_json, ct_style_sheets, ct_components_classes)wp-image-N classes, so a block that carries an ID and no URL still counts-scaled version and the preserved original. Inserting an image at Medium embeds the medium URL and never the full-size one, so checking only the full-size URL is how a live image ends up on an unused list.
URLs are also matched in their JSON-escaped form (\/ instead of /), which is how Elementor, Divi and Gutenberg block attributes store them.
What it cannot see
Media Mage reads the database. It does not read your code. It will not find:
wp-content/uploads/2026/01/hero.jpg straight out of a template file, that image is reported as unused. Review the list before deleting. There is a wpmj_is_referenced filter for protecting files programmatically - see the FAQ.
One more thing worth knowing, because it works the other way: an attachment whose post_parent points at a post that still exists counts as referenced. Most images uploaded through the post editor have one, so on a long-lived site the unused list will be shorter than you expect. That is deliberate - it errs toward keeping files - but it is why a scan can come back nearly empty on a site you were certain was full of clutter.
How it avoids breaking your site
wpmj_is_referenced and wpmj_replace_query, for sites that need the last wordmedia-mage folder to /wp-content/plugins/, or install the zip through Plugins > Add New > Upload Plugin.It is built not to, and the design assumes it will be run on a site nobody has a recent backup of.
Deleting unused media moves files to the trash rather than removing them, and every file is re-checked against the whole reference search immediately before it is touched. Resolving a duplicate rewrites every reference to the keeper - full-size URL, every size variant, the -scaled and original siblings, and the JSON-escaped forms - before the duplicate is removed.
Take a backup anyway. Any tool that can delete media can delete the wrong media.
References that live in code rather than in the database. Theme PHP, plugin PHP, child-theme templates, and anything served through a CDN URL that does not match what is stored. Media Mage cannot see those and does not pretend to. Read the unused list before acting on it.
Comfortably up to a couple of thousand attachments in the browser. Past that, use WP-CLI. The honest version: finding duplicates is cheap and scales linearly. Finding unused media is not. Every attachment is checked against post content, post meta, term/user/comment meta, options and comments, and those are substring searches that no database index can help with. The cost of each check grows with the size of the library, so the total grows faster than the number of files does. Measured on a test fixture (2 KB images, 3,000 posts, 70% of the library referenced), on a developer machine rather than production hardware:
wp media-mage scan from the command line, where there is no request timeout, and then use wp media-mage unused and wp media-mage delete --dry-run to review before acting. Making the unused scan fast on very large libraries is the main thing on the list for the next version.
Add it to the ignore list. Ignored files stay in the media library and stop appearing in the unused results.
For anything you want handled in code, the wpmj_is_referenced filter runs last, after every built-in check has come up empty, and receives the attachment ID and every URL path that was checked:
add_filter( 'wpmj_is_referenced', function ( $referenced, $att_id, $paths ) { if ( in_array( $att_id, [ 42, 108 ], true ) ) { return true; } return $referenced; }, 10, 3 );
Yes, and this is why the plugin exists. Oxygen stores layouts as base64-encoded post meta and options. Read those values as plain text and no image reference in them is visible, so an Oxygen site looks like it has hundreds of unused images. Media Mage decodes that data and searches inside it, matching on full paths rather than bare filenames so logo.png does not match site-logo.png.
Yes. All four store their data in post content or post meta, where the URL search reaches, including the JSON-escaped slashes that block attributes and JSON meta use. Gutenberg blocks that carry an attachment ID and no URL are matched on the ID and on the wp-image-N class.
They count as referenced. Trash is restorable, and deleting the images out from under a trashed post breaks the restore.
No. Resolving rewrites the references to the keeper and then removes the duplicate for good. The site keeps rendering, because every reference now points at the keeper, but the file is gone. Deleting unused media is the reversible one, up until the trash is emptied.
The hash phase runs 50 attachments per request, the reference phase 25, and hashes are cached against file mtime so a second scan skips the hashing work for anything that has not changed. On a test fixture of 500 attachments across 38 posts, a full first scan takes roughly 30 to 40 seconds. Larger libraries scale roughly linearly.
Yes. Every operation the admin screen performs is available under wp media-mage:
wp media-mage scan wp media-mage duplicates --format=json wp media-mage unused wp media-mage where <id> wp media-mage resolve --dry-run wp media-mage delete --dry-run wp media-mage delete --permanent --yes wp media-mage export --file=report.csv wp media-mage ignore add <id>... wp media-mage ignore list wp media-mage trash list wp media-mage trash restore <id>... wp media-mage trash empty --yes
resolve and delete both take --dry-run, which reports exactly what would happen and changes nothing. Use it first.
No. This plugin is free, GPLv2, and intentionally has no premium tier. If it's useful to you, a coffee is a kind way to say thanks.
The scan runs per site. Uninstall cleans up its data on every site in the network.
define() calls, so each constant was defined as itself. PHP 8 throws on the undefined constant and activation dies. If you have a 0.3.0 copy that white-screens or refuses to activate, this is why. Replace it with this version.-scaled / original siblings. It previously checked only the full-size URL, which is the URL a real site is least likely to have embedded. Images inserted at Thumbnail, Medium or Large were reported unused and deleted while still on the page.wp-image-N classes, the site logo, the site icon, term meta, user meta, comment meta and comment content. None of those were checked before.resolve_duplicate now validates its IDs against the stored scan results, and requires the keeper and the duplicates to belong to the same scanned group. It force-deletes attachments and runs a site-wide search and replace, so it must not act on IDs it has not just verified.s:N: length prefixes. Rewriting the outer layer only produced rows that passed validation and were permanently unreadable, so the owning plugin's get_option() returned false.__PHP_Incomplete_Class over user data, and __wakeup() can mutate on the round trip. Any rewrite that does not read back is discarded._wp_attached_file can never be re-pointed at the keeper's file just before wp_delete_attachment() deletes what that meta names.logo.png was matching site-logo.png, and that count decides which copy an automatic resolve keeps.LIMIT/OFFSET, which was stepping over live attachments whenever anything deleted rows mid-scan.-1, which is not JSON and reached the user as a generic "Invalid response from server". It now returns a real message and a bad_nonce code.$_POST reads go through wp_unslash() and a sanitizer. The scan phase is validated against a whitelist.wp_json_encode(). It was using esc_js(), which HTML-encodes quotes that are never decoded inside a script block, and the nonce was echoed raw.wp_cache_flush(). Emptying the entire object cache on every resolve is a site-wide performance event; only the affected posts and option keys are cleared now.set_time_limit() is guarded. It is in disable_functions on many shared hosts, and the warning lands inside the AJAX response and breaks the JSON parse.defined() guarded for the same reason.Scanning….SELECT 1 ... LIMIT 1 instead of COUNT(*), so they stop at the first hit.inherit, without which it disappears from the media library.admin-post so the browser gets a real download.scan, duplicates, unused, resolve, delete, export, where, ignore and trash. resolve and delete both take --dry-run.wpmj_is_referenced filter - the last word on whether an attachment counts as referenced, for references the plugin cannot see.wpmj_replace_query filter - control over which rows a rewrite is allowed to touch.languages/media-mage.pot.R:) sent the replacement walk into infinite recursion and exhausted memory. It fired part-way through resolving a duplicate, after post content had been rewritten and before the duplicate was removed, leaving the site half-migrated with no error. Back-references are now refused alongside objects, and the walk has a depth cap.wp media-mage trash restore accepted any post ID and forced its status to inherit, which is meaningful only for attachments. A trashed page restored this way vanished from the admin list, from every query and from the front end. It now checks the post type, the trash status, and that Media Mage was what trashed it.wp media-mage trash empty permanently deleted trashed media that Media Mage never trashed, including files a user had trashed by hand. Same three checks now apply.wp_nonce_url() HTML-escapes the query separator, which survived into the link as literal text, so the nonce arrived under the wrong parameter name and the download failed as an expired link.wp-image-217 also matched wp-image-21708 and an image could be reported as referenced because of a different one. Patterns are now anchored.reference_count column in the exported CSV was always zero, including for duplicates, where that number is the entire basis for choosing which copy to keep._ct_builder_shortcodes, _ct_builder_json, ct_style_sheets and ct_components_classes before checking for image references.