Skip to content

Keep media folders private to signed-in users, and apply the folder lock on every route - #2244

Merged
subodhr258 merged 4 commits into
mainfrom
fix/media-folder-lock
Oct 26, 2026
Merged

subodhr258 merged 4 commits into
mainfrom
fix/media-folder-lock

Conversation

@subodhr258

Copy link
Copy Markdown
Collaborator

What changes

Who can read media folders. /wp-json/wp/v2/media-folder (list and single) now needs a signed-in user who can write posts (edit_posts), so the gallery block's folder picker keeps working for Contributors. Creating, changing or deleting folders needs upload_files. The Media Library and block editor run signed in, so they are unaffected.

The folder lock now holds on every route, applied where the change happens instead of by matching one route path:

  • A rest_endpoints filter wraps the permission callback of every handler on the two media-folder term routes (check_media_folder_request()), so the check runs whatever the letter case, ?rest_route= or a batch request. It reads the folder id with $request->get_param( 'id' ), the way the controller does. The old route-text guard (Media_Library::enforce_locked_media_folder on rest_pre_dispatch) is removed.
  • map_meta_cap refuses assign_term for a locked folder or one inside it, which covers the core media route's media-folder field and REST uploads.
  • pre_insert_term refuses a new folder inside a locked folder.
  • rest_pre_insert_attachment refuses taking a file out of a locked folder through the core media route, with the same godam_folder_locked error as the assign-folder route.
  • add_media_library_taxonomy_on_media_upload() leaves an upload unfiled when it names a locked folder (or one inside it). The upload itself still succeeds.
  • The lock check moves to Media_Folder_Utils::is_folder_locked() so every path shares it. The rules are unchanged: a folder is locked when it or an ancestor is; bookmarking a locked folder is still allowed; Editors and Admins lock and unlock as before.

Testing

Throwaway WordPress (Playground, PHP 8.3) with the 2.5.0 release plus these files; each role signed in over HTTP with a REST nonce. Folders: open, open child, locked, and a child of the locked folder.

Check Role Result
Folder list, single folder, other letter case, ?rest_route= logged out 401, no folder data
/wp/v2/media?_embed logged out no folder names embedded
Folder list, single, search, GoDAM folder tree, upload.php Author 200
Rename, re-parent, create inside a locked folder or its child, by every spelling of the route Author 403 godam_folder_locked, nothing changed
Unlock or delete a locked folder; rename inside a batch request Author 403
Own image into or out of a locked folder through /wp/v2/media/<id> (any spelling) Author 403, file stays where it was
Edit an image's title in a locked folder Author 200, still in the folder
Upload naming a locked folder or its child (async-upload.php) Author upload succeeds, left unfiled; an open folder still files it
Open folders: rename, create, move, file images in and out, assign-folder route Author all work
Bookmark a locked folder Editor 200, still locked
Lock, unlock, rename, move Editor, Admin work as before; a locked folder can't be renamed until unlocked
Delete a locked folder / an unlocked one Admin 403 / 200
  • Contributors: the run above was made when reads still needed upload_files (Contributor got 403). Reads were then opened to edit_posts; a Contributor reading folders (allowed) and changing them (403) is covered by unit tests.
  • PHPUnit: 30 new tests in tests/php/MediaFolderLockTest.php and tests/php/MediaUploadLockedFolderTest.php; full suite OK (643 tests).
  • phpcs clean on every changed file; bin/hook-check balance and coverage checks pass.

Worth knowing

  • Subscribers and logged-out visitors no longer see folder names over REST, including in _embed on /wp/v2/media.
  • Admins and Editors can no longer file a media item into a locked folder through the core media route or a REST upload that names one. The assign-folder route already refused that.
  • A logged-out write now gets 401 rest_forbidden instead of 403 godam_folder_locked.
  • New string: "Sorry, you are not allowed to access media folders." (POT not regenerated.)
  • Still open: the classic attachment edit screen (post.php with tax_input[media-folder]) can file an Author's own image into a locked folder, because core only checks the generic assign_terms capability there. It needs a different hook and is left for a follow-up.

