=== XYZ Protect ===
Contributors: chaosunlimitedllc
Tags: content protection, media protection, memberpress, paid memberships pro, age verification, cloudflare
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.0.4
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Protect your images, videos, PDFs, and audio from unauthorized access. XYZ Protect rewrites media URLs through a Cloudflare Worker that verifies authorization before serving any file. Unauthorized visitors see a placeholder — your real content stays hidden. Works with MemberPress, Paid Memberships Pro, WordPress login, and age verification. No server configuration required.

== Description ==

Protect your images, videos, PDFs, and audio from unauthorized access. XYZ Protect rewrites media URLs through a Cloudflare Worker that verifies authorization before serving any file. Unauthorized visitors see a placeholder — your real content stays hidden. Works with MemberPress, Paid Memberships Pro, WordPress login, and age verification. No server configuration required.

**How It Works**

1. Protected media URLs are rewritten to use your dedicated media hostname on XYZ’s network.
2. Requests are handled at the edge (via Cloudflare) for speed and worldwide presence.
3. Each request is checked for authorization before the file is served.
4. Unauthorized requests receive a placeholder image or an empty response.

**Two Protection Modes**

* **Guard Cookie** — Simple, fast, page-cache compatible. Media URLs use a domain swap with obfuscated paths. A cookie proves the visitor is authorized.
* **Encrypted URL** — Maximum security. Each media URL is AES-256-GCM encrypted, unique per user, and time-limited. URLs cannot be shared or bookmarked.

**Tiered Mode** allows both modes simultaneously — standard members get Guard Cookie protection while premium members get Encrypted URLs.

**Authorization Sources**

XYZ Protect is authorization-model agnostic. It ships with built-in support for:

* **MemberPress** — When MemberPress is installed, it takes priority. Map membership levels to protection tiers. Supports path-based and MemberPress Rules–based media protection strategies.
* **Paid Memberships Pro** — When PMPro is installed and MemberPress is not, PMPro is used instead. Map membership levels to tiers; supports path-based and PMPro “rules” style strategies similar to MemberPress.
* **WordPress Login** — If neither membership plugin is active, any logged-in user is authorized. The tier follows the site’s global protection mode (Guard, Encrypted, or Tiered as configured).

Additional authorization modules can be added by implementing the `XYZ_Authorization_Module` interface.

**Module priority:** MemberPress is chosen first if available, then Paid Memberships Pro, otherwise WordPress Login.

**Age Verification**

Built-in age verification gating redirects visitors from regulated regions to an identity verification page before they can view content. Supports region-specific rules, test mode, and both cookie-based and account-based verification persistence.

**Key Features**

* Works with all media types: images, videos, PDFs, documents, audio
* Responsive image `srcset` URLs are rewritten automatically
* Path obfuscation prevents guessing original file locations
* Exempt specific directories from protection (logos, public assets)
* Automatic key rotation for encrypted URLs
* Built-in self-test to verify the protected media service is responding as expected
* MU-plugin option for compatibility with image optimization plugins
* WP-Cron automatic sync of protection settings to XYZ’s service (twice daily by default)
* Optional hotlink blocking (Referer check) for cross-site direct links
* Optional edge caching for Guard Cookie images (Cloudflare), with admin flush-by-site
* Trial mode with metered usage for evaluation

== Installation ==

= Prerequisites =

1. An **XYZ API key** (start a free trial from the plugin, or use a key from your account manager).
2. **DNS control** for your site’s domain so you can add the CNAME and TXT records the plugin shows (usually at your domain registrar or DNS host).
3. **XYZ provisions** your media hostname and secure edge configuration through **Cloudflare’s network**. You do **not** need your own Cloudflare account or any manual setup of edge infrastructure for normal use.

= Plugin Installation =

1. Upload the `xyz-protect` folder to `/wp-content/plugins/`.
2. Activate the plugin through the **Plugins** menu.
3. Navigate to **Settings > XYZ Protect**.

= Initial Setup =

**Step 1: API Connection**

New users can start a free trial directly from the plugin. Enter your email and site URL on the Connection & Status tab. You will receive an API key by email.

If you already have an API key, click "Already have an API key?" and enter your API URL and key manually.

To use a specific API server (development or staging), add this to `wp-config.php`:

`define( 'XYZ_PROTECT_API_URL', 'https://your-api-server.example.com' );`

**Step 2: Content Protection**

After connecting, switch to the **Content Protection** tab and click **Provision Media Hostname**. You will need to provide:

* **Origin hostname** — The server where WordPress serves your media files (usually your site's domain).
* **Media hostname** — A subdomain visitors will use to load protected media (e.g., `media.example.com`).

The plugin will display DNS records you need to create at your domain registrar:

* A **CNAME** record pointing your media hostname to the protection network
* One or two **TXT** records for hostname ownership and SSL certificate verification

DNS propagation typically takes 1–5 minutes. The plugin checks automatically every 30 seconds and activates protection once verified.

**Step 3: Configure Protection Settings**

Once active, configure your preferences:

* **Protection Mode** — Guard Cookie, Encrypted URL, or Tiered
* **Protection Scope** — Protect everything in `/wp-content/uploads/`, or only specific paths
* **Enforcement Mode** — Enforce (block unauthorized access) or Test Mode (evaluate rules without blocking)
* **Exempt Paths** — Directories that should remain publicly accessible
* **Placeholder Image** — Custom image shown to unauthorized visitors (optional)

Click **Save Settings** to apply your configuration to XYZ’s protection service.

== MemberPress Integration ==

XYZ Protect detects MemberPress automatically. When MemberPress is active, a **MemberPress Integration** section appears on the Content Protection tab.

= Media Protection Strategy =

Choose how media protection interacts with MemberPress content rules:

* **Path-based** — Media in protected paths is always rewritten regardless of which page it appears on. The protection tier varies by the visitor's membership level. This is simpler and more secure.
* **MemberPress Rules** — Media is only rewritten on pages that MemberPress rules protect. Public pages serve original media URLs. This respects all MemberPress rule types including single post, category, tag, and archive rules.

If the same media file appears on both a protected and a public page, use Path-based strategy to ensure complete protection.

= Membership Tier Mapping =

Assign a protection tier to each MemberPress membership level:

* **None** — No media protection for that membership
* **Guard Cookie** — Domain-swap URLs, compatible with page caching
* **Encrypted URL** — AES-GCM encrypted, per-user, time-limited

Members with multiple active memberships receive the highest tier. Users with the **manage_options** capability (typically site administrators) are treated as having the highest configured tier for testing, without needing an active membership.

When any membership is mapped to a tier, the plugin automatically sets the API protection mode to **Tiered**.

If MemberPress is deactivated, the plugin falls back to Paid Memberships Pro (if active) or WordPress Login. MemberPress settings are preserved and restore automatically when MemberPress is reinstalled.

== Paid Memberships Pro integration ==

When Paid Memberships Pro is active **and MemberPress is not**, the **Paid Memberships Pro Integration** section appears on the Content Protection tab. Behavior mirrors MemberPress: choose **Path-based** vs **PMPro Rules** strategy, map each membership level to **None**, **Guard Cookie**, or **Encrypted URL**, and save. Users with **manage_options** receive the highest configured tier for testing.

== Age Verification ==

The **Age Verification** tab provides region-based age gating for sites that serve age-restricted content.

= Setup =

1. Enable age verification and choose the scope (entire site or specific paths).
2. Click **Fetch from API** to retrieve your cookie signing key.
3. Save settings — the plugin creates an age gate page automatically.
4. Add region rules for countries and states that require verification.

= Region Rules =

Define rules for specific countries and states. Each rule specifies:

* **Action** — Verify, Allow, or Block
* **Minimum Age** — The required age threshold (non-18 thresholds require Tier 2 / ID verification)
* **Tier** — Tier 1 (face-based) or Tier 2 (full ID document verification)

Trial and prepaid accounts can manage up to 30 regions directly in the plugin. SaaS accounts manage regions through the XYZ admin dashboard.

= Test Mode =

Enable test mode to override visitor regions by appending `?reg=US-TX` (or any region code) to any page URL. This allows testing region-specific rules without needing traffic from those regions.

= Cloudflare Regional Headers =

Region matching uses the **`CF-IPCountry`** and **`CF-Region-Code`** request headers (see Cloudflare IP Geolocation and optional Transform Rules). The Age Verification tab shows a warning if they are missing.

If the **country** header is not present, automatic region evaluation does not work reliably; use **Test Mode** with `?reg=US-TX` (or similar) to simulate regions, and/or enable Cloudflare IP Geolocation / a Transform Rule so visitors receive the headers. If **country** is present but **region** is missing, country-level rules still work; state-level rules need a rule that supplies `CF-Region-Code`.

== Advanced ==

= MU-Plugin =

The optional MU-plugin ensures URL rewriting runs after all other plugins (image optimizers, lazy loading, CDN rewriters). Enable it from the Content Protection tab under **Advanced > MU-Plugin**.

The MU-plugin is installed to `wp-content/mu-plugins/xyz-protect-buffer.php`. If the `mu-plugins` directory does not exist, you may need to create it manually on your server.

The MU-plugin also handles early age verification redirects before page caching layers.

= Cookie Domain =

By default, the plugin auto-detects the cookie domain from your site URL. If your media subdomain is on a different parent domain, set the cookie domain manually on the Connection & Status tab (e.g., `.example.com`).

= Protection service sync =

The plugin schedules a **WP-Cron** job (`twicedaily`, roughly every 12 hours) to sync protection settings with XYZ’s service. When content protection is active, the Content Protection tab shows sync status; use **Sync Now** next to Auto Sync if the status shows **Stale** or after remote settings change.

= Content Organization =

For maximum protection, organize your media into directories that match your protection scope:

* `/wp-content/uploads/protected/` — Members-only content
* `/wp-content/uploads/public/` — Logos, thumbnails, public images

Use a file manager plugin, FTP, or SSH to create and organize directories. The Content Protection tab includes guidance on migrating existing uploads if needed.

= Self-Test =

The self-test checks that unauthorized requests to your media hostname are blocked as expected. When **content protection is enabled**, run it from the **Content Protection** tab, in the **Authorization Module** section (same area as Active Module / Self-Test), after completing DNS setup—not under the separate **Advanced** MU-plugin section.

== Frequently Asked Questions ==

= Does this work with page caching? =

Yes. Guard Cookie mode is fully compatible with page caching plugins like WP Rocket, W3 Total Cache, and LiteSpeed Cache. Media URLs are rewritten during the output buffer phase, so cached pages contain the protected URLs.

Encrypted URL mode generates per-user URLs, so full-page caching will serve the wrong URLs to other visitors. Use Guard Cookie mode or Tiered mode if you rely on page caching for logged-in users.

= What happens when credits run out? =

For trial and prepaid accounts, when media protection credits are exhausted, the plugin stops rewriting URLs. Visitors see original (unprotected) media URLs. An upgrade notice appears on the Connection & Status tab.

Age verification credits are a one-time allocation. When exhausted, the age gate behavior depends on your Fail Behavior setting (fail open or fail closed).

= Can I protect media on specific pages only? =

Yes. With **MemberPress**, use the **MemberPress Rules** strategy. With **Paid Memberships Pro**, use the **PMPro Rules** strategy. In both cases, media is only rewritten on pages gated by that membership plugin; public pages keep original media URLs.

Without a membership plugin, use **Protection Scope > Protect Specific Paths** to limit protection to certain directories.

= What file types are protected? =

URLs in page HTML are rewritten when they point at known media extensions under your protection scope (images, video, audio, PDFs, common web fonts, etc.). The protected media service then enforces access for those requests. Arbitrary extensions not recognized by the rewriter may still be served from your normal origin URLs until you add patterns or use a path scope that includes them.

= Does the media subdomain affect SEO? =

Protected media is served from your media subdomain (e.g., `media.example.com`). Search engines will not be able to access protected media without authorization cookies, which prevents indexing of protected content. Public/exempt media remains on your primary domain and is unaffected.

= What happens if the media network or the API is unavailable? =

If **XYZ’s media hostname** or edge service is unreachable, browsers may fail to load assets whose URLs already point at the media host. The plugin does **not** run a continuous live health check before rewriting HTML; rewriting is skipped when protection is off, credits are exhausted, the license has lapsed, or cached config lacks a media host.

If the **XYZ API** is unreachable, the plugin keeps using its **last cached configuration** where possible so existing behavior continues until the cache expires or is refreshed.

= Is the plugin compatible with all MemberPress editions? =

Yes. XYZ Protect uses only core MemberPress APIs available on all editions (Launch, Growth, and Scale). No add-ons are required.

== Changelog ==

= 1.0.4 =
* **Edge cache (optional):** New setting under Content Protection — cache authorized **Guard Cookie** image responses at Cloudflare (15-minute edge TTL) to reduce origin load on repeat views. Does not apply to Encrypted URL traffic. Requires current XYZ API and Worker; uses per-site cache tags so you can **Flush edge cache** from the same screen after replacing an image at the same URL.
* Response header **`X-XYZ-Protect-Edge-Cache`** (when edge caching is enabled) reflects Cloudflare subrequest cache status (e.g. HIT/MISS) for debugging.
* Fix: **Edge cache** and **Referer check** toggles are forwarded from the REST settings handler to the XYZ API (admin JS sent them but PHP did not include them in `PUT /settings`).

= 1.0.3 =
* Security: REST guard now also blocks anonymous access to the Paid Memberships Pro Downloads CPT Core route prefix `/wp/v2/pmpro_download` by default (sites often expose titles, slugs, and post meta including original filenames via `GET` without authentication). Media routes remain gated by `upload_files`; other blocked prefixes use `edit_posts`. Filter `xyz_protect_blocked_rest_route_prefixes` to add or remove prefixes; `xyz_protect_allow_rest_route` to override per request (legacy `xyz_protect_allow_wp_rest_media_access` still applies to `/wp/v2/media`).

= 1.0.2 =
* Security: When content protection is enabled, anonymous requests to WordPress Core REST media routes (`/wp/v2/media`, including single attachments) are blocked with a 401 response. Default WordPress behavior exposed media library metadata (including direct file URLs) to unauthenticated clients, which undermined URL-based protection. Users who can upload files (typically administrators, editors, and authors using the block editor or Media Library) retain access.
* Developer filter: `xyz_protect_allow_wp_rest_media_access` — return `true` to allow a request, `false` to deny, or `null` to use the default capability check (`upload_files`).

= 1.0.0 =
* Initial release
* Guard Cookie and Encrypted URL protection modes
* Tiered mode for mixed protection levels
* MemberPress integration with membership-to-tier mapping
* Path-based and MemberPress Rules-based media protection strategies
* Age verification with region-based rules
* Trial onboarding with metered usage
* MU-plugin for enhanced compatibility
* Automatic protection settings sync via WP-Cron
* Self-test for protected media connectivity

= 1.0.1 =
* Clarified admin field labels and documentation
* Documentation (this readme): corrected prerequisites, module priority (MemberPress / PMPro / WordPress Login), Self-Test location, sync behavior, and FAQ items that did not match the plugin; softened internal infrastructure wording for clarity

