Skip to content

Clean usage data before sending#2653

Merged
Crabcyborg merged 7 commits into
masterfrom
usage-tracking-improvements
Jan 21, 2026
Merged

Clean usage data before sending#2653
Crabcyborg merged 7 commits into
masterfrom
usage-tracking-improvements

Conversation

@truongwp

@truongwp truongwp commented Dec 12, 2025

Copy link
Copy Markdown
Contributor

Related issue: https://github.com/Strategy11/formidable-pro/issues/6112

This PR does:

  • Change field options and form action settings from JSON to array.
  • Data contains @ or key ends with _email will be replaced by {{contain_email}}.
  • Some long text data (description and key ends with _msg) is replaced by {{long_text}}. We clean more of them at the usage site.
  • Keys that end with _url are removed.
  • More keys are removed before sending. I copy them from the usage site.

This requires a change in the usage site to support the array instead of the old JSON data: https://github.com/Strategy11/ff-usage-2/pull/6

Summary by CodeRabbit

Bug Fixes

  • Enhanced data sanitization with improved filtering of sensitive information before transmission
  • Email addresses and sensitive fields are now masked with placeholders to protect user privacy
  • Long text values replaced with placeholders to optimize data transmission size
  • Admin email data removed from usage snapshots for improved privacy compliance

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 12, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

send_snapshot() now builds a local snapshot, runs clean_before_send(&$data) to remove/mask sensitive and long-text fields, logs the sanitized snapshot, and sends it. snapshot() no longer includes admin_email. fields() and actions() preserve decoded/unserialized data instead of re-encoding.

Changes

Cohort / File(s) Summary
Usage model (sanitization & snapshot flow)
classes/models/FrmUsage.php
Added private function clean_before_send(&$data) to recursively remove/mask sensitive keys and replace long-text fields; send_snapshot() now builds a local snapshot, calls clean_before_send() and JSON-encodes the sanitized payload for sending; snapshot() omits admin_email.
Field & action data handling
classes/models/FrmUsage.php
fields() stops JSON-encoding field_options and assigns decoded/unserialized values directly; actions() sets settings via FrmAppHelper::maybe_json_decode($action->post_content) instead of using raw post_content.

Sequence Diagram(s)

sequenceDiagram
    participant FrmUsage as FrmUsage (model)
    participant SnapshotGen as snapshot()
    participant Sanitizer as clean_before_send()
    participant Remote as External API

    FrmUsage->>SnapshotGen: build snapshot()
    SnapshotGen-->>FrmUsage: snapshot data
    FrmUsage->>Sanitizer: clean_before_send(snapshot)
    Sanitizer-->>FrmUsage: sanitized snapshot
    FrmUsage->>Remote: send JSON-encoded sanitized snapshot
    Remote-->>FrmUsage: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • Crabcyborg

Poem

"I hopped through code with tiny paws,
masking secrets, mending flaws.
Long texts tucked, emails swapped,
snapshot safe — my work is hopped. 🐇✨"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Clean usage data before sending' is clear, concise, and accurately reflects the main objective of the PR—sanitizing and cleaning usage data prior to transmission to the server.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
classes/models/FrmUsage.php (1)

16-40: Remove debug logging and harden JSON/body creation + request error handling.
Line 23 error_log( print_r( $snapshot, true ) ); shouldn’t ship (log noise + still potentially sensitive even after sanitization). Also consider using wp_json_encode + bailing on encoding failure, and handling wp_remote_request errors.

 		$snapshot = $this->snapshot();
 		$this->clean_before_send( $snapshot );
-		error_log( print_r( $snapshot, true ) );
 
 		$ep = 'aHR0cHM6Ly91c2FnZTIuZm9ybWlkYWJsZWZvcm1zLmNvbS9zbmFwc2hvdA==';
 		// $ep = base64_encode( 'http://localhost:4567/snapshot' ); // Uncomment for testing
-		$body = json_encode( $snapshot );
+		$body = wp_json_encode( $snapshot );
+		if ( false === $body ) {
+			return;
+		}

Optionally capture/inspect the response:

-		wp_remote_request( base64_decode( $ep ), $post );
+		$response = wp_remote_request( base64_decode( $ep ), $post );
+		// Optionally handle is_wp_error( $response ) here.
🧹 Nitpick comments (2)
classes/models/FrmUsage.php (2)

576-596: actions(): good move to maybe_json_decode settings; consider normalizing non-array fallbacks.
If $action->post_content isn’t valid JSON, maybe_json_decode returns the original string; downstream sanitization expects arrays in some places. Consider forcing an array fallback:

-				'settings' => FrmAppHelper::maybe_json_decode( $action->post_content ),
+				'settings' => (array) FrmAppHelper::maybe_json_decode( $action->post_content ),

556-569: fields(): remove unnecessary reassignment after unserialize_or_decode

Line 565 passes $field->field_options by reference to unserialize_or_decode(), which mutates it in-place and returns void. Line 566's reassignment is redundant. Simplify to:

-		foreach ( $fields as $k => $field ) {
-			FrmAppHelper::unserialize_or_decode( $field->field_options );
-			$fields[ $k ]->field_options = $field->field_options;
+		foreach ( $fields as $k => $field ) {
+			FrmAppHelper::unserialize_or_decode( $fields[ $k ]->field_options );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf1c4a1 and 53cb538.

📒 Files selected for processing (1)
  • classes/models/FrmUsage.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
classes/models/FrmUsage.php (1)
classes/helpers/FrmAppHelper.php (2)
  • FrmAppHelper (6-5084)
  • maybe_json_decode (3720-3741)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Cypress
  • GitHub Check: Cypress
  • GitHub Check: PHP 8 tests in WP trunk
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: PHP 8 tests in WP trunk

Comment thread classes/models/FrmUsage.php

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
classes/models/FrmUsage.php (1)

118-223: clean_before_send() still leaks sensitive fields, mishandles long text, and can fatally error on objects (PHP 8+)

Most of the issues called out in the earlier review are still present in this version:

  1. Skip keys and _url keys are not actually removed.

    • In the loop you continue when in_array( $key, $skip_keys, true ) or when the key ends with _url, but you never unset() those entries.
    • As a result, highly sensitive fields like api_key, business_email, account_num, routing_num, url, and *_url keys are still present in $data and will be sent in the payload, which contradicts the PR description (“remove keys that end with _url” and “remove several other keys before sending”).
    • These should be removed from the array, not just skipped for further processing.
  2. description (and some _msg fields) never get the {{long_text}} placeholder.

    • description is in both $skip_keys and $long_text_keys. Because the skip check runs first, description values are left untouched.
    • Similarly, some _msg keys (e.g. success_msg, redirect_msg, reg_email_msg, unique_msg) are also in $skip_keys, so they never hit the long-text branch either.
    • This violates the stated behavior of replacing long text (e.g. description and _msg fields) with {{long_text}}.
  3. Objects in the snapshot (e.g. from fields()) are not handled and will cause a PHP 8+ TypeError.

    • snapshot()['fields'] is now an array of stdClass rows, each with field_options as an array. In the recursive call on $snapshot['fields'], the inner foreach sees each row as an object.
    • For those entries:
      • is_array( $value ) is false, so recursion stops.
      • Later, strpos( $value, '@' ) is called with $value as stdClass, which raises TypeError: strpos(): Argument #1 ($haystack) must be of type string, stdClass given on PHP 8+.
    • That’s a hard fatal, and it also means you never sanitize nested field_options at all.
  4. strpos( $value, '@' ) is unsafe without a type check.

    • $value can be non-string (arrays, objects, integers, booleans, etc.) across the snapshot structure.
    • On PHP 8+, calling strpos() on a non-string haystack throws a TypeError. This must be guarded with is_string( $value ).
  5. The function assumes $data is always an array.

    • Filters like frm_usage_snapshot, frm_usage_skip_keys, and frm_usage_long_text_keys can change structure. A defensive if ( ! is_array( $data ) ) { return; } at the top makes this robust.
  6. Docblock nits.

    • @since x.x should be replaced with a real version before release.
    • @paramm array $long_text_keys should be @param array $long_text_keys.

To address all of the above, I recommend restructuring the sanitizer roughly like this:

 private function clean_before_send( &$data ) {
-		$skip_keys = array(
+		if ( ! is_array( $data ) ) {
+			return;
+		}
+
+		$skip_keys = array(
 			// ...
-			// Form settings.
-			'form_id',
-			'description',
+			// Form settings.
+			'form_id',
 			// ...
 		);
@@
-		foreach ( $data as $key => &$value ) {
-			if ( ! $value || in_array( $key, $skip_keys, true ) || '_url' === substr( $key, -4 ) ) {
-				continue;
-			}
-
-			if ( is_array( $value ) ) {
-				$this->clean_before_send( $value );
-				continue;
-			}
-
-			if ( in_array( $key, $long_text_keys, true ) || '_msg' === substr( $key, -4 ) ) {
-				// Replace it with a placeholder.
-				$value = '{{long_text}}';
-				continue;
-			}
-
-			// If key ends with `_email`, or value contains `@`, this might contain an email address.
-			if ( '_email' === substr( $key, -6 ) || false !== strpos( $value, '@' ) ) {
-				// Replace it with a placeholder.
-				$value = '{{contain_email}}';
-			}
-		}//end foreach
+		foreach ( $data as $key => &$value ) {
+			$key_str = (string) $key;
+
+			// 1) Remove sensitive keys entirely (including *_url).
+			if ( in_array( $key_str, $skip_keys, true ) || '_url' === substr( $key_str, -4 ) ) {
+				unset( $data[ $key ] );
+				continue;
+			}
+
+			// 2) Recurse into arrays.
+			if ( is_array( $value ) ) {
+				$this->clean_before_send( $value );
+				continue;
+			}
+
+			// 3) Recurse into objects (e.g. stdClass rows from DB).
+			if ( is_object( $value ) ) {
+				$props = get_object_vars( $value );
+				$this->clean_before_send( $props );
+				foreach ( $props as $prop_name => $prop_value ) {
+					$value->$prop_name = $prop_value;
+				}
+				continue;
+			}
+
+			// 4) Skip empty scalars.
+			if ( '' === $value || null === $value ) {
+				continue;
+			}
+
+			// 5) Long-text placeholders (eg description, *_msg).
+			if ( in_array( $key_str, $long_text_keys, true ) || '_msg' === substr( $key_str, -4 ) ) {
+				$value = '{{long_text}}';
+				continue;
+			}
+
+			// 6) Email-like content → placeholder.
+			if ( '_email' === substr( $key_str, -6 ) || ( is_string( $value ) && false !== strpos( $value, '@' ) ) ) {
+				$value = '{{contain_email}}';
+			}
+		}
+		unset( $value );
 	}

Key points of this change:

  • Sensitive keys and _url suffixes are actually removed from the outgoing data.
  • description is no longer skipped entirely and will be replaced with {{long_text}} via $long_text_keys.
  • Objects (like field rows) are safely sanitized without causing TypeError on PHP 8+.
  • strpos() is guarded with is_string().

Please verify this behavior under PHP 8+ by running usage tracking in an environment with forms and actions that exercise nested fields/actions data to confirm there are no TypeErrors and that api_key, emails, and _url keys are absent from the final payload.

🧹 Nitpick comments (1)
classes/models/FrmUsage.php (1)

21-28: Remove or conditionally guard error_log( print_r( $snapshot, true ) ) in send_snapshot()

Logging the (supposedly sanitized) snapshot on every send will spam PHP error logs and, given the current sanitizer gaps, can still leak sensitive data into server logs. This should either be removed or wrapped in a debug guard (e.g. behind WP_DEBUG or a dedicated logging helper) before shipping.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53cb538 and 4e8e467.

📒 Files selected for processing (1)
  • classes/models/FrmUsage.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
classes/models/FrmUsage.php (1)
classes/helpers/FrmAppHelper.php (2)
  • FrmAppHelper (6-5084)
  • maybe_json_decode (3720-3741)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: PHP 8 tests in WP trunk
  • GitHub Check: Cypress
🔇 Additional comments (2)
classes/models/FrmUsage.php (2)

562-569: fields() change to keep decoded field_options looks good, but depends on sanitizer handling objects

Returning field_options as already-unserialized/decoded data on each stdClass row is a good move and aligns with the goal of sending arrays instead of JSON strings. However, note that snapshot()['fields'] is now an array of objects with nested arrays, so the effectiveness of PII/long-text scrubbing depends on clean_before_send() correctly recursing into those objects (see previous comment). Once clean_before_send() supports objects, this change is solid.


584-592: Using FrmAppHelper::maybe_json_decode() for action settings is appropriate

Decoding post_content via FrmAppHelper::maybe_json_decode() so that settings is an array (when JSON) is consistent with the new “send arrays instead of JSON” approach and makes it possible for clean_before_send() to traverse and scrub action settings. Just ensure the external usage endpoint has been updated to expect an array here (per your PR description), which it sounds like you’ve already coordinated.

Please double-check end-to-end that the updated usage service correctly handles settings as an array and no longer expects JSON strings.

@truongwp
truongwp requested a review from Crabcyborg January 20, 2026 18:38

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

Awesome.

Thanks @truongwp!

🚀

@Crabcyborg Crabcyborg added this to the 6.28 milestone Jan 21, 2026
@Crabcyborg
Crabcyborg merged commit e55b9a4 into master Jan 21, 2026
16 checks passed
@Crabcyborg
Crabcyborg deleted the usage-tracking-improvements branch January 21, 2026 15:43
stephywells pushed a commit that referenced this pull request Apr 4, 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.

2 participants