diff --git a/assets/js/src/site/plugins/pum-integrations.js b/assets/js/src/site/plugins/pum-integrations.js index c01d0163d..b07380482 100644 --- a/assets/js/src/site/plugins/pum-integrations.js +++ b/assets/js/src/site/plugins/pum-integrations.js @@ -7,15 +7,17 @@ // Ensure PUM exists globally window.PUM = window.PUM || {}; window.PUM.integrations = window.PUM.integrations || {}; + const PUM = window.PUM; + const pumVars = window.pum_vars; function filterNull( x ) { return x; } $.extend( window.PUM.integrations, { - init: function () { - if ( 'undefined' !== typeof pum_vars.form_submission ) { - var submission = pum_vars.form_submission; + init() { + if ( pumVars && 'undefined' !== typeof pumVars.form_submission ) { + const submission = pumVars.form_submission; // Declare these are not AJAX submissions. submission.ajax = false; @@ -39,12 +41,15 @@ * @param {Object} form JavaScript DOM node or jQuery object for the form submitted * @param {Object} args { * @type {string} formProvider Such as gravityforms or ninjaforms - * @type {string|int} formId Usually an integer ID number such as 1 - * @type {int} formInstanceId Not all form plugins support this. + * @type {string|number} formId Usually an integer ID number such as 1 + * @type {number} formInstanceId Not all form plugins support this. + * @type {string|number} submissionId Optional provider submission or entry ID. + * @type {number} sourcePostId Optional post/page ID where the form was submitted. + * @type {Object} context Extension-owned submission context. * } */ - formSubmission: function ( form, args ) { - var $popup = PUM.getPopup( form ); + formSubmission( form, args ) { + const $popup = PUM.getPopup( form ); args = $.extend( { @@ -52,6 +57,9 @@ formProvider: null, formId: null, formInstanceId: null, + submissionId: null, + sourcePostId: null, + context: {}, formKey: null, ajax: true, // Allows detecting submissions that may have already been counted. tracked: false, @@ -73,6 +81,17 @@ // $popup.trigger('pumFormSuccess'); } + /** + * Filters normalized form submission arguments before success handlers run. + * + * Extensions can append context without coupling to individual providers. + */ + args = window.PUM.hooks.applyFilters( + 'pum.integration.form.submissionArgs', + args, + form + ); + /** * This hook fires after any integrated form is submitted successfully. * @@ -83,10 +102,13 @@ * @param {Object} form JavaScript DOM node or jQuery object for the form submitted * @param {Object} args { * @type {string} formProvider Such as gravityforms or ninjaforms - * @type {string|int} formId Usually an integer ID number such as 1 - * @type {int} formInstanceId Not all form plugins support this. + * @type {string|number} formId Usually an integer ID number such as 1 + * @type {number} formInstanceId Not all form plugins support this. + * @type {string|number} submissionId Optional provider submission or entry ID. + * @type {number} sourcePostId Optional post/page ID where the form was submitted. + * @type {Object} context Extension-owned submission context. * @type {string} formKey Concatenation of provider, ID & Instance ID. - * @type {int} popupId The ID of the popup the form was in. + * @type {number} popupId The ID of the popup the form was in. * @type {Object} popup Usable jQuery object for the popup. * } */ @@ -96,14 +118,14 @@ args ); }, - checkFormKeyMatches: function ( + checkFormKeyMatches( formIdentifier, formInstanceId, submittedFormArgs ) { formInstanceId = '' === formInstanceId ? formInstanceId : false; // Check if the submitted form matches trigger requirements. - var checks = [ + const checks = [ // Any supported form. formIdentifier === 'any', @@ -138,28 +160,28 @@ * @since 1.9.0 * * @param {boolean} matchFound A boolean determining whether a match was found. - * @param {Object} args { + * @param {Object} args { * @type {string} formIdentifier gravityforms_any or ninjaforms_1 - * @type {int} formInstanceId Not all form plugins support this. + * @type {number} formInstanceId Not all form plugins support this. * @type {Object} submittedFormArgs{ * @type {string} formProvider Such as gravityforms or ninjaforms - * @type {string|int} formId Usually an integer ID number such as 1 - * @type {int} formInstanceId Not all form plugins support this. + * @type {string|number} formId Usually an integer ID number such as 1 + * @type {number} formInstanceId Not all form plugins support this. * @type {string} formKey Concatenation of provider, ID & Instance ID. - * @type {int} popupId The ID of the popup the form was in. + * @type {number} popupId The ID of the popup the form was in. * @type {Object} popup Usable jQuery object for the popup. * } * } * - * @returns {boolean} + * @return {boolean} */ return window.PUM.hooks.applyFilters( 'pum.integration.checkFormKeyMatches', matchFound, { - formIdentifier: formIdentifier, - formInstanceId: formInstanceId, - submittedFormArgs: submittedFormArgs, + formIdentifier, + formInstanceId, + submittedFormArgs, } ); }, diff --git a/classes/Integration/Form/FluentForms.php b/classes/Integration/Form/FluentForms.php index d61524a34..6ee1155c4 100644 --- a/classes/Integration/Form/FluentForms.php +++ b/classes/Integration/Form/FluentForms.php @@ -112,13 +112,32 @@ public function on_success( $submission_id, $form_data, $form ) { pum_integrated_form_submission( [ - 'popup_id' => $popup_id, - 'form_provider' => $this->key, - 'form_id' => $form_id, + 'popup_id' => $popup_id, + 'form_provider' => $this->key, + 'form_id' => $form_id, + 'submission_id' => $submission_id, + 'source_post_id' => $this->get_source_post_id(), ] ); } + /** + * Resolve the post that supplied the submitted form. + * + * @return int|null + */ + private function get_source_post_id() { + $referer = wp_get_raw_referer(); + + if ( ! $referer ) { + return null; + } + + $post_id = url_to_postid( $referer ); + + return $post_id ? $post_id : null; + } + /** * Get the popup ID for this form submission. * diff --git a/classes/Integrations.php b/classes/Integrations.php index d29c61980..6de111a3d 100644 --- a/classes/Integrations.php +++ b/classes/Integrations.php @@ -534,7 +534,9 @@ public static function pum_vars( $vars = [] ) { 'form_provider' => 'formProvider', 'form_id' => 'formId', 'form_instance_id' => 'formInstanceId', + 'submission_id' => 'submissionId', 'popup_id' => 'popupId', + 'source_post_id' => 'sourcePostId', ] ); } diff --git a/docs/form-submission-context.md b/docs/form-submission-context.md new file mode 100644 index 000000000..1a5fbd11d --- /dev/null +++ b/docs/form-submission-context.md @@ -0,0 +1,42 @@ +# Normalized form submission context + +Popup Maker form integrations report successful submissions through +`pum_integrated_form_submission()` in PHP and +`PUM.integrations.formSubmission()` in JavaScript. Extensions can attach +portable, provider-independent data to the `context` object without coupling +their behavior to a specific form plugin. + +```php +pum_integrated_form_submission( [ + 'form_provider' => 'example', + 'form_id' => 12, + 'submission_id' => 'entry-456', + 'source_post_id' => 78, + 'context' => [ + 'my_extension' => [ + 'campaign_id' => 90, + ], + ], +] ); +``` + +JavaScript integrations use camel-cased keys. The +`pum.integration.form.submissionArgs` filter runs after Popup Maker has built +the form key and resolved the popup, but before conversion and success handlers +run. + +```js +PUM.hooks.addFilter( + 'pum.integration.form.submissionArgs', + ( args ) => ( { + ...args, + context: { + ...args.context, + myExtension: { campaignId: 90 }, + }, + } ) +); +``` + +Context is descriptive, not proof of authorization. Consumers must validate +untrusted values before using them for privileged operations. diff --git a/includes/functions/developers.php b/includes/functions/developers.php index 2149dd4ee..431568376 100644 --- a/includes/functions/developers.php +++ b/includes/functions/developers.php @@ -44,7 +44,10 @@ function pum_trigger_popup_form_success( $popup_id = null, $settings = [] ) { * @type string $form_provider Key indicating which form provider this form belongs to. * @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 Optional provider submission or entry ID. * @type int $popup_id Optional popup ID. + * @type int $source_post_id Optional post/page ID where the form was submitted. + * @type array $context Optional extension-owned submission context. * @type bool $ajax If the submission was processed via AJAX. Generally gonna be false outside of JavaScript. * @type bool $tracked Whether the submission has been handled by tracking code or not. Prevents duplicates. * } @@ -57,6 +60,9 @@ function pum_integrated_form_submission( $args = [] ) { 'form_provider' => null, 'form_id' => null, 'form_instance_id' => null, + 'submission_id' => null, + 'source_post_id' => null, + 'context' => [], 'ajax' => false, 'tracked' => false, ] diff --git a/tests/php/tests/FormSubmissionContext_Test.php b/tests/php/tests/FormSubmissionContext_Test.php new file mode 100644 index 000000000..6e41491c8 --- /dev/null +++ b/tests/php/tests/FormSubmissionContext_Test.php @@ -0,0 +1,91 @@ +context_filter ) { + remove_filter( 'pum_integrated_form_submission_args', $this->context_filter ); + } + if ( $this->submission_action ) { + remove_action( 'pum_integrated_form_submission', $this->submission_action ); + } + + parent::tearDown(); + } + + /** + * New context fields receive backward-compatible defaults. + */ + public function test_context_fields_have_defaults() { + pum_integrated_form_submission( + [ + 'form_provider' => 'gravityforms', + 'form_id' => 7, + ] + ); + + $submission = PUM_Integrations::$form_submission; + + $this->assertNull( $submission['submission_id'] ); + $this->assertNull( $submission['source_post_id'] ); + $this->assertSame( [], $submission['context'] ); + } + + /** + * Extension context survives filtering and normalized dispatch. + */ + public function test_extension_context_is_preserved() { + $received = null; + + $this->context_filter = static function ( $args ) { + $args['context']['content_upgrade'] = [ + 'incentive_id' => 42, + ]; + + return $args; + }; + add_filter( + 'pum_integrated_form_submission_args', + $this->context_filter + ); + + $this->submission_action = static function ( $args ) use ( &$received ) { + $received = $args; + }; + add_action( + 'pum_integrated_form_submission', + $this->submission_action + ); + + pum_integrated_form_submission( + [ + 'form_provider' => 'gravityforms', + 'form_id' => 7, + 'submission_id' => 'entry-99', + 'source_post_id' => 123, + ] + ); + + $this->assertSame( 'entry-99', $received['submission_id'] ); + $this->assertSame( 123, $received['source_post_id'] ); + $this->assertSame( 42, $received['context']['content_upgrade']['incentive_id'] ); + } +}