| @@ -14,8 +14,9 @@ | ||
| 14 | 14 | use Forge12\DoubleOptIn\Audit\AuditLogger; |
| 15 | 15 | use Forge12\DoubleOptIn\FormSettings\FormSettingsDTO; |
| 16 | 16 | use Forge12\DoubleOptIn\FormSettings\FormSettingsService; |
| 17 | 17 | use Forge12\DoubleOptIn\FormSettings\FormSettingsValidator; |
| 18 | +use Forge12\DoubleOptIn\Integration\SubmittedContent; | |
| 18 | 19 | use Forge12\Shared\LoggerInterface; |
| 19 | 20 | |
| 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { |
| 21 | 22 | exit; |
| @@ -736,8 +737,18 @@ | ||
| 736 | 737 | $where[] = 'cf_form_id = %d'; |
| 737 | 738 | $params[] = (int) $formId; |
| 738 | 739 | } |
| 739 | 740 | |
| 741 | + // Confirmed opt-ins whose follow-up actions failed or have an | |
| 742 | + // unknown outcome — the admin's "needs attention" list. | |
| 743 | + if ( sanitize_text_field( (string) ( $request->get_param( 'follow_up' ) ?? '' ) ) === 'problem' ) { | |
| 744 | + $followUpTable = $wpdb->prefix . \Forge12\DoubleOptIn\Repository\FollowUpSchema::TABLE_NAME; | |
| 745 | + $problems = \Forge12\DoubleOptIn\FollowUp\FollowUpStatus::problematic(); | |
| 746 | + $where[] = "EXISTS (SELECT 1 FROM {$followUpTable} fu WHERE fu.optin_id = {$table}.id AND fu.status IN (" | |
| 747 | + . implode( ', ', array_fill( 0, count( $problems ), '%s' ) ) . '))'; | |
| 748 | + $params = array_merge( $params, $problems ); | |
| 749 | + } | |
| 750 | + | |
| 740 | 751 | $whereClause = implode( ' AND ', $where ); |
| 741 | 752 | |
| 742 | 753 | // Count |
| 743 | 754 | $countQuery = "SELECT COUNT(*) FROM {$table} WHERE {$whereClause}"; |
| @@ -1439,8 +1450,13 @@ | ||
| 1439 | 1450 | |
| 1440 | 1451 | public function getSettings( \WP_REST_Request $request ): \WP_REST_Response { |
| 1441 | 1452 | $defaults = array( |
| 1442 | 1453 | 'telemetry' => 1, |
| 1454 | + // Optional "Double Opt-In by Forge12" credit on the confirmation | |
| 1455 | + // page. Defaults to 0 and must stay that way: wordpress.org | |
| 1456 | + // guideline 10 requires credit links to be off unless the site | |
| 1457 | + // owner explicitly turns them on. | |
| 1458 | + 'credit_link' => 0, | |
| 1443 | 1459 | 'delete' => 12, |
| 1444 | 1460 | 'delete_unconfirmed' => 7, |
| 1445 | 1461 | 'delete_period' => 'months', |
| 1446 | 1462 | 'delete_unconfirmed_period' => 'months', |
| @@ -1515,8 +1531,13 @@ | ||
| 1515 | 1531 | 'type' => 'int', |
| 1516 | 1532 | 'min' => 0, |
| 1517 | 1533 | 'max' => 1, |
| 1518 | 1534 | ), |
| 1535 | + 'credit_link' => array( | |
| 1536 | + 'type' => 'int', | |
| 1537 | + 'min' => 0, | |
| 1538 | + 'max' => 1, | |
| 1539 | + ), | |
| 1519 | 1540 | 'privacy_policy_page' => array( |
| 1520 | 1541 | 'type' => 'int', |
| 1521 | 1542 | 'min' => 0, |
| 1522 | 1543 | ), |
| @@ -2408,28 +2429,31 @@ | ||
| 2408 | 2429 | // configured, look up the value the user actually submitted. |
| 2409 | 2430 | // Truthy = explicit acknowledgment captured. Falsy = either |
| 2410 | 2431 | // gate wasn't enforced or this is a legacy record. |
| 2411 | 2432 | // |
| 2412 | - // Storage shape varies per integration: | |
| 2413 | - // - CF7 / WPForms / GF (default path) store fields flat | |
| 2414 | - // at the top level: $content[fieldName] = value. | |
| 2415 | - // - Avada wraps fields under a `data` sub-key alongside | |
| 2416 | - // metadata (field_labels, field_types, form_parameter) | |
| 2417 | - // — its OnSubmit overrides the flat content set by | |
| 2418 | - // createOptIn(). For Avada records, $content[fieldName] | |
| 2419 | - // is undefined; the value lives at $content['data'][fieldName]. | |
| 2433 | + // Where that value sits differs per integration, and this | |
| 2434 | + // reader got the list wrong twice: | |
| 2420 | 2435 | // |
| 2421 | - // Pre-2026-05-01 we only checked the flat shape, so every | |
| 2422 | - // Avada opt-in showed "User acknowledged: ✗ No" even when | |
| 2423 | - // the user explicitly checked the GDPR box. The fallback | |
| 2424 | - // below recognises the Avada shape too — adding a third | |
| 2425 | - // shape would be the next addition. | |
| 2426 | - $data['consentAcknowledged'] = ! empty( $data['consentField'] ) | |
| 2427 | - && is_array( $content ) | |
| 2428 | - && ( | |
| 2429 | - ! empty( $content[ $data['consentField'] ] ) | |
| 2430 | - || ! empty( $content['data'][ $data['consentField'] ] ?? null ) | |
| 2431 | - ); | |
| 2436 | + // 2026-05-01 Avada wraps its fields under `data`, so the | |
| 2437 | + // flat lookup missed and every Avada opt-in | |
| 2438 | + // showed "User acknowledged: ✗ No" even with | |
| 2439 | + // the GDPR box explicitly checked. | |
| 2440 | + // 2026-08-27 Elementor stores the whole $_POST parameter | |
| 2441 | + // dict, so its fields sit under `form_fields` | |
| 2442 | + // — the same symptom, one integration further | |
| 2443 | + // on. The docblock added after the Avada fix | |
| 2444 | + // had predicted exactly this ("adding a third | |
| 2445 | + // shape would be the next addition"). | |
| 2446 | + // | |
| 2447 | + // The shape list now lives in SubmittedContent, shared with | |
| 2448 | + // OptInFrontend::addPlaceholders() — the other consumer that | |
| 2449 | + // already knew all of them. A fourth integration with a | |
| 2450 | + // fourth layout is taught to both at once. | |
| 2451 | + // | |
| 2452 | + // The lookup also tolerates a consent_field that the | |
| 2453 | + // pre-5.3.2 sanitize_key() lowercased, so installations | |
| 2454 | + // recover from the update without re-saving every form. | |
| 2455 | + $data['consentAcknowledged'] = SubmittedContent::hasValue( $content, (string) $data['consentField'] ); | |
| 2432 | 2456 | |
| 2433 | 2457 | // Parse mail_optin |
| 2434 | 2458 | $mailOptin = maybe_unserialize( $row['mail_optin'] ); |
| 2435 | 2459 | $data['mailOptin'] = is_array( $mailOptin ) ? $mailOptin : array(); |