Skip to content

fix(media-protection): invalidate authorization cache when media is protected#3194

Open
faisalahammad wants to merge 1 commit into
gocodebox:devfrom
faisalahammad:fix/3167-media-protection-object-cache
Open

fix(media-protection): invalidate authorization cache when media is protected#3194
faisalahammad wants to merge 1 commit into
gocodebox:devfrom
faisalahammad:fix/3167-media-protection-object-cache

Conversation

@faisalahammad

@faisalahammad faisalahammad commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Media protection URL rewriting was broken when a persistent object cache (like Object Cache Pro) is active. The authorization result for an attachment is cached per (media_id, user_id) with wp_cache_add(), so once an unprotected attachment is viewed its 'null' sentinel stays in cache and never gets overwritten when the file is later protected. This fix switches the writes to wp_cache_set() and explicitly invalidates the current user's cache whenever a media file is moved into the protected directory.

Fixes #3167

Changes

includes/class-llms-media-protector.php

Before:

wp_cache_add( $cache_key, 'null', 'llms_media_authorization', $cache_expiration );
...
wp_cache_add( $cache_key, $authorization, 'llms_media_authorization', $cache_expiration );

After:

wp_cache_set( $cache_key, 'null', 'llms_media_authorization', $cache_expiration );
...
wp_cache_set( $cache_key, $authorization, 'llms_media_authorization', $cache_expiration );

Why: wp_cache_add() only writes when the key is missing. With a persistent object cache the 'null' sentinel from the first unprotected view survives across requests, so is_authorized_to_view() keeps returning the old value after the file is protected. wp_cache_set() overwrites the cache with the current state.

Also added invalidate_authorization_cache() and hooked it into add_authorization_meta_to_media_post() so the cache is cleared when the protection meta is added.

includes/admin/class-llms-admin-media-protection-attachment-settings.php

After: after a successful move to the protected directory, call $protector->invalidate_authorization_cache( $attachment_id ).

Why: clears the current user's cached authorization immediately so the next wp_get_attachment_url() call returns the protected URL.

includes/class-llms-rest-fields.php

After: in the REST update_callback for _llms_media_protection_product_id, after a successful move call $protector->invalidate_authorization_cache( $object->ID ).

Why: the block-editor protection modal uses the REST endpoint, so the classic-admin path alone did not cover it.

tests/phpunit/unit-tests/class-llms-test-media-protector.php

New tests covering:

  • invalidate_authorization_cache() deletes the cache for the supplied user and defaults to the current user.
  • add_authorization_meta_to_media_post() clears the cached authorization.
  • is_authorized_to_view() overwrites a stale 'null' cache entry.

.changelogs/3167.yml

Patch-level changelog entry for the fix.

Testing

Test 1: Protect an unprotected image with Object Cache Pro active

  1. Upload an image to the media library.
  2. View the image URL. It should be the normal /wp-content/uploads/... URL.
  3. Open the attachment, select a course/membership in "LifterLMS Media Protection", and save.
  4. Refresh the attachment. URL is now https://example.com/?llms_media_id={ID}.

Result: Works as expected.

Test 2: Re-protect an already protected media file

  1. With Object Cache Pro active, protect an attachment.
  2. Re-open it and change the selected course/membership.
  3. The new product is honored on the next load.

Result: Works as expected.

Test 3: Without object cache

  1. Repeat Test 1 after dropping the object-cache.php drop-in.

Result: Same behavior, no regression.

Automated tests

composer tests-run -- --filter LLMS_Test_Media_Protector

Result: OK (4 tests, 10 assertions).

…rotected

- Switch wp_cache_add to wp_cache_set in is_authorized_to_view() to overwrite
  stale sentinel values from persistent object caches like Object Cache Pro.
- Add invalidate_authorization_cache() helper and call it from
  add_authorization_meta_to_media_post() to clear the current user's cache
  immediately when protection state changes.
- Call invalidation from classic editor save (move_attachment_to_protected_dir)
  and REST API update callback for belt-and-suspenders.
- Ensure 'null' sentinel cache uses the same expiration filter as boolean results.

Fixes gocodebox#3167
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Awaiting Review

Development

Successfully merging this pull request may close these issues.

2 participants