Skip to content

Add a mark-entry-spam ability - #3601

Merged
Crabcyborg merged 5 commits into
masterfrom
fix/issue-6863-mark-entry-spam
Oct 21, 2026
Merged

Crabcyborg merged 5 commits into
masterfrom
fix/issue-6863-mark-entry-spam

Conversation

@vivi-the-going-merry

@vivi-the-going-merry vivi-the-going-merry Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes Strategy11/formidable-pro#6863

Broken: no ability could mark an entry as spam. Lite had list-entries, get-entry and delete-entry only.

Changed

  • New ability formidable-forms/mark-entry-spam. Input: id (entry ID or item_key). It calls FrmSpamEntriesHelper::mark_as_spam(), the same method the Entries screen uses, so the entry is flagged as manual spam and its child entries move with it.
  • Returns the entry in the get-entry shape, with is_draft 4.
  • Permission: FrmSpamEntriesController::current_user_can_moderate() (frm_delete_entries), or an administrator, like delete-entry.
  • Annotations: not read-only, not destructive, idempotent. An entry that is already spam returns success, as reloading the Entries screen does.
  • A draft, or a site where spam can't be stored, returns a frm_entry_not_markable 400 error. A missing entry returns the usual 404.

Verified: Lite's local PHPUnit can't run here, so CI is the red/green. Test-only commit: all 4 PHPUnit jobs failed (The mark-entry-spam ability should be registered). With the fix, all 4 pass, plus PHPCS, PHPStan, Psalm, Mago, Rector and PHP CS Fixer. Tests cover the result and get-entry match, item_key lookup, repeat calls, draft and unknown IDs, the permission check, and the contract list.

Not checked: that the entry then shows in list-entries with is_draft: 4. That needs #3599. Not run through a live MCP client.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: Strategy11/formidable-forms/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6e6e1a62-3e0b-4292-b7a6-dbbf7bb029b3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@deepsource-io

deepsource-io Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 1927ae3...700aa12 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 ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
PHP Oct 21, 2026 3:44a.m. Review ↗
JavaScript Oct 21, 2026 3:44a.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.

$entry = $this->factory->entry->get_object_by_id( $entry_id );

$marked = $this->execute( 'mark-entry-spam', array( 'id' => $entry->item_key ) );
$this->assertNotWPError( $marked, 'An item_key should resolve like an ID.' );

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_FrmAbilitiesEntriesController::assertNotWPError()


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


$marked = $this->execute( 'mark-entry-spam', array( 'id' => $entry->item_key ) );
$this->assertNotWPError( $marked, 'An item_key should resolve like an ID.' );
$this->assertSame( FrmSpamEntriesHelper::SPAM_ENTRY_STATUS, $marked['is_draft'] );

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_FrmAbilitiesEntriesController::assertSame()


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

$marked = $this->execute( 'mark-entry-spam', array( 'id' => $entry->item_key ) );
$this->assertNotWPError( $marked, 'An item_key should resolve like an ID.' );
$this->assertSame( FrmSpamEntriesHelper::SPAM_ENTRY_STATUS, $marked['is_draft'] );
$this->assertSame( $entry_id, $marked['id'] );

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_FrmAbilitiesEntriesController::assertSame()


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

$this->assertNotWPError( $marked, 'An item_key should resolve like an ID.' );
$this->assertSame( FrmSpamEntriesHelper::SPAM_ENTRY_STATUS, $marked['is_draft'] );
$this->assertSame( $entry_id, $marked['id'] );
$this->assertSame( $marked, $this->execute( 'get-entry', array( 'id' => $entry_id ) ), 'The response should match get-entry.' );

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_FrmAbilitiesEntriesController::assertSame()


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

$this->assertSame( $marked, $this->execute( 'get-entry', array( 'id' => $entry_id ) ), 'The response should match get-entry.' );

$stored = FrmEntry::getOne( $entry_id );
$this->assertTrue( FrmSpamEntriesHelper::is_manual_spam( $stored ), 'It should be recorded as marked by hand, like the Entries screen does.' );

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_FrmAbilitiesEntriesController::assertTrue()


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


$again = $this->execute( 'mark-entry-spam', array( 'id' => $entry_id ) );
$this->assertNotWPError( $again );
$this->assertSame( FrmSpamEntriesHelper::SPAM_ENTRY_STATUS, $again['is_draft'] );

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_FrmAbilitiesEntriesController::assertSame()


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

);

$draft = $this->execute( 'mark-entry-spam', array( 'id' => $draft_id ) );
$this->assertWPError( $draft, 'Only a submitted entry can be marked as spam.' );

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_FrmAbilitiesEntriesController::assertWPError()


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


$draft = $this->execute( 'mark-entry-spam', array( 'id' => $draft_id ) );
$this->assertWPError( $draft, 'Only a submitted entry can be marked as spam.' );
$this->assertSame( 1, (int) FrmEntry::getOne( $draft_id )->is_draft, 'A refused draft should keep its status.' );

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_FrmAbilitiesEntriesController::assertSame()


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

