Changelog#
0.4.1 (2026-11-08)#
Security review of 0.4.0 (independent review, every finding then tested on local):
- High: a same-named upload was treated as a copy. WordPress keeps names like
photo.jpg.webp, so an author's upload next to an admin'sphoto.jpgcould be overwritten by copy-making and deleted with the admin's image or byoptimize --remove. BB Media now records each copy it writes (bytes and mtime, on the source file's index row) and only overwrites or deletes a copy that is recorded and unchanged; a file that belongs to an attachment is never a copy and never served as one. Test: the author's file untouched; BB Media's own copies still deleted with their image. - Medium: a crafted meta value could stall the usage scan. Stored values are unserialized once (no recursion into serialized-looking text) and walked with a budget: a 770-byte value describing 2^40 leaves now stops at 1,656 strings in under a millisecond. Same for attachment-ID fields.
- Medium: the unused report could run on an unfinished scan, which empties the index first and would list nearly
everything.
wp bb-media unused, the CSV and the screen refuse until a full scan has finished. - Low: "Used in" showed authors the titles of private posts; it now lists only posts the user can read (options, terms and theme files to administrators) and counts the rest.
- Low: same-content reuse on BB Migrate imports only picks attachments an administrator owns, so a pushed page is never bound to an author's upload.
- Hardening: ImageMagick reads for copies name the decoder from the extension (a file whose extension lies fails instead of reaching another coder); images over 50 megapixels are not copied.
- Seen on the way, not in BB Media: the AJAX Thumbnail Rebuild plugin (2.2.1, active on coalesce.io) deletes any
.webpor.aviffile named after an attachment's files when the attachment is deleted, whoever uploaded it.
0.4.0 (2026-11-08)#
Phase 4: optimization on this server, to replace Smush Pro's one job here. Everything is off until switched on in Media → Media index.
- Compression (paused while Smush compresses automatically): new image sizes are written as JPEG quality 80 with
4:2:0 chroma, through a small subclass of WordPress's Imagick editor. Most uploads on coalesce.io are 4:4:4 JPEGs, and
ImageMagick keeps the source's sampling, which is what made WordPress's sizes about a quarter heavier than Smush's.
Originals are not touched.
wp bb-media optimize --recompressremakes existing sizes from their originals. - WebP/AVIF copies (
photo.jpg.webp) of the main file and every size, made from the original through WordPress's editor (decoded once, cloned per size) at WebP quality 86, kept only when smaller; queued on upload, made in timed background batches; deleted with the files they copy;--removedeletes them all. BB Cache serves them to browsers that accept them and asks BB Media which exist (no disk checks, no day-old memory); BB Media drops BB Cache's remembered answers and purges once when a run made or removed copies. - Gate on 48 sizes (large, medium_large, medium) of 16 Smush-compressed originals, through WordPress's own upload: BB Media JPEG 4.6% smaller than Smush at the same SSIM (mean 0.9936 vs 0.9935); WebP copies 29.9% smaller at a higher SSIM (0.9939). The worst-scoring file, viewed at 2x: no visible difference. WebP made from the compressed JPEG instead of the original lost visibly more (0.9907), so that is only the fallback for sizes no longer registered.
- The status screen checks the content type the server sends for a copy (it must be image/webp).
0.3.0 (2026-11-08)#
Phase 3: the BB Migrate bridge (needs the matching BB Migrate change on both sites).
- Content, not dates. BB Migrate's uploads listings carry the SHA-1 hashes from this index, and a push sends a file exactly when its content differs. Local test sites: 852 identical files with older dates went from all sent (93 MB) to none; a one-byte change was still sent.
- Same image, other name. BB Migrate packages carry each attachment's hash; without a path match, the destination's index names an attachment with the same content (main file or unscaled original), which is reused. Local B: a pushed post's featured image was matched to B's own copy under another name; no duplicate created.
- Sizes in the background. BB Migrate's size regeneration is queued (
bb_migrate_regenerate) and made in timed batches (20 s; cron, WP-CLI or the Media index screen). For attachments with metadata only the missing sizes are made (wp_update_image_subsizes); existing sizes are never rewritten.wp bb-media regenerate [--missing] [--dry-run]also queues attachments whose indexed sizes are missing on disk. wp bb-media hash --originalshashes only what package matching needs; Media index → Hash files does it from the browser.
0.2.0 (2026-11-08)#
Phase 2: where files are used.
{prefix}bb_media_usage: one row per reference, found in post content and excerpts (uploads URLs,wp-image-N, captions, galleries, block attributes), post meta (featured image, Yoast social images, ACF image/file/gallery fields by their field type inside repeaters and flexible content, URLs in any value), options (ACF options pages, site icon, custom logo, Yoast), term meta, and the theme's files. URLs resolve through the file index to the exact file and its attachment; a size no longer in the metadata falls back to its attachment.- Kept current from post, meta, option and term changes and BB Migrate theme pushes; the first full scan runs from WP-CLI, chained cron after the file index, or the Media index screen.
- Reports, read-only: unused attachments (an attachment naming the same file as a used one counts as used), those used
only by drafts or trash, unused unattached files, and indexed files referenced but missing on disk. WP-CLI
usage,unused [--files] [--live] [--older-than],where <id|url>; a CSV download (formula-safe) on the Media index screen, "Used in" on every attachment, a "Used" column in the media list. - Local mirror: 7,462 references in 1 s. Gate: none of the 5,017 uploads files the 323 public pages render is in the unused report (two referenced files are missing from local's disk). Unused: 1,154 attachments (1,003 MB with their sizes), 209 more used only by drafts or trash, 8,846 unattached files (963 MB).
- Fixed on the way:
$wpdb->delete()treats a column namedobject_idas a number, so a delete by option name matched every option row; formats are explicit now.
0.1.0 (2026-11-08)#
Phase 1: the file index.
{prefix}bb_media_files: one row per file under uploads (attachment, size, present, width/height from the file, bytes, mtime, sha1, mime, WebP/AVIF siblings). Attachments' files come from_wp_attached_file, the metadata sizes,original_imageand image-editor backups; files no attachment owns are indexed as unattached by a scan that skips plugin working folders. When two attachments name one file, the lower ID owns it.- Kept current on upload, metadata updates, file and backup changes, Smush compressing a file, deletion (another attachment naming the same file takes it over), and BB Migrate file pushes. New files are hashed.
bb_media_lookup()for absolute, scheme-less and root-relative URLs, absolute paths and uploads-relative paths.- Backfill: WP-CLI in one go, cron batches after activation, or the Media → Media index button; resumable.
- WP-CLI: index, scan, status, lookup, verify, hash.
- Local mirror: 3,895 attachments, 42,879 files indexed in 15 s; 8,860 unattached files (about 970 MB) found in 3 s; index identical to the disk.