Skip to content

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

Merged
brianhogg merged 2 commits into
gocodebox:devfrom
faisalahammad:fix/3167-media-protection-object-cache
Jul 23, 2026
Merged

fix(media-protection): invalidate authorization cache when media is protected#3194
brianhogg merged 2 commits 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
@faisalahammad
faisalahammad requested a review from brianhogg as a code owner June 18, 2026 08:34
@brianhogg brianhogg moved this to Awaiting Review in Development Jun 18, 2026
@brianhogg brianhogg moved this from Awaiting Review to Review in Progress in Development Jul 16, 2026
Comment thread includes/class-llms-media-protector.php Outdated
Comment thread tests/phpunit/unit-tests/class-llms-test-media-protector.php Outdated
Comment thread tests/phpunit/unit-tests/class-llms-test-media-protector.php Outdated
@github-project-automation github-project-automation Bot moved this from Review in Progress to To do in Development Jul 21, 2026
- Swap @SInCE 10.0.6 to @SInCE [version] on new methods
- Remove @SInCE lines from test method/property docblocks
- Keep file-header @SInCE [version] in new test file

Addresses PR review feedback on gocodebox#3194.

@faisalahammad faisalahammad left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed all 3 review comments:

  • Switched @SInCE 10.0.6 to @SInCE [version] on new methods
  • Switched test file header to @SInCE [version]
  • Removed @SInCE lines from all test method/property docblocks

Ready for re-review.

@brianhogg brianhogg modified the milestones: 10.2, 10.1 Jul 21, 2026
@brianhogg
brianhogg merged commit b4822cd into gocodebox:dev Jul 23, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from To do to Done in Development Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants