Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions assets/js/src/integration/wsforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,8 @@
const $ = window.jQuery;

$( document ).on(
'wsf-submit-success wsf-save-success',
function (
event,
formObject,
formId,
formInstanceId,
formEl,
formCanvasEl
) {
'wsf-submit-success',
function ( event, formObject, formId, formInstanceId, formEl ) {
// All the magic happens here.
window.PUM.integrations.formSubmission( $( formEl ), {
formProvider,
Expand Down
17 changes: 17 additions & 0 deletions classes/Integration/Form/Forminator.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ public function get_form_selectlist() {
* @param array $field_data_array Field data array.
*/
public function on_success( $entry, $form_id, $field_data_array ) {
if ( ! is_object( $entry ) && ! is_array( $entry ) ) {
return;
}

$status = is_object( $entry ) && isset( $entry->status ) ? $entry->status : ( is_array( $entry ) && isset( $entry['status'] ) ? $entry['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.

P2 Badge Reject malformed Forminator entry values

When another plugin or a Forminator signature change invokes this hook with null or a scalar entry, both status checks fall through to the legacy path, where spam defaults to false and the draft ID to null, so the callback dispatches a submission and may run actions or record a conversion for a value that cannot represent a successful entry. Continue accepting statusless legacy objects, but return unless $entry is an object or supported array before applying the legacy fallback.

AGENTS.md reference: AGENTS.md:L428-L432

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 773a94f. The callback now rejects every non-object/non-array entry before applying legacy status fallback, with null and scalar regression cases. Focused PHP tests and PHPCS remain clean.

if ( null !== $status && 'active' !== $status ) {
return;
Comment on lines +126 to +128

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve legacy Forminator success entries

On installations using Forminator versions whose Forminator_Form_Entry_Model predates the status property, successful entries reach this callback without that field and are now unconditionally discarded, disabling server-side submission actions and conversion tracking even though this integration declares no minimum Forminator version. Treat a missing status as the legacy entry shape—while checking legacy spam data where available—and reject only explicit non-success states; the new test currently codifies the regression by expecting a statusless object to be ignored.

AGENTS.md reference: AGENTS.md:L428-L432

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a23f680. Official WordPress.org Forminator SVN tags confirm the status property is absent through 1.44.3 and present by 1.48.3. Statusless legacy entries now pass only when is_spam is false and draft_id is empty; explicit non-active statuses remain rejected. Regression coverage now includes legacy success, legacy spam/draft, and current active/failure shapes.

}

if ( null === $status ) {
$is_spam = is_object( $entry ) && isset( $entry->is_spam ) ? $entry->is_spam : ( is_array( $entry ) && isset( $entry['is_spam'] ) ? $entry['is_spam'] : false );
$draft_id = is_object( $entry ) && isset( $entry->draft_id ) ? $entry->draft_id : ( is_array( $entry ) && isset( $entry['draft_id'] ) ? $entry['draft_id'] : null );
if ( $is_spam || ! empty( $draft_id ) ) {
return;
}
}

if ( ! $this->should_process_submission() ) {
return;
}
Expand Down
12 changes: 12 additions & 0 deletions classes/Integration/Form/WSForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ public function get_form_selectlist() {
* @param \WS_Form_Submit $submit
*/
public function on_success( $submit ) {
if ( ! is_object( $submit )
|| ! isset( $submit->post_mode )
|| 'submit' !== $submit->post_mode
|| ! empty( $submit->error )
|| ! empty( $submit->error_validation_actions )
|| ! isset( $submit->form_id )
|| ! is_numeric( $submit->form_id )
|| $submit->form_id <= 0
) {
return;
}

if ( ! $this->should_process_submission() ) {
return;
}
Expand Down
55 changes: 55 additions & 0 deletions tests/php/tests/FormSubmissionPhases_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,61 @@ public function get_form( $id ) {
$this->assertNull( PUM_Integrations::$form_submission );
}

/**
* Forminator rejects explicit failures and legacy spam/draft entries.
*/
public function test_forminator_requires_active_or_legacy_success_entry() {
$observed = 0;
$this->observation_action = static function () use ( &$observed ) {
++$observed;
};
add_action( 'pum_integrated_form_submission', $this->observation_action );

$integration = new PUM_Integration_Form_Forminator();
foreach ( [ 'draft', 'abandoned', 'spam' ] as $status ) {
$integration->on_success( (object) [ 'status' => $status ], 7, [] );
}
$integration->on_success( null, 7, [] );
$integration->on_success( 'invalid-entry', 7, [] );
$integration->on_success( (object) [ 'is_spam' => true ], 7, [] );
$integration->on_success( (object) [ 'draft_id' => 'draft-7' ], 7, [] );

$this->assertSame( 0, $observed );

$integration->on_success( (object) [ 'status' => 'active' ], 7, [] );
$integration->on_success( new stdClass(), 7, [] );
$this->assertSame( 2, $observed );
}

/**
* WS Form saves and failed validation never become conversions.
*/
public function test_ws_form_requires_one_valid_submit_receipt() {
$observed = 0;
$this->observation_action = static function () use ( &$observed ) {
++$observed;
};
add_action( 'pum_integrated_form_submission', $this->observation_action );

$integration = new PUM_Integration_Form_WSForms();
$valid = [
'form_id' => 7,
'post_mode' => 'submit',
'error' => false,
'error_validation_actions' => [],
];

$integration->on_success( (object) array_merge( $valid, [ 'post_mode' => 'save' ] ) );
$integration->on_success( (object) array_merge( $valid, [ 'error' => true ] ) );
$integration->on_success( (object) array_merge( $valid, [ 'error_validation_actions' => [ 'field_1' => 'Required' ] ] ) );
$integration->on_success( (object) array_merge( $valid, [ 'form_id' => 0 ] ) );

$this->assertSame( 0, $observed );

$integration->on_success( (object) $valid );
$this->assertSame( 1, $observed );
}

/**
* Non-AJAX callbacks increment each Core metric exactly once.
*
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/wsforms-integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
describe( 'WS Form success integration', () => {
let eventName;
let handler;
let formSubmission;

beforeEach( () => {
jest.resetModules();
formSubmission = jest.fn();
window.PUM = { integrations: { formSubmission } };
window.jQuery = jest.fn( () => ( {
on: jest.fn( ( registeredEvent, callback ) => {
eventName = registeredEvent;
handler = callback;
} ),
} ) );

require( '../../assets/js/src/integration/wsforms' );
} );

test( 'observes submit success and never registers save success', () => {
expect( eventName ).toBe( 'wsf-submit-success' );
expect( eventName ).not.toContain( 'wsf-save-success' );

const formElement = document.createElement( 'form' );
handler( {}, {}, 7, 'instance-1', formElement, {} );

expect( formSubmission ).toHaveBeenCalledTimes( 1 );
expect( formSubmission ).toHaveBeenCalledWith( expect.anything(), {
formProvider: 'wsforms',
formId: 7,
formInstanceId: 'instance-1',
} );
} );
} );
Loading