$this->assertSame( 1, (int) FrmEntry::getOne( $draft_id )->is_draft, 'A refused draft should keep its status.' );

$missing = $this->execute( 'mark-entry-spam', array( 'id' => 0 ) );
$this->assertWPError( $missing );

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_FrmAbilitiesEntriesController::assertWPError()


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


$missing = $this->execute( 'mark-entry-spam', array( 'id' => 0 ) );
$this->assertWPError( $missing );
$this->assertSame( 404, $missing->get_error_data()['status'] ?? null );

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_FrmAbilitiesEntriesController::assertSame()


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

Moves a submitted entry to spam through FrmSpamEntriesHelper::mark_as_spam(),
gated on the same capability as the Entries screen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vivi-the-going-merry
vivi-the-going-merry Bot marked this pull request as ready for review October 21, 2026 02:58
@franky-the-going-merry franky-the-going-merry Bot added the franky-working Franky is actively reviewing this label Oct 21, 2026

@franky-the-going-merry franky-the-going-merry 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.

The ability works for normal entries, but it can mark a child entry as spam on its own. That one needs a fix before this merges.

Checked: I ran the PR's tests (FrmAbilitiesEntriesController and FrmAbilitiesContract, plus the MCP tests) on PHP 8.2 with Lite only: 35 pass. A probe confirmed: mark by item_key, result matches get-entry, is_draft 4, recorded as manual spam, repeat call succeeds, a draft returns 400, unknown id returns 404, subscriber is denied, parent plus child move together, and frm_entry_marked_spam fires once.

Blocking

  • A child entry (repeater or embedded form row) can be marked as spam by itself. See the inline comment.

Not blocking

  • execute_mark_entry_spam() treats any mark_as_spam() failure as a 400 "only a submitted entry" error. If another request marks the same entry first, this call fails even though the entry is spam. The Entries screen avoids this with mark_as_spam( $id ) || is_spam( getOne( $id ) ) (FrmSpamEntriesController::mark_spam()). Re-read the entry after a failure and return success if it is now spam.
  • The draft test passes the message as the second argument of assertWPError(), which does not check the message. Assert get_error_code() is frm_entry_not_markable and the status is 400. No test covers the parent-and-child move or the can_store_spam() false branch.

Not run: a live MCP client, Pro or Views loaded (Pro's own spam support), the full Lite suite.

Comment on lines +407 to +409
if ( ! $entry ) {
return self::get_invalid_entry_error();
}

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.

A child entry (a row where parent_item_id is not 0, from a repeater or embedded form) is accepted here. mark_as_spam() then sets only that row to is_draft 4. The parent stays submitted. I ran this: parent marked, child follows (good); child marked alone, parent stays is_draft 0 and the child becomes spam. The Entries screen only offers "Mark as spam" on top-level entries, and the PR body says child entries move with their parent, so this breaks that rule. The Spam tab does not list child rows, so the caller has no way to undo it.

Fix: refuse a child entry before marking it.

Suggested change
if ( ! $entry ) {
return self::get_invalid_entry_error();
}
if ( ! $entry ) {
return self::get_invalid_entry_error();
}
// A repeater or embedded form row moves to spam with its parent, never on its own.
if ( (int) $entry->parent_item_id > 0 ) {
return new WP_Error( 'frm_entry_not_markable', __( 'A child entry cannot be marked as spam on its own. Mark its parent entry instead.', 'formidable' ), array( 'status' => 400 ) );
}

Also add a test that marks a child entry and expects frm_entry_not_markable, and that the parent keeps is_draft 0.

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.

Fixed in 194ac54 and 700aa12: a child entry now gets a 400 frm_entry_not_markable. A failed mark re-reads the entry after clearing the cache and returns success if it is now spam. Tests added for the child refusal, the parent moving its child, and the concurrent mark; the draft test asserts the error code and status. All PHPUnit jobs and PHPCS pass. The can_store_spam() false branch is not tested (needs a separate-process fixture), and neither a live MCP client nor Pro/Views was run.

@franky-the-going-merry franky-the-going-merry Bot added vivi-pickup and removed franky-review franky-working Franky is actively reviewing this labels Oct 21, 2026
@vivi-the-going-merry vivi-the-going-merry Bot added vivi-working Vivi is actively working this and removed vivi-pickup labels Oct 21, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

$draft = $this->execute( 'mark-entry-spam', array( 'id' => $draft_id ) );
$this->assertWPError( $draft );
$this->assertSame( 'frm_entry_not_markable', $draft->get_error_code() );

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_FrmAbilitiesEntriesController::assertSame()


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

$draft = $this->execute( 'mark-entry-spam', array( 'id' => $draft_id ) );
$this->assertWPError( $draft );
$this->assertSame( 'frm_entry_not_markable', $draft->get_error_code() );
$this->assertSame( 400, $draft->get_error_data()['status'] ?? null );

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_FrmAbilitiesEntriesController::assertSame()


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

)
);

