$knownFields The form's field inventory, `name => label` * or a plain list of names. Empty when unknown. * * @return string One of the three class constants. */ public static function evaluate( string $consentField, $content, array $knownFields = array() ): string { $consentField = trim( $consentField ); if ( $consentField === '' ) { return self::PASSED; } // Which key does the payload actually use? Settings written before // 5.3.2 are lowercased, the payload is not. $resolved = self::resolveInPayload( $content, $consentField ); $lookup = $resolved !== '' ? $resolved : $consentField; if ( ! empty( SubmittedContent::findValue( $content, $lookup ) ) ) { return self::PASSED; } if ( self::mirrorSaysTicked( $content, $lookup ) ) { return self::PASSED; } // The payload carries the key but nothing in it — the field was on // the form and the visitor left it alone. No inventory needed. if ( $resolved !== '' ) { return self::NOT_GIVEN; } // Not in the payload. Only the form's own definition can say // whether that is an unticked checkbox or a stale setting. if ( SubmittedContent::matchFieldName( $consentField, self::normalizeFieldNames( $knownFields ) ) !== '' ) { return self::NOT_GIVEN; } return self::FIELD_UNKNOWN; } /** * May a `NOT_GIVEN` verdict actually reject this submission? * * The one method here that touches WordPress. It exists as an escape * hatch for the 5.4.0 rollout: enforcement is on by default, and a * site that hits an edge case nobody anticipated can switch it off per * form without downgrading or losing the rest of the release. * * Not a setting on purpose — a checkbox in the UI would invite people * to turn the gate off to make an inconvenient rejection go away, and * the rejection is the point. * * @param int $formId The form being submitted. * @param string $integration The integration identifier. */ public static function isEnforced( int $formId, string $integration = '' ): bool { if ( ! function_exists( 'apply_filters' ) ) { return true; } /** * Filter whether the consent gate may reject a submission whose * acceptance field was not confirmed. * * Returning false accepts the submission and logs a warning — the * stored consent proof is then not backed by a confirmation. * * @since 5.4.0 * * @param bool $enforce Whether to reject. Default true. * @param int $formId The form being submitted. * @param string $integration The integration identifier. */ return (bool) apply_filters( 'f12_doi_enforce_consent_gate', true, $formId, $integration ); } /** * Reduce a field inventory to a plain list of field names. * * Every integration's `getFormFields()` returns `name => label`, but a * caller that already has a list should not have to flip it. * * @param array $fields * * @return array */ public static function normalizeFieldNames( array $fields ): array { if ( $fields === array() ) { return array(); } $isList = array_keys( $fields ) === range( 0, count( $fields ) - 1 ); $names = $isList ? array_values( $fields ) : array_keys( $fields ); $out = array(); foreach ( $names as $name ) { if ( is_scalar( $name ) && (string) $name !== '' ) { $out[] = (string) $name; } } return $out; } /** * The canonical key the payload uses for this field, or '' when the * payload does not carry it at all. * * @param mixed $content * @param string $consentField */ private static function resolveInPayload( $content, string $consentField ): string { $names = array(); $levels = self::payloadLevels( $content ); foreach ( $levels as $level ) { foreach ( array_keys( $level ) as $key ) { $names[] = (string) $key; } } return SubmittedContent::matchFieldName( $consentField, $names ); } /** * WPForms ships every checkbox twice: the bare id holds the joined * display labels, and `field_{id}` holds the structured record. When * the labels are empty the joined string is empty too, so a ticked box * reads as untouched — the `value_raw` array in the mirror is the * honest signal. Reported 2026-05-13. * * @param mixed $content * @param string $consentField */ private static function mirrorSaysTicked( $content, string $consentField ): bool { $mirror = SubmittedContent::findValue( $content, 'field_' . $consentField ); if ( ! is_array( $mirror ) ) { return false; } $candidates = array( $mirror['value_raw'] ?? null, $mirror['value'] ?? null, ); foreach ( $candidates as $candidate ) { if ( is_array( $candidate ) ) { foreach ( $candidate as $entry ) { if ( is_scalar( $entry ) && (string) $entry !== '' ) { return true; } } continue; } if ( is_scalar( $candidate ) && (string) $candidate !== '' ) { return true; } } return false; } /** * Top level plus the unwrapped field map, in probe order. * * @param mixed $content * * @return array> */ private static function payloadLevels( $content ): array { if ( ! is_array( $content ) ) { return array(); } $levels = array( $content ); $fields = SubmittedContent::unwrapFields( $content ); if ( $fields !== $content ) { $levels[] = $fields; } return $levels; } }