PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.9.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.9.0
5.9.0 5.8.0 5.8.1 5.7.0 5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 All 42 releases
← All changes | src/Admin/AdminRestController.php +31 -2 5.8.0 → 5.9.0 View file →
@@ -32,8 +32,14 @@
32 32 class AdminRestController {
33 33
34 34 const API_NAMESPACE = 'f12-doi/v1';
35 35
36 + /**
37 + * SQL form of OptIn::isOptedOut(): not confirmed, withdrawal IP and time
38 + * recorded. A re-opt-in clears the time, so the row counts as confirmed again.
39 + */
40 + private const REVOKED_SQL = "(doubleoptin = 0 AND ipaddr_optout IS NOT NULL AND ipaddr_optout <> '' AND optouttime IS NOT NULL AND optouttime NOT IN ('', '0'))";
41 +
36 42 private LoggerInterface $logger;
37 43 private FormSettingsService $formService;
38 44 private FormSettingsValidator $formValidator;
39 45
@@ -617,9 +623,10 @@
617 623 $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
618 624
619 625 $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
620 626 $confirmed = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE doubleoptin = 1" );
621 - $pending = $total - $confirmed;
627 + $revoked = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE " . self::REVOKED_SQL ); // phpcs:ignore WordPress.DB.PreparedSQL -- fixed SQL, no input.
628 + $pending = max( 0, $total - $confirmed - $revoked );
622 629 $rate = $total > 0 ? round( ( $confirmed / $total ) * 100, 1 ) : 0;
623 630
624 631 // Recent opt-ins (raw activity feed — not analytics).
625 632 // Time-bucketed activity, top-forms breakdown and the big
@@ -625,9 +632,9 @@
625 632 // Time-bucketed activity, top-forms breakdown and the big
626 633 // conversion-rate card moved into addon-analytics, which
627 634 // renders them at the `dashboard.widget` mount point.
628 635 $recent = $wpdb->get_results(
629 - "SELECT id, email, cf_form_id, doubleoptin, createtime FROM {$table} ORDER BY id DESC LIMIT 5",
636 + "SELECT id, email, cf_form_id, doubleoptin, createtime, ipaddr_optout, optouttime FROM {$table} ORDER BY id DESC LIMIT 5",
630 637 ARRAY_A
631 638 );
632 639
633 640 foreach ( $recent as &$row ) {
@@ -633,14 +640,18 @@
633 640 foreach ( $recent as &$row ) {
634 641 $post = get_post( (int) $row['cf_form_id'] );
635 642 $row['formName'] = $post ? $post->post_title : sprintf( '#%d', $row['cf_form_id'] );
636 643 $row['confirmed'] = (int) $row['doubleoptin'] === 1;
644 + $row['revoked'] = self::isRevokedRow( $row );
645 + unset( $row['ipaddr_optout'], $row['optouttime'] );
637 646 }
647 + unset( $row );
638 648
639 649 $data = array(
640 650 'totalOptins' => $total,
641 651 'confirmed' => $confirmed,
642 652 'pending' => $pending,
653 + 'revoked' => $revoked,
643 654 'conversionRate' => $rate,
644 655 'recentOptins' => $recent ?: array(),
645 656 );
646 657
@@ -721,8 +732,11 @@
721 732 if ( $status === 'confirmed' ) {
722 733 $where[] = 'doubleoptin = 1';
723 734 } elseif ( $status === 'pending' ) {
724 735 $where[] = '(doubleoptin = 0 OR doubleoptin IS NULL)';
736 + $where[] = 'NOT ' . self::REVOKED_SQL;
737 + } elseif ( $status === 'revoked' ) {
738 + $where[] = self::REVOKED_SQL;
725 739 }
726 740
727 741 if ( $formId !== null && $formId !== '' ) {
728 742 $where[] = 'cf_form_id = %d';
@@ -2170,8 +2184,21 @@
2170 2184 // HELPERS
2171 2185 // ═══════════════════════════════════════════════════════════════
2172 2186
2173 2187 /**
2188 + * PHP form of REVOKED_SQL for a raw table row.
2189 + *
2190 + * @param array<string, mixed> $row The database row.
2191 + */
2192 + private static function isRevokedRow( array $row ): bool {
2193 + $optOutTime = (string) ( $row['optouttime'] ?? '' );
2194 +
2195 + return (int) ( $row['doubleoptin'] ?? 0 ) !== 1
2196 + && (string) ( $row['ipaddr_optout'] ?? '' ) !== ''
2197 + && $optOutTime !== '' && $optOutTime !== '0';
2198 + }
2199 +
2200 + /**
2174 2201 * Format an opt-in database row for the API response.
2175 2202 *
2176 2203 * @param array $row The database row.
2177 2204 * @param bool $detailed Whether to include full detail (content, mail data).
@@ -2188,8 +2215,10 @@
2188 2215 'formId' => (int) $row['cf_form_id'],
2189 2216 'formName' => $post ? $post->post_title : sprintf( '#%d', $row['cf_form_id'] ),
2190 2217 'category' => (int) $row['category'],
2191 2218 'confirmed' => (int) $row['doubleoptin'] === 1,
2219 + // Consent withdrawn via the opt-out (5.9.0). Not "pending".
2220 + 'revoked' => self::isRevokedRow( $row ),
2192 2221 // Confirmation mail: 'sent' (handed to the mail server), 'failed',
2193 2222 // or '' (recorded before 5.8.0). Since 5.8.0.
2194 2223 'mailStatus' => (string) ( $row['mail_status'] ?? '' ),
2195 2224 'createtime' => $this->toSiteLocalTime( $row['createtime'] ),