$this->assertNotWPError( $this->execute( 'mark-entry-spam', array( 'id' => $parent_id ) ) );

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_FrmAbilitiesEntriesController::assertNotWPError()


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

$this->assertNotWPError( $this->execute( 'mark-entry-spam', array( 'id' => $parent_id ) ) );

FrmEntry::clear_cache();
$this->assertTrue( FrmSpamEntriesHelper::is_spam( FrmEntry::getOne( $child_id ) ), 'A child entry should follow its parent.' );

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_FrmAbilitiesEntriesController::assertTrue()


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

);

$child = $this->execute( 'mark-entry-spam', array( 'id' => $child_id ) );
$this->assertWPError( $child );

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_FrmAbilitiesEntriesController::assertWPError()


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


$child = $this->execute( 'mark-entry-spam', array( 'id' => $child_id ) );
$this->assertWPError( $child );
$this->assertSame( 'frm_entry_not_markable', $child->get_error_code() );

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_FrmAbilitiesEntriesController::assertSame()


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

$child = $this->execute( 'mark-entry-spam', array( 'id' => $child_id ) );
$this->assertWPError( $child );
$this->assertSame( 'frm_entry_not_markable', $child->get_error_code() );
$this->assertSame( 400, $child->get_error_data()['status'] ?? null );

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_FrmAbilitiesEntriesController::assertSame()


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

FrmEntry::clear_cache();

foreach ( array( $parent_id, $child_id ) as $id ) {
$this->assertSame( 0, (int) FrmEntry::getOne( $id )->is_draft, 'A refused child should leave its parent and itself submitted.' );

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_FrmAbilitiesEntriesController::assertSame()


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

$wpdb->update( $wpdb->prefix . 'frm_items', array( 'is_draft' => FrmSpamEntriesHelper::SPAM_ENTRY_STATUS ), array( 'id' => $entry_id ) );

$marked = $this->execute( 'mark-entry-spam', array( 'id' => $entry_id ) );
$this->assertNotWPError( $marked );

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_FrmAbilitiesEntriesController::assertNotWPError()


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


$marked = $this->execute( 'mark-entry-spam', array( 'id' => $entry_id ) );
$this->assertNotWPError( $marked );
$this->assertSame( FrmSpamEntriesHelper::SPAM_ENTRY_STATUS, $marked['is_draft'] );

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_FrmAbilitiesEntriesController::assertSame()


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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vivi-the-going-merry vivi-the-going-merry Bot added franky-review and removed vivi-working Vivi is actively working this labels Oct 21, 2026
@vivi-the-going-merry

Copy link
Copy Markdown
Contributor Author

Method: in-place push
Pushed to: #3601 (branch fix/issue-6863-mark-entry-spam, unchanged PR number)

@franky-the-going-merry franky-the-going-merry Bot added the franky-working Franky is actively reviewing this label Oct 21, 2026

@franky-the-going-merry franky-the-going-merry 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.

Approve at 700aa12. This is a re-review of the whole PR; what changed since my last pass at 5b7b278 is the controller guard and three tests.

Blocking item from last time: fixed. execute_mark_entry_spam() now returns a frm_entry_not_markable 400 when parent_item_id > 0, so a repeater or embedded-form row can no longer go to spam on its own. The parent still takes its children with it.

Non-blocking items from last time: also fixed.

  • After a failed mark_as_spam() it clears the cache, re-reads the entry and returns success if it is now spam. A racing request no longer turns into a false 400.
  • The draft test now asserts the error code and the 400 status.
  • New tests cover the child refusal, the parent-moves-its-children case and the race.

Verified by running it:

  • FrmAbilitiesEntriesController, FrmAbilitiesContract and the MCP tests: 38 pass on PHP 8.4, Lite only.
  • Mutation check: the old controller (5b7b278) fails the child test and the race test. Removing only the child guard fails the child test. Removing only the re-read fails the race test.
  • The can_store_spam() false branch has no test in the PR, so I probed it: with a FrmProAppHelper stub that lacks SPAM_ENTRIES_SUPPORTED, the call returns frm_entry_not_markable / "Spam entries cannot be stored while an active add-on does not support them." / status 400, and the entry stays is_draft 0.

Not run: a live MCP client, real Pro or Views loaded, the full Lite suite. DeepSource PHP is red only on PHPUnit assert methods it can't resolve; the other checks are green.

@franky-the-going-merry franky-the-going-merry Bot removed franky-review franky-working Franky is actively reviewing this labels Oct 21, 2026

@Crabcyborg Crabcyborg 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.

🚀

@Crabcyborg
Crabcyborg merged commit b0f278c into master Oct 21, 2026
49 of 50 checks passed
@Crabcyborg
Crabcyborg deleted the fix/issue-6863-mark-entry-spam branch October 21, 2026 12:13
@Crabcyborg Crabcyborg added this to the 7.1 milestone Oct 21, 2026
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.

1 participant