QA:

  1. As an Author, open the Media Library: the folder tree loads; a locked folder can't be renamed, moved, or have files dragged in or out.
  2. As an Editor or Admin, lock and unlock a folder, then rename it once unlocked.
  3. Add a GoDAM gallery block as a Contributor: the folder picker lists folders.
  4. Logged out, open /wp-json/wp/v2/media-folder: refused.

Issue: rtCamp/godam-plugin-wp#72

@subodhr258 subodhr258 self-assigned this Oct 22, 2026

@rtBot rtBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code analysis identified issues

action-phpcs-code-review has identified potential problems in this pull request during automated scanning. We recommend reviewing the issues noted and that they are resolved.

phpcs scanning turned up:

⚠️ 1 warning


Powered by rtCamp's GitHub Actions Library

Comment thread inc/classes/rest-api/class-media-library.php Outdated
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
13 0 13

⚠️ Warnings (13)

📁 readme.txt (2 warnings)
📍 Line 🔖 Check 💬 Message
0 mismatched_plugin_name Plugin name "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" is different from the name declared in plugin header "GoDAM".
0 trademarked_term The plugin name includes a restricted term. Your chosen plugin name - "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" - contains the restricted term "wordpress" which cannot be used at all in your plugin name.
📁 assets/build/css/main.css (1 warning)
📍 Line 🔖 Check 💬 Message
0 EnqueuedStylesScope This style is being loaded in all contexts.
📁 assets/src/libs/analytics.min.js (5 warnings)
📍 Line 🔖 Check 💬 Message
0 EnqueuedScriptsScope This script is being loaded in all frontend contexts.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880 (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/2026/10/26/hello-world/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/sample-page/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/demo-attachment-post/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
📁 assets/build/js/main.min.js (5 warnings)
📍 Line 🔖 Check 💬 Message
0 EnqueuedScriptsScope This script is being loaded in all frontend contexts.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880 (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/2026/10/26/hello-world/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/sample-page/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/demo-attachment-post/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

…ock on every route

Reading the media-folder terms over REST now needs a signed-in user who
can write posts (edit_posts), so the gallery block's folder picker keeps
working for Contributors. Creating, changing or deleting folders needs
upload_files. The Media Library folder tree and the block editor run
signed in, so they are unaffected.

The folder lock is applied where a folder changes rather than by matching
one route path:
- The media-folder term routes carry the check on their own permission
  callbacks, so it holds however a request reaches them (letter case,
  ?rest_route=, batch requests). It reads the folder id the way the
  controller does.
- The assign_term capability is refused for a locked folder, which covers
  the core media route's media-folder field. That route also refuses to
  take a file out of a locked folder, as the assign-folder route does.
- A new folder cannot be created inside a locked folder on any route.
- An upload that names a locked folder, or one inside it, still succeeds
  but is left unfiled.

The lock check itself moves to Media_Folder_Utils so these paths share it.
@subodhr258
subodhr258 changed the base branch from develop to main October 23, 2026 06:21
@subodhr258
subodhr258 force-pushed the fix/media-folder-lock branch from e811337 to 7fec177 Compare October 23, 2026 06:21

@rtBot rtBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code analysis identified issues

action-phpcs-code-review has identified potential problems in this pull request during automated scanning. We recommend reviewing the issues noted and that they are resolved.

phpcs scanning turned up:

⚠️ 1 warning


Powered by rtCamp's GitHub Actions Library

Comment thread inc/classes/rest-api/class-media-library.php Outdated

@rtBot rtBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code analysis identified issues

action-phpcs-code-review has identified potential problems in this pull request during automated scanning. We recommend reviewing the issues noted and that they are resolved.

phpcs scanning turned up:

⚠️ 1 warning


Powered by rtCamp's GitHub Actions Library

Comment thread inc/classes/rest-api/class-media-library.php Outdated
@subodhr258

Copy link
Copy Markdown
Collaborator Author

Tested together with the other five 2.5.1 PRs on a hosted test site connected to production GoDAM Central. Full table and all recordings: https://github.com/rtCamp/godam-plugin-wp/issues/68 (recordings are in the rtCamp Test media library; sign in to watch).

Tested commit e811337fc; the current head 18edf5aa4 rewrites the same capability check with the same behaviour.

  • Locked folder: "Add Media File" is greyed out; an upload request naming the locked folder is left unfiled and still transcodes; an upload into an unlocked folder is filed into it: T6
  • The media-folder route answers 401 to a logged-out visitor (step 1 of T6). On 2.5.0 it answered 200, and an upload naming a locked folder was filed into it.

All passed.

@elifvish elifvish left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Medium, the leak this PR closes is still open through class_list: media-folder is still a public taxonomy, so core adds media-folder-<slug> to every attachment's class_list. Logged out, GET /wp/v2/media?per_page=100 on a build of this PR returned media-folder-<slug> for both an open and a locked folder (attachment pages carry the same classes). Slugs are the folder names, and the changelog says folders are visible only to signed-in users. Suggested fix (inline): drop the taxonomy from post_class_taxonomies, or register it with public => false after checking the Media Library's query-attachments filtering.

Everything else held on the merged build: 401 for logged-out reads in any letter case, _embed returns rest_forbidden, and an Author gets 403 for rename / child-rename via /wp/v2/Media-Folder/ / body id / create-inside / move in or out of a locked folder, while title edits and open-folder changes still work.

Verification

  • Head tested: 18edf5aa45. PHPUnit on this PR's head: 643/643 OK.
  • All seven 2.5.1 PRs merged onto main (no conflicts): PHPUnit 915/915, Jest 690/690, build:prod OK.
  • Playwright API checks against that build on a local WordPress 7.1.3 site: 17/18 for this PR.
  • Not covered: Classic attachment edit screen (post.php tax_input), left for a follow-up by the PR; Subscriber role: on test-this the Subscriber role holds upload_files (site config), so Subscriber results there do not reflect stock WordPress.

Screenshots and a recording of the local run are available on request.

Comment thread inc/classes/taxonomies/class-media-folders.php
Core adds a media-folder-<slug> class for each folder an attachment is in,
and the media route's class_list and attachment pages show those classes
to logged-out visitors. Folder slugs are folder names, so leave the
taxonomy out of the post classes.
@KMchaudhary

Copy link
Copy Markdown
Collaborator

Review: Approvable ✅

Reviewed at high effort against the local WordPress core source. The fix is well-structured and the core-integration assumptions all check out.

Verified correct:

  • lock_folder_assignment is genuinely effective: core's check_assign_terms_permission() calls current_user_can('assign_term', $term_id) per term (create + update), and map_meta_cap maps assign_term — so do_not_allow really does block filing into a locked folder via /wp/v2/media.
  • The removed private is_folder_locked() has no dangling callers; all 9 sites use Media_Folder_Utils::is_folder_locked().
  • guard_media_folder_routes wraps only the media-folder base + descendant routes; siblings (media-folders-sibling), media, categories are untouched. Wrapping the permission callback (vs. path-matching) correctly covers letter-case, ?rest_route=, batch and _embed.
  • hide_folders_from_post_classes uses the real post_class_taxonomies filter with a slug-keyed array, so the unset() works.
  • enforce_locked_folder_on_media_update uses the real rest_pre_insert_attachment filter and correctly complements core's assign check (blocks leaving a locked folder; core blocks entering).
  • New edit_posts read gate: the only consumer of wp/v2/media-folder is the admin media-library bundle — no public/frontend consumer, so no regression.

Notes (non-blocking):

  • The documented follow-up — classic post.php + tax_input[media-folder] can still file an author's own image into a locked folder (core only checks the generic assign_terms cap there) — is a sensible deferral for a hotfix. Please keep it tracked.
  • Parent-lock on creation is enforced both on the REST route and via pre_insert_term; intentional redundancy, fine to keep.

Nice work — approvable as-is.

@subodhr258
subodhr258 merged commit 5d29f8f into main Oct 26, 2026
9 of 10 checks passed
@subodhr258
subodhr258 deleted the fix/media-folder-lock branch October 26, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants