Skip to content

Preserve GravityForms field mapping on connection change; add feed JSON export/import - #281

Open
davidperezgar wants to merge 1 commit into
trunkfrom
claude/feed-connection-field-mapping-wf59rc
Open

Preserve GravityForms field mapping on connection change; add feed JSON export/import#281
davidperezgar wants to merge 1 commit into
trunkfrom
claude/feed-connection-field-mapping-wf59rc

Conversation

@davidperezgar

Copy link
Copy Markdown
Contributor

Summary

Closes #265.

  • Preserve field mapping on connection change: GFCRM::save_feed_settings() now reconciles a feed's field mapping instead of letting it be silently wiped when the CRM connection or module changes. It matches by target field key first, then falls back to matching by label against a stored listFields_labels snapshot, so standard fields (email, phone, etc.) keep their mapping across a connection switch.
  • Export/Import GravityForms feeds as JSON: added "Export Feed (JSON)" / "Import Feed (JSON)" controls on the feed edit page. Export downloads the feed's connection type, module, field mapping, merge strategy and conditional logic as JSON; import reconciles the uploaded mapping against the feed's current connection using the same key/label matching logic, then applies it via GFAPI::update_feed(). CRM credentials are intentionally excluded from the export for security.
  • Added reusable pure helpers in helpers-functions.php: formscrm_gf_build_field_map_labels(), formscrm_gf_merge_field_map(), formscrm_gf_build_feed_export(), formscrm_gf_feed_meta_from_import().
  • Bumped plugin version to 4.5.0 with a changelog entry, and added the missing GFAPI/GFFeedAddOn method stubs used by PHPStan.

Test plan

  • composer lint — PHPCS (WordPress coding standards)
  • composer phpstan
  • composer test — new suite tests/Unit/test-gravityforms-feed-mapping.php covers the merge/export/import helpers (key match, label match/case-insensitivity, unmatched fields, credential exclusion, round trip)
  • Manual: on a GravityForms feed with an existing mapping, switch the feed's CRM connection/module and confirm previously mapped fields are retained where the target field still exists (by key or label)
  • Manual: export a feed to JSON, then import it into another feed/form and confirm the mapping is restored

Note: this sandbox could not run composer install to completion (a GitHub API zipball request for phpstan/phpstan was blocked by the environment's egress policy), so the automated lint/phpstan/test commands above could not be executed here. Syntax was verified with php -l on every changed file, and the new pure helper functions were exercised against hand-written stubs outside PHPUnit; the checklist above should be run before merging.

🤖 Generated with Claude Code


Generated by Claude Code

…ON export/import

- GFCRM::save_feed_settings() now reconciles the field mapping against the
  feed's previous values whenever the CRM connection or module changes,
  matching by target field key first and falling back to a label match
  (via a stored listFields_labels snapshot), instead of losing the mapping.
- Added Export/Import JSON controls on the feed edit page: a feed's
  connection type, module and field mapping can be downloaded as JSON and
  re-applied later, reconciled against the current connection the same way.
  Credentials are intentionally excluded from the export.
- New pure helpers in helpers-functions.php: formscrm_gf_build_field_map_labels(),
  formscrm_gf_merge_field_map(), formscrm_gf_build_feed_export(),
  formscrm_gf_feed_meta_from_import(), with unit tests.

Closes #265.

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac80cc678c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +951 to +952
if ( ! empty( $old_values[ $key ] ) ) {
$merged[ $key ] = $old_values[ $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.

P1 Badge Honor deliberately cleared field mappings

When an existing feed mapping is deliberately changed to the blank option, the submitted value is empty (or omitted), so this branch restores the previous non-empty value. Because save_feed_settings() invokes this merge for every save rather than only during a connection/module transition, administrators cannot unmap a field and submissions continue sending data they explicitly attempted to remove.

Useful? React with 👍 / 👎.

}
}

$merged = $new_values;

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 Restrict merged mappings to the new field map

When the connection or module selector auto-submits, new_values contains inputs rendered for the previous selection, but this initializes the result with every one of those keys and never removes keys absent from new_field_map. The obsolete CRM field names are therefore saved alongside reconciled fields, and process_feed() later iterates every stored listFields entry at class-gravityforms.php:1098-1105, potentially sending invalid or unintended fields to the newly selected module.

Useful? React with 👍 / 👎.

if ( $new_crmlib ) {
$old_feed = GFAPI::get_feed( $feed_id );
$old_values = ( ! is_wp_error( $old_feed ) && ! empty( $old_feed['meta']['listFields'] ) ) ? $old_feed['meta']['listFields'] : array();
$old_labels = ( ! is_wp_error( $old_feed ) && ! empty( $old_feed['meta']['listFields_labels'] ) ) ? $old_feed['meta']['listFields_labels'] : array();

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 Backfill labels before the first connection switch

Feeds created before this release—and newly created feeds, because the initial save has no feed ID—do not yet contain listFields_labels. On their first connection/module switch, $old_labels is consequently empty, so mappings whose target keys changed cannot use the advertised label fallback; storing labels for the new map later in this save is too late. The old field definitions need to be derived or backfilled before performing the first reconciliation.

Useful? React with 👍 / 👎.


$current_feed = GFAPI::get_feed( $feed_id );

if ( false === $meta || is_wp_error( $current_feed ) || empty( $current_feed ) ) {

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 Validate the imported feed belongs to this form and add-on

The import nonce is scoped only to the client-supplied form_id, while feed_id can be replaced independently, and this validation accepts any feed returned by the globally keyed GFAPI::get_feed(). A user with the FormsCRM capability and a valid import nonce can therefore substitute another form's or another add-on's feed ID, after which update_feed() overwrites that feed and supplies the attacker-selected form ID; verify both the feed's form_id and addon_slug before updating it.

Useful? React with 👍 / 👎.

$merged_meta = array_merge( $current_meta, $meta );
$updated = GFAPI::update_feed( $feed_id, $merged_meta, $form_id );

wp_safe_redirect( add_query_arg( 'formscrm_import_status', $updated ? 'success' : 'error', $redirect_url ) );

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 Report GFAPI update errors as import failures

When GFAPI::update_feed() returns a WP_Error—which the newly added PHPStan stub explicitly permits—the object is truthy, so the redirect reports success even though the feed was not updated. Check is_wp_error( $updated ) before converting the result to an import status so database/API failures do not produce a false success notice.

Useful? React with 👍 / 👎.

'formscrm_feed_export' => true,
'schema_version' => 1,
'feed_name' => isset( $meta['feedName'] ) ? $meta['feedName'] : '',
'fc_crm_custom_type' => isset( $meta['fc_crm_custom_type'] ) ? $meta['fc_crm_custom_type'] : 'no',

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 Export the resolved default CRM type

For the common case where a feed uses the plugin-wide CRM connection, this field contains only the sentinel no; the actual connector type lives in global settings and is absent from the JSON. Importing that advertised cross-site backup on a site with a different default CRM consequently selects the target site's connector while retaining the source module and mappings, so the feed can submit an incompatible payload. Include the resolved CRM type separately while continuing to exclude its credentials.

Useful? React with 👍 / 👎.

'fc_crm_module' => isset( $meta['fc_crm_module'] ) ? $meta['fc_crm_module'] : '',
'fc_crm_merge_entry' => isset( $meta['fc_crm_merge_entry'] ) ? $meta['fc_crm_merge_entry'] : '',
'fc_crm_webhook' => isset( $meta['fc_crm_webhook'] ) ? $meta['fc_crm_webhook'] : '',
'listFields' => isset( $meta['listFields'] ) && is_array( $meta['listFields'] ) ? $meta['listFields'] : array(),

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 Reconcile source field IDs when importing into another form

The exported mapping values are Gravity Forms field IDs from the source form, but the import logic reconciles only the CRM keys and stores these values unchanged. In the advertised move-to-another-form/site workflow, a mapping such as email => 3 therefore reads field 3 from the target form even when its email field has a different ID (and field 3 may contain unrelated data), because get_value_from_field() uses the stored ID directly. Export source-field identity metadata and resolve it against the target form before applying the mapping.

Useful? React with 👍 / 👎.

Comment on lines +1017 to +1018
if ( isset( $import_data['fc_crm_custom_type'] ) && is_string( $import_data['fc_crm_custom_type'] ) ) {
$meta['fc_crm_custom_type'] = sanitize_text_field( $import_data['fc_crm_custom_type'] );

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 Reject imported CRM types unavailable on the target site

This accepts any sanitized string as fc_crm_custom_type, including a valid export naming an external CRM connector that is not installed on the target site. The import handler resolves fields using the feed's pre-import connector and therefore still reports success, but subsequent process_feed() calls make formscrm_get_api_class() return null and then dereference it at class-gravityforms.php:1170, fatally failing every submission handled by that feed. Validate that the imported connector resolves before updating the feed.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feed field mapping is lost when changing the CRM connection; add export/import for Gravity Forms feeds

2 participants