PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.8.1
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.8.1
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 3.0.51 All 41 releases
double-opt-in / src / Repository / OptInMailStatusRepository.php

OptInMailStatusRepository.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.8.1, at src/Repository/OptInMailStatusRepository.php

165 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Delivery status of the confirmation mail, per opt-in.
4 *
5 * Its own class rather than a method on OptInRepositoryInterface: that
6 * interface is public Core API for add-ons, and the three columns
7 * (`mail_status`, `mail_error`, `mail_status_at`, since 5.8.0) are written
8 * independently of the entity. OptInRepository::update() writes only the
9 * entity's fields, so a later save leaves them untouched.
10 *
11 * "sent" means handed to the mail server (wp_mail returned true). Whether a
12 * mail arrives in an inbox is nothing WordPress can know.
13 *
14 * @package Forge12\DoubleOptIn\Repository
15 * @since 5.8.0
16 */
17
18 declare( strict_types=1 );
19
20 namespace Forge12\DoubleOptIn\Repository;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 class OptInMailStatusRepository {
27
28 public const SENT = 'sent';
29 public const FAILED = 'failed';
30
31 private const ERROR_MAX = 255;
32
33 /** @var \wpdb|object */
34 private $wpdb;
35
36 /** @var string */
37 private $table;
38
39 /** @var callable():int */
40 private $clock;
41
42 /**
43 * @param \wpdb|object $wpdb The database handle.
44 * @param callable|null $clock Returns the current Unix time (test seam).
45 */
46 public function __construct( $wpdb, ?callable $clock = null ) {
47 $this->wpdb = $wpdb;
48 $this->table = $wpdb->prefix . 'f12_cf7_doubleoptin';
49 $this->clock = $clock ?? 'time';
50 }
51
52 /**
53 * Record the outcome of a send attempt.
54 */
55 public function mark( int $optInId, string $status, string $error = '' ): bool {
56 if ( $optInId <= 0 || ! in_array( $status, array( self::SENT, self::FAILED ), true ) ) {
57 return false;
58 }
59 $error = $status === self::FAILED ? self::truncate( $error ) : '';
60
61 $result = $this->wpdb->update(
62 $this->table,
63 array(
64 'mail_status' => $status,
65 'mail_error' => $error,
66 'mail_status_at' => gmdate( 'Y-m-d H:i:s', (int) call_user_func( $this->clock ) ),
67 ),
68 array( 'id' => $optInId ),
69 array( '%s', '%s', '%s' ),
70 array( '%d' )
71 );
72
73 return $result !== false;
74 }
75
76 /**
77 * Stored status of one opt-in.
78 *
79 * @return array{status: string, error: string, at: string}
80 */
81 public function find( int $optInId ): array {
82 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS does not recognise $this->wpdb->prepare(); the table name comes from $wpdb->prefix.
83 $row = $this->wpdb->get_row(
84 $this->wpdb->prepare(
85 "SELECT mail_status, mail_error, mail_status_at FROM {$this->table} WHERE id = %d",
86 $optInId
87 ),
88 ARRAY_A
89 );
90 // phpcs:enable
91 $row = is_array( $row ) ? $row : array();
92
93 return array(
94 'status' => (string) ( $row['mail_status'] ?? '' ),
95 'error' => (string) ( $row['mail_error'] ?? '' ),
96 'at' => (string) ( $row['mail_status_at'] ?? '' ),
97 );
98 }
99
100 /**
101 * Drop the stored error text. It can quote the recipient address, so the
102 * privacy eraser clears it along with the other personal data.
103 */
104 public function clearError( int $optInId ): bool {
105 return $this->wpdb->update(
106 $this->table,
107 array( 'mail_error' => '' ),
108 array( 'id' => $optInId ),
109 array( '%s' ),
110 array( '%d' )
111 ) !== false;
112 }
113
114 /**
115 * Failed sends recorded within the last $seconds.
116 */
117 public function countFailedSince( int $seconds ): int {
118 $since = gmdate( 'Y-m-d H:i:s', (int) call_user_func( $this->clock ) - max( 0, $seconds ) );
119
120 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS does not recognise $this->wpdb->prepare(); the table name comes from $wpdb->prefix.
121 $count = $this->wpdb->get_var(
122 $this->wpdb->prepare(
123 "SELECT COUNT(*) FROM {$this->table} WHERE mail_status = %s AND mail_status_at >= %s",
124 self::FAILED,
125 $since
126 )
127 );
128 // phpcs:enable
129
130 return (int) $count;
131 }
132
133 /**
134 * Failed sends recorded in [$from, $to).
135 *
136 * @since 5.8.0
137 */
138 public function countFailedBetween( int $from, int $to ): int {
139 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS does not recognise $this->wpdb->prepare(); the table name comes from $wpdb->prefix.
140 $count = $this->wpdb->get_var(
141 $this->wpdb->prepare(
142 "SELECT COUNT(*) FROM {$this->table} WHERE mail_status = %s AND mail_status_at >= %s AND mail_status_at < %s",
143 self::FAILED,
144 gmdate( 'Y-m-d H:i:s', $from ),
145 gmdate( 'Y-m-d H:i:s', $to )
146 )
147 );
148 // phpcs:enable
149
150 return (int) $count;
151 }
152
153 /**
154 * Keep an error message short enough for the column, on a character
155 * boundary.
156 */
157 public static function truncate( string $error ): string {
158 $error = trim( preg_replace( '/\s+/', ' ', $error ) ?? '' );
159 if ( function_exists( 'mb_substr' ) ) {
160 return mb_substr( $error, 0, self::ERROR_MAX );
161 }
162 return substr( $error, 0, self::ERROR_MAX );
163 }
164 }
165