Skip to content

Fix gated content and custom post type capabilities - #3211

Open
truongwp wants to merge 17 commits into
masterfrom
fix-gated-content-and-cpt
Open

Fix gated content and custom post type capabilities#3211
truongwp wants to merge 17 commits into
masterfrom
fix-gated-content-and-cpt

Conversation

@truongwp

@truongwp truongwp commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fix #3203 and https://secure.helpscout.net/conversation/3398946279/255863

Summary by CodeRabbit

  • Bug Fixes
    • Improved private gated-content unlocking to respect content-type-specific “read private” permissions instead of a single hardcoded capability.
    • Prevented incorrect 404 behavior for taxonomy archive pages and for private items not referenced by gated-content actions.
    • Ensured gated private items are properly restricted (including 404 when no valid token exists).
    • Refined gated-content item selection to only surface private and password-protected content where applicable.
  • Tests
    • Added/expanded PHPUnit coverage for gated-content access edge cases and for correct post selection/grouping behavior.

@deepsource-io

deepsource-io Bot commented Jul 27, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 9f5d2e2...3ea3602 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
PHP Jul 29, 2026 9:15p.m. Review ↗
JavaScript Jul 29, 2026 9:15p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

* @covers FrmGatedContentController::maybe_unlock_post
*/
public function test_maybe_unlock_post_skips_on_taxonomy_archive() {
$cat_id = $this->factory->category->create();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

$cat_id = $this->factory->category->create();
$this->set_front_end( get_category_link( $cat_id ) );

$this->assertFalse( is_singular(), 'Prerequisite: category archive must not be singular.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertFalse()


The method you are trying to call is not defined, which can result in a fatal error.

$this->set_front_end( get_category_link( $cat_id ) );

$this->assertFalse( is_singular(), 'Prerequisite: category archive must not be singular.' );
$this->assertFalse( is_404(), 'Prerequisite: category archive must not start as a 404.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertFalse()


The method you are trying to call is not defined, which can result in a fatal error.


FrmGatedContentController::maybe_unlock_post();

$this->assertFalse( is_404(), 'maybe_unlock_post() must not force-404 a taxonomy archive.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertFalse()


The method you are trying to call is not defined, which can result in a fatal error.

)
);

$post = $this->factory->post->create_and_get(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

);

// Subscriber has read_private_books (the CPT cap) but NOT read_private_posts.
$user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.


FrmGatedContentController::maybe_unlock_post();

$this->assertFalse(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertFalse()


The method you are trying to call is not defined, which can result in a fatal error.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

maybe_unlock_post() now checks gated-action membership and resolves each post type’s private-read capability. Gated content selection uses a direct query for enabled private or password-protected items, with tests covering access control and grouping behavior.

Changes

Gated content access handling

Layer / File(s) Summary
Access control and gated-action resolution
classes/controllers/FrmGatedContentController.php
Private-post checks use the resolved post type capability, and token/404 handling applies only to items included in published gated actions.
Gated item selection and typing
classes/models/FrmGatedContentAction.php, classes/views/frm-form-actions/*
get_posts() uses a fixed database query for enabled post types and private or password-protected statuses, while related annotations use grouped object entries.
Access behavior validation
tests/phpunit/misc/test_FrmGatedContentController.php, tests/phpunit/forms/test_FrmGatedContentAction.php
Tests cover archives, custom capabilities, gated and non-gated private posts, token behavior, visibility filtering, grouping, and disabled types.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WordPressQuery
  participant FrmGatedContentController
  participant GatedActionPosts
  participant FrmGatedTokenHelper
  WordPressQuery->>FrmGatedContentController: resolve post request
  FrmGatedContentController->>GatedActionPosts: check gated-action membership
  GatedActionPosts-->>FrmGatedContentController: matching action or no match
  FrmGatedContentController->>FrmGatedTokenHelper: validate token
  FrmGatedTokenHelper-->>FrmGatedContentController: token result
  FrmGatedContentController-->>WordPressQuery: preserve access or force 404
Loading

Possibly related PRs

Suggested reviewers: crabcyborg

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main fix for gated content and custom post type capability handling.
Linked Issues check ✅ Passed The private-read capability now comes from the post type, and authorized private CPT content avoids incorrect 404s as requested in #3203.
Out of Scope Changes check ✅ Passed The model, view, and test changes support gated-content selection and access handling; no unrelated behavior changes stand out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-gated-content-and-cpt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Crabcyborg
Crabcyborg requested a review from garretlaxton July 27, 2026 13:48
@Crabcyborg Crabcyborg added this to the 6.34 milestone Jul 27, 2026
*/
public function test_maybe_unlock_post_does_not_404_non_gated_private_post() {
// No gated content actions exist — this post is unrelated to gated content.
$post = $this->factory->post->create_and_get( array( 'post_status' => 'private' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

$post = $this->factory->post->create_and_get( array( 'post_status' => 'private' ) );

// Subscriber cannot read private posts.
wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.


FrmGatedContentController::maybe_unlock_post();

$this->assertFalse(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertFalse()


The method you are trying to call is not defined, which can result in a fatal error.

* @covers FrmGatedContentController::maybe_unlock_post
*/
public function test_maybe_unlock_post_force_404s_gated_private_post_without_token() {
$post = $this->factory->post->create_and_get( array( 'post_status' => 'private' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

);

// Subscriber cannot read private posts and has no token.
wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentController::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.


FrmGatedContentController::maybe_unlock_post();

$this->assertTrue(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentController::assertTrue()


The method you are trying to call is not defined, which can result in a fatal error.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/phpunit/misc/test_FrmGatedContentController.php (1)

140-158: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the CPT capability test reach the capability branch.

This test never registers a published gated action containing $post. At Line 158, has_gated_action_for_item() therefore returns early before $post_type_obj->cap->read_private_posts is evaluated; a regression to hard-coded read_private_posts would still pass. Add an action containing frm_gc_test_book and the created post ID before invoking the controller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/misc/test_FrmGatedContentController.php` around lines 140 -
158, Update the test setup before FrmGatedContentController::maybe_unlock_post()
to create and publish a gated action containing the frm_gc_test_book post and
its ID. Ensure has_gated_action_for_item() finds this action so execution
reaches the CPT capability check using the subscriber’s read_private_books
capability.
🧹 Nitpick comments (1)
classes/controllers/FrmGatedContentController.php (1)

163-179: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid scanning all gated actions on every singular request.

maybe_unlock_post() performs this lookup before checking whether the post is private/password-protected or whether an access code is present. Every normal singular page therefore runs the action query, and cache misses can load each published action individually. Cache the membership result or defer the lookup until gated-access handling is needed.

Also applies to: 228-242

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@classes/controllers/FrmGatedContentController.php` around lines 163 - 179,
Update maybe_unlock_post() so it checks whether the post requires gated-access
handling—private, password-protected, or carrying an access code—before calling
has_gated_action_for_item(). Avoid querying gated actions for ordinary public
posts, while preserving the existing early return for items without an active
gated action once the lookup is needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/phpunit/misc/test_FrmGatedContentController.php`:
- Around line 140-158: Update the test setup before
FrmGatedContentController::maybe_unlock_post() to create and publish a gated
action containing the frm_gc_test_book post and its ID. Ensure
has_gated_action_for_item() finds this action so execution reaches the CPT
capability check using the subscriber’s read_private_books capability.

---

Nitpick comments:
In `@classes/controllers/FrmGatedContentController.php`:
- Around line 163-179: Update maybe_unlock_post() so it checks whether the post
requires gated-access handling—private, password-protected, or carrying an
access code—before calling has_gated_action_for_item(). Avoid querying gated
actions for ordinary public posts, while preserving the existing early return
for items without an active gated action once the lookup is needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c83768b9-cb4f-4e01-83b0-7e13de936dc1

📥 Commits

Reviewing files that changed from the base of the PR and between fc97e5c and 3ea7865.

📒 Files selected for processing (2)
  • classes/controllers/FrmGatedContentController.php
  • tests/phpunit/misc/test_FrmGatedContentController.php

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.26%. Comparing base (c3d0460) to head (3ea3602).
⚠️ Report is 204 commits behind head on master.

Files with missing lines Patch % Lines
classes/controllers/FrmGatedContentController.php 44.00% 14 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3211      +/-   ##
============================================
- Coverage     26.44%   26.26%   -0.18%     
- Complexity     9237     9464     +227     
============================================
  Files           149      155       +6     
  Lines         31002    31680     +678     
============================================
+ Hits           8198     8322     +124     
- Misses        22804    23358     +554     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

* @covers FrmGatedContentAction::get_posts
*/
public function test_get_posts_excludes_plain_published_post() {
$post = $this->factory->post->create_and_get( array( 'post_status' => 'publish' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentAction::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

$grouped = FrmGatedContentAction::get_posts();

$ids = array_column( $grouped['post'] ?? array(), 'ID' );
$this->assertNotContains(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertNotContains()


The method you are trying to call is not defined, which can result in a fatal error.

* @covers FrmGatedContentAction::get_posts
*/
public function test_get_posts_includes_private_post() {
$post = $this->factory->post->create_and_get( array( 'post_status' => 'private' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentAction::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.


$grouped = FrmGatedContentAction::get_posts();

$this->assertArrayHasKey( 'post', $grouped );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertArrayHasKey()


The method you are trying to call is not defined, which can result in a fatal error.


$this->assertArrayHasKey( 'post', $grouped );
$ids = array_column( $grouped['post'], 'ID' );
$this->assertContains(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertContains()


The method you are trying to call is not defined, which can result in a fatal error.


$this->assertContains( $post->ID, $post_ids, 'Private post must appear under the "post" key.' );
$this->assertNotContains( $post->ID, $page_ids, 'Post must not bleed into the "page" bucket.' );
$this->assertContains( $page->ID, $page_ids, 'Private page must appear under the "page" key.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertContains()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertContains( $post->ID, $post_ids, 'Private post must appear under the "post" key.' );
$this->assertNotContains( $post->ID, $page_ids, 'Post must not bleed into the "page" bucket.' );
$this->assertContains( $page->ID, $page_ids, 'Private page must appear under the "page" key.' );
$this->assertNotContains( $page->ID, $post_ids, 'Page must not bleed into the "post" bucket.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertNotContains()


The method you are trying to call is not defined, which can result in a fatal error.

public function test_get_posts_omits_disabled_types() {
$grouped = FrmGatedContentAction::get_posts();

$this->assertArrayNotHasKey( 'frm_file', $grouped );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertArrayNotHasKey()


The method you are trying to call is not defined, which can result in a fatal error.

$grouped = FrmGatedContentAction::get_posts();

$this->assertArrayNotHasKey( 'frm_file', $grouped );
$this->assertArrayNotHasKey( 'frm_pdf', $grouped );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertArrayNotHasKey()


The method you are trying to call is not defined, which can result in a fatal error.


remove_all_filters( 'frm_gated_content_item_types' );

$this->assertSame( array(), $grouped, 'get_posts() must return [] when all registered types are disabled.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call to an undefined method test_FrmGatedContentAction::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@classes/models/FrmGatedContentAction.php`:
- Around line 201-208: Update the post query or filtering logic in the selector
using FrmDb::get_results so private posts are included only when the current
user has the corresponding private-read capability for each post type. Preserve
published-post inclusion and ensure unauthorized private custom-post-type
entries are excluded before autocomplete values are encoded.

In `@tests/phpunit/forms/test_FrmGatedContentAction.php`:
- Around line 115-129: Update the test around FrmGatedContentAction::get_posts()
to store the callback passed to add_filter('frm_gated_content_item_types'), then
remove only that callback with remove_filter() in a finally block so cleanup
runs even when the assertion or setup throws; do not use remove_all_filters().
- Around line 17-21: Resolve the PHPCS formatting failures in the affected test
methods: group each consecutive assignment without blank lines around the post
creation, grouped results, and ID extraction statements, and reformat the keyed
arrays near the later test setup so each value begins on its own line. Preserve
the existing test logic while applying the project’s spacing and multiline array
conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 96c80435-15c8-4118-8788-9958a978c571

📥 Commits

Reviewing files that changed from the base of the PR and between 3ea7865 and 6f3ceff.

📒 Files selected for processing (6)
  • classes/controllers/FrmGatedContentController.php
  • classes/models/FrmGatedContentAction.php
  • classes/views/frm-form-actions/_gated_content_item_row.php
  • classes/views/frm-form-actions/_gated_content_settings.php
  • tests/phpunit/forms/test_FrmGatedContentAction.php
  • tests/phpunit/misc/test_FrmGatedContentController.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/phpunit/misc/test_FrmGatedContentController.php
  • classes/controllers/FrmGatedContentController.php

Comment on lines +201 to +208
$raw_posts = FrmDb::get_results(
'posts',
array(
'post_type' => $post_types,
'post_status' => array( 'publish', 'private' ),
'orderby' => 'title',
'order' => 'ASC',
'numberposts' => -1,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
'post_type' => $post_types,
'post_status' => array( 'publish', 'private' ),
),
'ID, post_title, post_password, post_status, post_type',
array( 'order_by' => 'post_title ASC' )

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file and relevant symbols"
fd -a 'FrmGatedContentAction\.php' . || true
echo

file="$(fd 'FrmGatedContentAction\.php' . | head -n 1)"
if [ -n "${file:-}" ]; then
  echo "File: $file"
  wc -l "$file"
  echo
  ast-grep outline "$file" --view expanded || true
  echo
  echo "Relevant lines 160-230:"
  sed -n '160,230p' "$file" | nl -ba -v160
fi

echo
echo "Search for related gated content selectors and capabilities"
rg -n "FrmGatedContentAction|gated.*content|public_posts|post_types|read_post|get_results\('posts'|get_results\(\s*'\s*posts|FrmDb::get_results" -S .

Repository: Strategy11/formidable-forms

Length of output: 1105


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="./classes/models/FrmGatedContentAction.php"

echo "Relevent lines 160-230:"
sed -n '160,230p' "$file" | awk '{printf "%d\t%s\n", NR+159, $0}'

echo
echo "Relevant lines 230-310:"
sed -n '230,310p' "$file" | awk '{printf "%d\t%s\n", NR+229, $0}'

echo
echo "Call sites / related permissions query:"
rg -n "FrmGatedContentAction::get_posts|get_posts_autocomplete_source|fmbldr-gated-content|gated.*content|read_post|current_user_can" -S .

Repository: Strategy11/formidable-forms

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="./classes/models/FrmGatedContentAction.php"

echo "--- classes/models/FrmGatedContentAction.php lines 188-230 ---"
sed -n '188,230p' "$file"

echo
echo "--- classes/models/FrmGatedContentAction.php lines 1-100 ---"
sed -n '1,100p' "$file"

echo
echo "--- Search for FmrgGatedContentAction::get_posts calls / fmbldr-gated-content usage ---"
python3 - <<'PY'
import pathlib, re, json
roots = [pathlib.Path('classes/models/FrmGatedContentAction.php'), pathlib.Path('tests/phpunit/forms/test_FrmGatedContentAction.php')]
for p in roots:
    if p.exists():
        print(f'FILE {p}')
        txt = p.read_text(encoding='utf-8', errors='replace')
        for line in range(1, len(txt.splitlines())+1):
            if 'FrmGatedContentAction' in txt.splitlines()[line-1] or 'get_posts' in txt.splitlines()[line-1] or 'fmbldr-gated-content' in txt.splitlines()[line-1]:
                print(f'{line}\t{txt.splitlines()[line-1]}')
PY

echo
echo "--- JavaScript file candidates containing fmbldr-gated-content: ---"
python3 - <<'PY'
import pathlib
for p in pathlib.Path('.').rglob('*'):
    if not p.is_file():
        continue
    if '.min.' in p.name or 'node_modules' in p.parts or 'vendor' in p.parts:
        continue
    try:
        txt = p.read_text(encoding='utf-8', errors='replace')
    except Exception:
        continue
    if 'fmbldr-gated-content' in txt or 'frm_gated_content' in txt:
        print(p)
PY

Repository: Strategy11/formidable-forms

Length of output: 6410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- js/src/admin/gated-content-action.js relevant outline/snippets ---"
wc -l js/src/admin/gated-content-action.js
sed -n '1,260p' js/src/admin/gated-content-action.js | awk '{printf "%d\t%s\n", NR, $0}'

echo
echo "--- relevant controller/helper references ---"
sed -n '1,220p' classes/controllers/FrmGatedContentController.php | awk '{printf "%d\t%s\n", NR, $0}'
echo
rg -n "fmbldr-gated-content|get_posts_autocomplete_source|current_user_can|read_post|admin_post|wp_json_file|check_ajax_referrer|wp_rest_auth_cookie|rest_authentication_errors|permission_callback|access_callback" js classes -S

Repository: Strategy11/formidable-forms

Length of output: 24882


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- classes/controllers/FrmHooksController.php get posts hook registration ---"
rg -n "frm_gc_posts_source|fmbldr-gated-content|FrmGatedContentAction::get_posts|FrmGatedContentAction::get_posts_autocomplete_source|frm_form_action_fill|register_rest_route|admin_post" classes/controllers/FrmHooksController.php classes/controllers -S

echo
sed -n '1,260p' classes/controllers/FrmHooksController.php | awk '{printf "%d\t%s\n", NR, $0}'

echo
echo "--- classes/views/frm-form-actions/_gated_content_settings.php ---"
sed -n '1,120p' classes/views/frm-form-actions/_gated_content_settings.php | awk '{printf "%d\t%s\n", NR, $0}'

echo
echo "--- test get_posts private post snippet ---"
sed -n '1,90p' tests/phpunit/forms/test_FrmGatedContentAction.php | awk '{printf "%d\t%s\n", NR, $0}'

Repository: Strategy11/formidable-forms

Length of output: 20178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- classes/controller snippets around form action settings include/permission ---"
rg -n "frm_before_update_form_settings|Form action|form_action|frm_gc_settings|frm_gated_content|gated_content|_gated_content_settings|formidable-form-action|frm_actions" classes/controllers/classes/helpers -S

echo
rg -n "formidable-form-action|frm_before_update_form_settings|form_action" classes -S

echo
echo "--- likely FrmFormActionsController settings inclusion ---"
file="$(fd 'FrmFormActionsController\.php' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  echo "File: $file"
  rg -n "frm_before_update_form_settings|include|gated_content|formidable-form-action" "$file"
  idx="$(rg -n "frm_before_update_form_settings|frm_after_update_form_settings" "$file" | head -n 2 | cut -d: -f1 | tr '\n' ' ')"
  if [ -n "${idx:-}" ]; then
    first="$(echo $idx | awk '{print $1}')"
    end=$((first+180))
    echo "--- lines ${first}-${end} ---"
    sed -n "${first},${end}p" "$file" | awk '{printf "%d\t%s\n", NR+$first-1, $0}'
  fi
fi

echo
echo "--- direct file permission guard snippets containing gated_content form_action ---"
rg -n "frm_gated_content_settings|formidable-form-action|frm_form_action_fill|FrmGatedContentAction::get_posts|current_user_can\\(" classes/views classes/controllers classes/models js/formidable_admin.js -S

Repository: Strategy11/formidable-forms

Length of output: 391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- locate FrmFormActionsController.php ---"
fd 'FrmFormActionsController\.php' . || true
file="$(fd 'FrmFormActionsController\.php' . | head -n 1 || true)"

if [ -n "${file:-}" ]; then
  echo "File: $file"
  wc -l "$file"
  echo "--- form action settings hooks and gated_content includes ---"
  rg -n "frm_before_update_form_settings|frm_after_update_form_settings|include|gated_content|_gated_content_settings|formidable-form-action|frm_gated_content_settings|current_user_can|read_private" "$file"
  echo
  matches="$(rg -n "frm_before_update_form_settings|frm_after_update_form_settings" "$file" | cut -d: -f1 | tr '\n' ' ')"
  if [ -n "${matches:-}" ]; then
    first="$(echo $matches | awk '{print $1}')"
    end=$((first+220))
    echo "--- lines ${first}-${end} ---"
    sed -n "${first},${end}p" "$file" | awk '{printf "%d\t%s\n", NR+$first-1, $0}'
  fi
fi

echo
echo "--- settings render view includes ---"
rg -n "frm_before_update_form_settings|frm_action_settings|frm_after_update_form_settings|formidable-form-action|frm_gated_content_settings|FrmGatedContentAction::get_posts|current_user_can\\(" classes/views -S

Repository: Strategy11/formidable-forms

Length of output: 1884


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="./classes/controllers/FrmFormActionsController.php"

echo "--- lines 158-195 from FrmFormActionsController.php ---"
sed -n '158,195p' "$file" | awk '{printf "%d\t%s\n", NR+158-1, $0}'

echo
echo "--- lines 635-675 from FrmFormActionsController.php ---"
sed -n '635,675p' "$file" | awk '{printf "%d\t%s\n", NR+635-1, $0}'

echo
echo "--- FrmGatedContentAction::get_posts_autocomplete_source ---"
sed -n '234,260p' "./classes/models/FrmGatedContentAction.php" | awk '{printf "%d\t%s\n", NR+234-1, $0}'

echo
echo "--- read-only summary of post query/filter semantics ---"
python3 - <<'PY'
from pathlib import Path
import re
path = Path('classes/models/FrmGatedContentAction.php')
text = path.read_text(encoding='utf-8')

m_query = re.search(r"\$raw_posts\s*=\s*FrmDb::get_results\([^;]+", text, re.S)
m_filter = re.search(r"foreach\s*\(\s*\$raw_posts\s+as\s+\$post\s*\)\s*\{(?P<body>.*?)(?=\n\t\})", text, re.S)
print("query_uses_posts_table:", bool(m_query))
if m_query:
    print("query_uses_private_status:", "post_status' => array( 'publish', 'private' )" in m_query.group(0))
print("filter_matches_raw_rows:", "$raw_posts" in filter.group(0) if "filter" in dir() else False)
if m_filter:
    print("filters_private_without_capability:", "private' !== $post->post_status" in m_filter.group("body") and "current_user_can" not in m_filter.group(0))
PY

Repository: Strategy11/formidable-forms

Length of output: 4121


Filter the private post selector by read capability.

get_posts() queries /posts for both publish and private rows, then excludes only non-private public posts. This causes every private post in enabled post types to be encoded into the autocomplete selector, so users who can edit forms but cannot read a custom CPT’s private posts can select unauthorized private content. Apply the corresponding private-read capability check before including private rows in the selector.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@classes/models/FrmGatedContentAction.php` around lines 201 - 208, Update the
post query or filtering logic in the selector using FrmDb::get_results so
private posts are included only when the current user has the corresponding
private-read capability for each post type. Preserve published-post inclusion
and ensure unauthorized private custom-post-type entries are excluded before
autocomplete values are encoded.

Comment thread tests/phpunit/forms/test_FrmGatedContentAction.php Outdated
Comment on lines +115 to +129
add_filter(
'frm_gated_content_item_types',
static function () {
return array(
'frm_file' => array(
'label' => 'File',
'disabled' => true,
),
);
}
);

$grouped = FrmGatedContentAction::get_posts();

remove_all_filters( 'frm_gated_content_item_types' );

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove only the filter installed by this test.

remove_all_filters( 'frm_gated_content_item_types' ) can delete callbacks registered by other tests or extensions, causing order-dependent failures. Store the test callback and remove it with remove_filter() in a finally block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/forms/test_FrmGatedContentAction.php` around lines 115 - 129,
Update the test around FrmGatedContentAction::get_posts() to store the callback
passed to add_filter('frm_gated_content_item_types'), then remove only that
callback with remove_filter() in a finally block so cleanup runs even when the
assertion or setup throws; do not use remove_all_filters().

'post_status' => 'private',
)
);
$page = $this->factory->post->create_and_get(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Access to an undefined property test_FrmGatedContentAction::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

@garretlaxton garretlaxton left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good!

Comment thread classes/controllers/FrmGatedContentController.php Outdated
Comment thread classes/controllers/FrmGatedContentController.php Outdated

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
tests/phpunit/forms/test_FrmGatedContentAction.php (1)

119-133: 🩺 Stability & Availability | 🟡 Minor

Remove only this test’s filter callback.

remove_all_filters( 'frm_gated_content_item_types' ) can remove callbacks registered by other tests or extensions, causing order-dependent failures. Store the callback, remove it with remove_filter(), and perform cleanup in finally so exceptions do not leak the filter.

🧰 Proposed fix
+		$filter = static function () {
+			return array(
+				'frm_file' => array(
+					'label'    => 'File',
+					'disabled' => true,
+				),
+			);
+		};
 		add_filter(
 			'frm_gated_content_item_types',
-			static function () {
-				return array(
-					'frm_file' => array(
-						'label'    => 'File',
-						'disabled' => true,
-					),
-				);
-			}
+			$filter
 		);
 
-		$grouped = FrmGatedContentAction::get_posts();
-
-		remove_all_filters( 'frm_gated_content_item_types' );
+		try {
+			$grouped = FrmGatedContentAction::get_posts();
+		} finally {
+			remove_filter( 'frm_gated_content_item_types', $filter );
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/forms/test_FrmGatedContentAction.php` around lines 119 - 133,
Update the filter setup in the test around FrmGatedContentAction::get_posts() to
store the callback passed to add_filter, then remove only that callback with
remove_filter('frm_gated_content_item_types', ...). Wrap the get_posts()
execution in a finally block so the test-specific filter is always cleaned up,
including when an exception occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@tests/phpunit/forms/test_FrmGatedContentAction.php`:
- Around line 119-133: Update the filter setup in the test around
FrmGatedContentAction::get_posts() to store the callback passed to add_filter,
then remove only that callback with
remove_filter('frm_gated_content_item_types', ...). Wrap the get_posts()
execution in a finally block so the test-specific filter is always cleaned up,
including when an exception occurs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c5e47e6-9186-4f58-b776-d78c50c5eb42

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3ceff and 3ea3602.

📒 Files selected for processing (5)
  • classes/controllers/FrmGatedContentController.php
  • classes/models/FrmGatedContentAction.php
  • classes/views/frm-form-actions/_gated_content_item_row.php
  • classes/views/frm-form-actions/_gated_content_settings.php
  • tests/phpunit/forms/test_FrmGatedContentAction.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • classes/controllers/FrmGatedContentController.php
  • classes/models/FrmGatedContentAction.php

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gated Content: private custom post types with their own capability_type are forced to 404 for all non-editor users

3 participants