=== StaticQ Media ===
Contributors: goonydev, philooo
Tags: image optimization, cloud storage, webp, cdn, media offload
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 3.3.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Resize, convert to WebP, offload to Cloudflare R2, and deliver images through your CDN — all in one queue-based pipeline with zero monthly fees.

== Description ==

StaticQ Media is a complete image processing pipeline for WordPress. Upload once — StaticQ handles resizing, WebP conversion, cloud offloading, and front-end delivery in the background via WordPress cron. No external API. No per-image fees. Processing runs on your server, files live in your own cloud bucket.

**How It Works**

1. Upload — image enters the processing queue
2. WordPress cron generates all registered sizes + WebP variants
3. Processed files are uploaded to your Cloudflare R2 bucket
4. Front-end delivery rewrites `<img>` tags to `<picture>` elements with CDN URLs and WebP sources

**Processing**

* Queue-based via WordPress cron — no upload slowdowns
* Configurable batch size and time budget per cron tick
* Automatic retry on failure
* Works on shared hosting — no memory spikes, no timeouts
* Cloudflare Image Resizing support (optional)
* WebP conversion with quality control
* EXIF metadata stripping for privacy
* Big image scaling control
* Non-image media support (PDFs, documents, etc.)

**Storage**

* Cloudflare R2
* Three modes: Local only / Local + Cloud / Cloud only
* Chunked uploads for large files
* Credentials via wp-config.php constants or settings page

**Front-End Delivery**

* HTML5 `<picture>` elements with WebP `<source>` fallbacks
* Automatic srcset generation from registered image sizes
* Lazy loading with eager-first-image option
* Output buffer-based — works with any theme or page builder
* Per-class and per-size exclusions

**Media Library Scanner**

Change your image sizes, switch themes, enable WebP — your media library drifts out of sync. The Media Library Scanner realigns everything in one pass across local disk and cloud bucket.

* Metadata check — detects corrupted metadata, missing sizes, wrong dimensions, deprecated sizes, and missing WebP entries
* File check — verifies every file exists in the correct storage location and flags sync gaps between local and cloud
* One-click repair per attachment — fixes metadata first, then generates missing files, uploads, or downloads as needed
* Batch Fix All — resolve every issue across your entire library at once
* Retroactive registration — index your existing media library for processing after install
* Regenerate mode — force re-process all sizes from the original file

**Post Content Scanner**

Switched CDN providers, migrated domains, changed image sizes — your post content may still reference old URLs or outdated filenames. The Post Content Scanner inspects every post for stale image references baked into your database.

* Scans all posts, pages, and custom post types regardless of status
* Wrong domain detection — finds CDN or foreign domain URLs that should point to your local site
* Stale size references — detects image filenames that no longer match current attachment metadata
* Deleted attachment references — flags images linked to attachments that no longer exist (info only)
* Broken URL detection — identifies image URLs that cannot be resolved to any attachment (info only)
* One-click fix per post — rewrites wrong domain URLs and updates stale size references in post content
* Batch Fix All — resolve every fixable issue across all posts at once
* Table backup — back up wp_posts before applying fixes as an extra safety measure

**Orphan Detection**

Deleted posts, removed theme sizes, failed uploads — files pile up with no attachment record. The Orphan Detection tool finds them across local disk and cloud bucket.

* Scans local uploads directory and cloud bucket for files with no WordPress attachment
* Folder exclusions — skip known third-party directories to avoid false positives
* Quarantine — move orphans to an isolated folder for review before permanent deletion
* Restore from quarantine — bring files back if needed
* Export orphan list as CSV for external review

Run the Media Library Scanner first to fix metadata issues, then the Post Content Scanner to clean up stale references in your posts, then Orphan Detection to catch leftover files. Nothing is deleted without your review.

**Cloudflare Worker Deployment**

Deploy a Cloudflare Worker to serve media directly from your R2 bucket with automatic fallback to your origin server. The built-in wizard handles deployment, testing, and configuration.

**Developer Configuration**

Key wp-config.php constants for fine-tuning:

* `SQMEDIA_CRON_BATCH_SIZE` — attachments per cron tick (default: 1)
* `SQMEDIA_CRON_MAX_SECONDS` — time budget per cron tick (default: 25)
* `SQMEDIA_SCAN_BATCH_SIZE` — attachments per scanner batch (default: 50)
* `SQMEDIA_R2_ACCESS_KEY_ID` — R2 API credentials
* `SQMEDIA_R2_SECRET_ACCESS_KEY` — R2 API credentials
* `SQMEDIA_R2_BUCKET` — R2 bucket name
* `SQMEDIA_R2_ACCOUNT_ID` — Cloudflare account ID

== Installation ==

1. Install and activate from Plugins > Add New, or upload the `staticq-media` folder to `/wp-content/plugins/`.
2. Go to StaticQ > Settings.

**Cloudflare R2 setup:**

1. Create an R2 bucket in the Cloudflare dashboard.
2. Create an API token with R2 read/write permissions.
3. Add credentials to wp-config.php (recommended) or the settings page.
4. Set a custom domain or enable the R2.dev public URL.
5. Click "Test Connection."

**After storage is configured:**

1. Use Media Manager > Register to index your existing media library.
2. New uploads are registered automatically.
3. Run the Media Library Scanner to detect and repair any issues.

== Frequently Asked Questions ==

= How does the processing queue work? =

Images are not processed on upload. They enter a queue and are processed in batches by WordPress cron. Batch size and time budget are configurable. This avoids CPU spikes and timeouts, especially on shared hosting.

= Do I need a Cloudflare account? =

No — Cloudflare is optional. The plugin is optimized for Cloudflare workflows (R2 offload, Cloudflare Image Resizing, and the optional Worker fallback), but it runs fully on your own server without any cloud account. In local-only mode, StaticQ uses WordPress's native image editor (GD or Imagick) for resizing and WebP generation, files stay in your uploads directory, and the Media Library Scanner, Post Content Scanner, and Orphan Detection all work as a pure audit-and-cleanup toolkit. Cloudflare comes in when you want offsite storage, edge-based image resizing, or CDN fallback. R2's free tier (10 GB storage, 10 million reads/month) is generous for most WordPress sites if you decide to enable it.

= Will it slow down uploads? =

No. Processing is fully decoupled from the upload request. WordPress cron handles it in the background.

= Can I process my existing media library? =

Yes. The Media Manager registration tool indexes your entire existing library. Then run the Media Library Scanner to detect issues and repair them — missing sizes, WebP variants, and cloud sync gaps are all handled.

= What happens if I deactivate the plugin? =

Your original files remain untouched. Cloud copies stay in your bucket. Front-end delivery stops — WordPress falls back to local URLs. No data is deleted on deactivation.

= Does it work with page builders? =

Yes. Front-end delivery uses output buffering, so it works with any theme, page builder, or caching plugin that outputs standard HTML.

= How do I monitor processing progress? =

The Media Manager page shows queue status, processed counts, and per-attachment state. Scanner results show issue breakdowns by category.

= What does the Media Library Scanner do? =

It checks every attachment against your current settings in one pass. Metadata is validated for missing sizes, wrong dimensions, deprecated entries, and missing WebP variants. Files are verified across local disk and cloud bucket to ensure they exist in the correct storage location. Issues are categorized by type and severity. You can repair individual attachments with one click, or use Batch Fix All to resolve everything at once.

= What does the Post Content Scanner do? =

It inspects every post, page, and custom post type for image references baked into the content. It detects URLs pointing to wrong domains (e.g. old CDN or migration leftovers), stale size references that no longer match current attachment metadata, deleted attachments still referenced in content, and broken URLs that cannot be resolved. Wrong domain and stale size issues can be fixed automatically — the scanner rewrites the URLs directly in the database. Deleted attachment and broken URL issues are reported for manual review.

= How does Orphan Detection work? =

Orphan Detection lists all files in your local uploads directory and cloud bucket, then compares them against the WordPress database. Any file with no matching attachment record is flagged as an orphan. You can exclude specific folders to avoid false positives. Orphans can be quarantined for safe review before permanent deletion. Run the Media Library Scanner first to fix metadata issues, then Orphan Detection to catch leftover files.

= Does it strip EXIF data? =

Yes. EXIF metadata (GPS coordinates, camera info, timestamps) is stripped during processing. This reduces file size and removes potentially sensitive location data.

= Is StaticQ useful if I don't use cloud storage? =

Absolutely. Cloud offloading is just one part of the pipeline. The Media Library Scanner audits your entire library and lets you repair everything in one pass. Orphan Detection cleans up leftover files. The Post Content Scanner fixes stale image references in your posts. And the queue-based processing prevents CPU spikes on upload — all without moving a single file off your server.

== Screenshots ==

1. Media Library Scanner detects missing sizes, missing WebP variants, broken metadata, and storage drift across your library — with one-click repair per category.
2. Drill into any attachment to see exactly which sizes, WebP variants, and storage locations need attention.
3. Post Content Scanner finds stale image URLs baked into your posts — wrong domains, deleted attachments, broken references — and rewrites them safely.
4. Orphan Detection walks your uploads directory and bucket for files that no longer belong to any attachment in your media library. Safe scan, then review before cleaning anything up.
5. Review orphan files before touching anything. Exclude folders, quarantine the rest, restore with one click if needed.
6. Choose how your media is stored: local only, cloud only, or both. Cloudflare R2 included free — no monthly fees.
7. Configure WebP conversion, picture-tag delivery, and per-size handling. Every option works without a paid tier.
8. One-click Cloudflare Worker deployment — no Wrangler CLI, no JavaScript. The wizard generates the script, deploys it, sets up routes, binds the R2 bucket, and verifies the result.
9. Register your existing media library in one read-only pass. New uploads register automatically.

== External services ==

This plugin connects to the following third-party services when configured by the user. No data is sent to any external service unless the user explicitly enables and configures the corresponding feature.

= Cloudflare R2 (Object Storage) =

When the user configures Cloudflare R2 as their storage provider, the plugin uploads processed media files (resized images and WebP variants) to the user's own Cloudflare R2 bucket. Files are uploaded, downloaded, and deleted via the S3-compatible API.

* Data sent: media files (images), bucket credentials (API token, account ID)
* When: during cron-based queue processing, scanner repair operations, and orphan quarantine/restore
* Service provider: Cloudflare, Inc.
* [Terms of Service](https://www.cloudflare.com/terms/)
* [Privacy Policy](https://www.cloudflare.com/privacypolicy/)

= Cloudflare Image Resizing (optional) =

When enabled, the plugin sends original images to Cloudflare's Image Resizing API to generate resized and WebP variants. This requires the user's domain to be proxied through Cloudflare. The resized images are stored locally or in the user's bucket — they are not hosted by Cloudflare.

* Data sent: image files via Cloudflare-proxied URL
* When: during cron-based queue processing, when Cloudflare Image Resizing is enabled in settings
* Service provider: Cloudflare, Inc.
* [Terms of Service](https://www.cloudflare.com/terms/)
* [Privacy Policy](https://www.cloudflare.com/privacypolicy/)

= Cloudflare Workers API (optional) =

When the user deploys a Cloudflare Worker fallback via the plugin's setup wizard, the plugin communicates with the Cloudflare API to create and configure the Worker script on the user's own Cloudflare account.

* Data sent: Worker script source code, Cloudflare API token, account ID, zone ID
* When: only during the one-time Worker deployment wizard
* Service provider: Cloudflare, Inc.
* [Terms of Service](https://www.cloudflare.com/terms/)
* [Privacy Policy](https://www.cloudflare.com/privacypolicy/)

== Changelog ==

= 3.3.4 =
* Fixed a post_content corruption bug in the `content_save_pre` filter (`revert_cdn_urls_in_content`). In Worker mode (the default), when the CDN host equals the WordPress site host, the previous logic computed a CDN prefix without the `/wp-content/uploads/` path segment and then ran `str_replace(cdn_prefix, local_prefix, $content)`, which prepended `/wp-content/uploads/` to every internal URL in the post on save. This doubled the path on media URLs (`/wp-content/uploads/wp-content/uploads/...`) and injected the path on non-media internal links (`/lm002/` became `/wp-content/uploads/lm002/`).
* Fixed a related URL-corruption bug in the output-time `encode_upload_url_paths` rewriter. The same too-short prefix computation also misled its foreign-domain detection and stripped the `/wp-content/uploads/` segment when rewriting legacy/foreign CDN URLs to the current CDN host. Visible on sites with legacy-domain URLs in content (e.g. `static.example.com → www.example.com`) or already-rewritten URLs from a separate CDN host.
* Both functions now route through a shared `compute_current_cdn_prefix()` helper that correctly returns `{cdn_base}/{r2_prefix}/` in Direct mode and `{cdn_base}/wp-content/uploads/` (host-swapped if needed) in Worker mode. When the CDN URL prefix equals the local prefix, the filters short-circuit.
* The original bug was introduced by the 2026-04-29 refactor that wired `force_prefix_in_url` through the URL rewriter and made `get_url_prefix()` return empty in Worker mode; the two affected functions then started producing wrong results because they relied on `get_url_prefix()` to fill in the URL's path segment, which only worked in Direct mode. Sites in Direct mode (`force_prefix_in_url = true`) were never affected.

= 3.3.3 =
* Applied late output escaping at every echo site in the scanner and post-content-scanner admin UI, replacing earlier "pre-escaped helper returns HTML" patterns with helpers that return data and inline `esc_attr()` / `esc_html()` at the echo site (WP.org review hardening).
* Refactored `render_pagination_buttons()` to print directly with per-attribute escaping instead of building an HTML string and echoing it whole.
* Hardened inline JSON data islands (`<script type="application/json">`) with `JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT` flags.
* Fixed Media Library Scanner "All" tab count: it now shows the number of distinct attachments with issues (matching the rendered table), rather than the sum of per-category counts (which double-counted attachments matching multiple categories).
* Cron pipeline now detects silent size- and WebP-generation failures (transient image-editor or Cloudflare errors that previously left records marked `completed` despite producing nothing) and converts them to a real failure status, allowing the existing retry logic to recover the attachment on subsequent cron ticks.
* Registration no longer guesses `original_location` from the current Original File Handling setting; it leaves the field NULL until verified. This prevents the Orphan Scanner from flagging legacy originals as orphans on sites where the setting was changed at some point — the scanner's NULL branch indexes both possible locations until the Media Library Scanner aligns the record to observed state.
* AJAX nonces on the Media Manager page are now refreshed continuously via the WordPress Heartbeat API. A tab left open beyond the 24-hour nonce-expiry window no longer silently fails on actions; the nonce stays valid for the life of the user's login session. A safety-net handler also surfaces a clear "session expired, please reload" message if the refresh ever fails (e.g., browser-throttled background tab).

= 3.3.2 =
* Fixed `Move to /originals/` original file handling: the unscaled original is now actually moved to `wp-content/uploads/originals/<dir>/` locally, mirroring the bucket placement. Previously only the bucket copy moved while the local copy stayed alongside the master.
* Fixed Media Library Scanner contradictory report on original-image rows: the location column now shows presence at the intended folder only (no longer reports a stale legacy copy as "present"), and the misplaced badge label was clarified to "Original: stale copy in old folder."
* Fixed scanner double-emit of "Original missing" + "Original misplaced" badges on the same file when the original was at a legacy location.
* Fixed `Undefined array key "status"` PHP warning during quarantine restore.
* Suppressed harmless `Table doesn't exist` errors logged when the WordPress Plugin Check tool runs the plugin under its alternate `wp_pc_` table prefix.
* Removed verbose per-file scanner diagnostics from `debug.log` (kept once-per-scan summaries).
* Quieted per-image cron pipeline debug logs by default; opt back in via `define('SQMEDIA_DEBUG_VERBOSE', true);` in `wp-config.php` when investigating cron behavior.

= 3.3.1 =
* Renamed remaining `STATICQ_*` wp-config.php constants to the `SQMEDIA_*` prefix for project-wide naming consistency (`SQMEDIA_R2_ACCOUNT_ID`, `SQMEDIA_R2_ACCESS_KEY_ID`, `SQMEDIA_R2_SECRET_ACCESS_KEY`, `SQMEDIA_R2_BUCKET`, `SQMEDIA_R2_PREFIX`, `SQMEDIA_CF_TOKEN`, `SQMEDIA_SKIP_DIRS`). Update wp-config.php on upgrade.
* Escaped CDN URLs injected into `<img>` `src` and `srcset` via the `the_content` filter and the frontend output buffer with `esc_url()`, hardening rendered output.
* Refactored frontend output buffer to a plain `ob_start()` / `ob_get_clean()` pair on `template_redirect` / `shutdown` (replacing the callback-based buffer) for clearer pairing and added a buffer-level guard so foreign nested buffers cannot trigger our close.
* Made the JS-template `ob_start()` / `ob_get_clean()` pairing in the scanner UI renderer trivially auditable by capturing the buffer to a local variable before passing it to `wp_add_inline_script()`.
* Included the plugin's top-level `composer.json` in the WordPress.org distribution zip.

= 3.3.0 =
* Renamed internal identifiers from `sq_` to `sqmedia_` to meet WordPress.org prefix uniqueness requirements. Pre-3.3 dev-build installs: settings reset on upgrade — reconfigure R2 credentials and re-run the Cloudflare Worker wizard.
* Removed Google Cloud Storage support from the free plugin; R2 is now the only storage provider.
* Hardened JSON input sanitization in orphan quarantine AJAX handlers.
* Excluded vendor binaries and build scaffolding from the distribution zip.

= 3.2.0 =
* Added Post Content Scanner — detects wrong domain URLs, stale size references, deleted attachments, and broken URLs in post content.
* Cloudflare R2 is now the default storage provider.
* All features fully available — no restrictions.
* Added table backup option before applying scanner repairs.

= 3.1.0 =
* Added Cloudflare Worker deployment wizard.
* Improved `<picture>` tag delivery with format fallbacks.
* Added Media Library Scanner with disk and bucket cross-referencing.
* Added orphan file detection and cleanup.
* Removed debug logging for cleaner production output.

= 3.0.0 =
* Introduced queue-based processing architecture.
* Added Cloudflare R2 offloading.
* Added WebP conversion.
* Added Media Library Scanner.

== Upgrade Notice ==

= 3.3.4 =
Critical fix: prevents post_content corruption on save when StaticQ runs in Worker mode (the default) with the CDN host equal to the WordPress site host. Upgrade immediately if you've been on 3.3.0 – 3.3.3 with Worker-mode delivery on the same host.

= 3.3.3 =
Security hardening: late output escaping applied to admin UI render paths per WordPress.org review guidelines.

= 3.3.2 =
Bug fixes for the `Move to /originals/` mode (now also moves locally), Media Library Scanner badge accuracy, and quieter `debug.log` output.

= 3.3.1 =
Renames the remaining `STATICQ_*` wp-config.php constants to the `SQMEDIA_*` prefix. If you set R2 credentials or the Cloudflare token via wp-config.php, update those constant names after upgrade.

= 3.3.0 =
Upgrading from a pre-3.3 dev build: settings will be reset. Reconfigure storage credentials and re-run the Worker wizard after upgrade.

= 3.2.0 =
Post Content Scanner, all features unlocked, table backup for scanner repairs.

= 3.1.0 =
Media Library Scanner, orphan detection, and Cloudflare Worker deployment.

= 3.0.0 =
New queue-based architecture. Existing media can be registered for reprocessing.
