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
14 changes: 10 additions & 4 deletions classes/Integration/Form/FluentForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,18 @@ public function on_success( $submission_id, $form_data, $form ) {
$form_id = $form->attributes->id;
}

$native_entry_id = is_string( $submission_id ) || is_int( $submission_id ) ? $submission_id : null;
$source_url = wp_get_raw_referer();

pum_integrated_form_submission(
[
'popup_id' => $popup_id,
'form_provider' => $this->key,
'form_id' => $form_id,
'submission_id' => is_scalar( $submission_id ) ? $submission_id : null,
'popup_id' => $popup_id,
'form_provider' => $this->key,
'form_id' => $form_id,
'submission_id' => $native_entry_id,
'native_entry_id' => $native_entry_id,
'fields' => is_array( $form_data ) ? $form_data : [],
'source_url' => $source_url ? $source_url : null,
]
);
}
Expand Down
5 changes: 4 additions & 1 deletion classes/Integrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,12 @@ public static function pum_vars( $vars = [] ) {
}

if ( ! empty( self::$form_submission ) ) {
$frontend_submission = self::$form_submission;
unset( $frontend_submission['fields'], $frontend_submission['raw_fields'], $frontend_submission['native_entry_id'] );

// Remap values from PHP underscore_case to JS camelCase
$vars['form_submission'] = PUM_Utils_Array::remap_keys(
self::$form_submission,
$frontend_submission,
[
'form_provider' => 'formProvider',
'form_id' => 'formId',
Expand Down
28 changes: 22 additions & 6 deletions docs/form-submission-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,29 @@ PHP integrations use snake-cased keys:

```php
pum_integrated_form_submission( [
'form_provider' => 'example',
'form_id' => 12,
'submission_id' => 'entry-456',
'source_post_id' => 78,
'source_url' => 'https://example.com/guide/',
'context' => [
'form_provider' => 'example',
'form_id' => 12,
'submission_id' => 'entry-456',
'native_entry_id' => 'entry-456',
'fields' => [
'email' => 'person@example.test',
],
'source_post_id' => 78,
'source_url' => 'https://example.com/guide/',
'context' => [
'my_extension' => [
'campaign_id' => 90,
],
],
] );
```

`native_entry_id` identifies the provider-owned persisted entry when the
provider supplies one. `fields` contains the server-observed submitted values
available to PHP observation and action consumers. Providers must dispatch
these values through the existing normalized success call; features must not
add parallel provider hooks.

## Processing phases

The normalized phase map is:
Expand Down Expand Up @@ -137,6 +147,12 @@ authorization. Consumers must validate untrusted values before privileged
operations and apply their own privacy and retention policies before storing
submission data.

Submitted `fields`, `raw_fields`, and `native_entry_id` are server-only. Popup
Maker removes them from localized non-AJAX frontend replay data so submitted
PII and provider-admin identity are not exposed in page source. The existing
`submissionId` remains available to the browser for provider-native
cross-runtime deduplication.

## Public extension points

| Name | Runtime | Purpose |
Expand Down
12 changes: 12 additions & 0 deletions includes/functions/developers.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ function pum_get_integrated_form_submission_phases( $args = [] ) {
* @type string|int $form_id Form ID, usually numeric, but can be hash based.
* @type int $form_instance_id Optional form instance ID.
* @type string|int $submission_id Stable submission or provider entry ID. Generated when omitted.
* @type string|int $native_entry_id Optional authoritative provider entry ID.
* @type array $fields Optional server-observed submitted field values.
* @type int $popup_id Optional popup ID.
* @type int $source_post_id Optional post/page ID where the form was submitted.
* @type string $source_url Optional URL where the form was submitted.
Expand All @@ -133,6 +135,8 @@ function pum_integrated_form_submission( $args = [] ) {
'form_id' => null,
'form_instance_id' => null,
'submission_id' => null,
'native_entry_id' => null,
'fields' => [],
'source_post_id' => null,
'source_url' => $source_url,
'context' => [],
Expand All @@ -145,6 +149,8 @@ function pum_integrated_form_submission( $args = [] ) {
if ( ! isset( $args['submission_id'] ) || ( ! is_string( $args['submission_id'] ) && ! is_int( $args['submission_id'] ) ) || '' === (string) $args['submission_id'] ) {
$args['submission_id'] = wp_generate_uuid4();
}
$args['native_entry_id'] = isset( $args['native_entry_id'] ) && ( is_string( $args['native_entry_id'] ) || is_int( $args['native_entry_id'] ) ) && '' !== (string) $args['native_entry_id'] ? $args['native_entry_id'] : null;
$args['fields'] = isset( $args['fields'] ) && is_array( $args['fields'] ) ? $args['fields'] : [];

$source_post_id = is_scalar( $args['source_post_id'] ) && ! is_bool( $args['source_post_id'] ) && is_numeric( $args['source_post_id'] ) ? absint( $args['source_post_id'] ) : 0;
$args['source_post_id'] = $source_post_id ? $source_post_id : null;
Expand All @@ -157,13 +163,19 @@ function pum_integrated_form_submission( $args = [] ) {
}

$submission_id = $args['submission_id'];
$native_entry_id = $args['native_entry_id'];
$fields = $args['fields'];
$source_post_id_before_filter = $args['source_post_id'];

$args = apply_filters( 'pum_integrated_form_submission_args', $args );

if ( ! isset( $args['submission_id'] ) || ( ! is_string( $args['submission_id'] ) && ! is_int( $args['submission_id'] ) ) || '' === (string) $args['submission_id'] ) {
$args['submission_id'] = $submission_id;
}
if ( ! isset( $args['native_entry_id'] ) || ( ! is_string( $args['native_entry_id'] ) && ! is_int( $args['native_entry_id'] ) ) || '' === (string) $args['native_entry_id'] ) {
$args['native_entry_id'] = $native_entry_id;
}
$args['fields'] = isset( $args['fields'] ) && is_array( $args['fields'] ) ? $args['fields'] : $fields;

$filtered_source_post_id = isset( $args['source_post_id'] ) ? $args['source_post_id'] : null;
$filter_changed_source_post_id = $filtered_source_post_id !== $source_post_id_before_filter;
Expand Down
45 changes: 41 additions & 4 deletions tests/php/tests/FormSubmissionContext_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public function test_context_fields_have_defaults() {
$submission = PUM_Integrations::$form_submission;

$this->assertTrue( wp_is_uuid( $submission['submission_id'], 4 ) );
$this->assertNull( $submission['native_entry_id'] );
$this->assertSame( [], $submission['fields'] );
$this->assertNull( $submission['source_post_id'] );
$this->assertNull( $submission['source_url'] );
$this->assertSame( [], $submission['context'] );
Expand Down Expand Up @@ -235,10 +237,12 @@ public function test_filter_removed_source_url_clears_implicit_post_id() {
*/
public function test_invalid_context_values_are_normalized() {
$this->context_filter = static function ( $args ) {
$args['submission_id'] = [];
$args['source_post_id'] = 'not-a-post';
$args['source_url'] = [];
$args['context'] = 'not-an-array';
$args['submission_id'] = [];
$args['native_entry_id'] = [];
$args['fields'] = 'not-an-array';
$args['source_post_id'] = 'not-a-post';
$args['source_url'] = [];
$args['context'] = 'not-an-array';

return $args;
};
Expand All @@ -249,11 +253,36 @@ public function test_invalid_context_values_are_normalized() {
$submission = PUM_Integrations::$form_submission;

$this->assertTrue( wp_is_uuid( $submission['submission_id'], 4 ) );
$this->assertNull( $submission['native_entry_id'] );
$this->assertSame( [], $submission['fields'] );
$this->assertNull( $submission['source_post_id'] );
$this->assertNull( $submission['source_url'] );
$this->assertSame( [], $submission['context'] );
}

/**
* Invalid filters cannot erase authoritative provider evidence.
*/
public function test_invalid_filter_values_preserve_provider_evidence() {
$this->context_filter = static function ( $args ) {
$args['native_entry_id'] = [];
$args['fields'] = 'not-an-array';

return $args;
};
add_filter( 'pum_integrated_form_submission_args', $this->context_filter );

pum_integrated_form_submission(
[
'native_entry_id' => 'entry-42',
'fields' => [ 'email' => 'person@example.test' ],
]
);

$this->assertSame( 'entry-42', PUM_Integrations::$form_submission['native_entry_id'] );
$this->assertSame( 'person@example.test', PUM_Integrations::$form_submission['fields']['email'] );
}

/**
* Non-numeric source post IDs are never coerced into post 1.
*
Expand Down Expand Up @@ -296,6 +325,9 @@ public function test_submission_context_is_remapped_for_javascript() {
'form_id' => 4,
'form_instance_id' => 2,
'submission_id' => 'entry-12',
'native_entry_id' => 'entry-12',
'fields' => [ 'email' => 'private@example.test' ],
'raw_fields' => [ 'email' => 'raw-private@example.test' ],
'popup_id' => 55,
'source_post_id' => 78,
'source_url' => 'https://example.com/guide/',
Expand All @@ -314,6 +346,11 @@ public function test_submission_context_is_remapped_for_javascript() {
$this->assertSame( 4, $submission['formId'] );
$this->assertSame( 2, $submission['formInstanceId'] );
$this->assertSame( 'entry-12', $submission['submissionId'] );
$this->assertArrayNotHasKey( 'native_entry_id', $submission );
$this->assertArrayNotHasKey( 'fields', $submission );
$this->assertArrayNotHasKey( 'raw_fields', $submission );
$this->assertNotContains( 'private@example.test', $submission, true );
$this->assertNotContains( 'raw-private@example.test', $submission, true );
$this->assertSame( 55, $submission['popupId'] );
$this->assertSame( 78, $submission['sourcePostId'] );
$this->assertSame( 'https://example.com/guide/', $submission['sourceUrl'] );
Expand Down
37 changes: 27 additions & 10 deletions tests/php/tests/FormSubmissionPhases_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class FormSubmissionPhases_Test extends WP_UnitTestCase {
public function tearDown(): void {
PUM_Integrations::$form_submission = null;
unset( $_REQUEST['pum_form_popup_id'] );
unset( $_REQUEST['_wp_http_referer'] );
unset( $_REQUEST['action'] );
unset( $_POST['gform_ajax'] );

Expand Down Expand Up @@ -220,45 +221,61 @@ public function test_ajax_provider_callback_actions_without_tracking_or_frontend
'post_status' => 'publish',
]
);
$source_post_id = self::factory()->post->create();
$_REQUEST['pum_form_popup_id'] = $popup_id;
$_REQUEST['_wp_http_referer'] = get_permalink( $source_post_id );
$tracking_service = new \PopupMaker\Services\FormConversionTracking( new stdClass() );
$tracking_service->reset_site_count();
$tracking_service->reset_popup_count( $popup_id );
$tracking_service->init();

$observed = null;
$action_runs = 0;
$observed = [];
$action_args = [];
add_action(
'pum_integrated_form_submission',
static function ( $args ) use ( &$observed ) {
$observed = $args;
$observed[] = $args;
}
);
add_action(
'pum_integrated_form_submission_actions',
static function () use ( &$action_runs ) {
++$action_runs;
static function ( $args ) use ( &$action_args ) {
$action_args[] = $args;
}
);

$integration = new PUM_Integration_Form_FluentForms();
$integration->on_success(
'entry-91',
[],
[
'email' => 'person@example.test',
'name' => [
'first' => 'Ada',
'last' => 'Lovelace',
],
],
(object) [ 'attributes' => (object) [ 'id' => 7 ] ]
);

$this->assertSame( 'entry-91', $observed['submission_id'] );
$this->assertTrue( $observed['ajax'] );
$this->assertCount( 1, $observed );
$this->assertCount( 1, $action_args );
$submission = $observed[0];
$this->assertSame( 'entry-91', $submission['submission_id'] );
$this->assertSame( 'entry-91', $submission['native_entry_id'] );
$this->assertSame( 'person@example.test', $submission['fields']['email'] );
$this->assertSame( 'Ada', $submission['fields']['name']['first'] );
$this->assertSame( get_permalink( $source_post_id ), $submission['source_url'] );
$this->assertSame( $source_post_id, $submission['source_post_id'] );
$this->assertSame( $submission, $action_args[0] );
$this->assertTrue( $submission['ajax'] );
$this->assertSame(
[
'actions' => true,
'tracking' => false,
'frontend' => false,
],
$observed['phases']
$submission['phases']
);
$this->assertSame( 1, $action_runs );
$this->assertNull( PUM_Integrations::$form_submission );
$this->assertSame( 0, (int) get_post_meta( $popup_id, 'popup_conversion_count', true ) );
$this->assertSame( 0, $tracking_service->get_site_count() );
Expand Down
Loading