PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
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 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
← All changes | core/OptIn.class.php +669 -755 2.135.5.0 View file →
@@ -1,865 +1,779 @@
1 1 <?php
2 +/**
3 + * OptIn Facade
4 + *
5 + * Backward-compatible facade for the legacy OptIn API.
6 + * Internally uses the new Repository pattern.
7 + *
8 + * @package forge12\contactform7\CF7DoubleOptIn
9 + * @since 4.0.0
10 + */
2 11
3 -namespace forge12\contactform7\CF7DoubleOptIn {
4 - if (!defined('ABSPATH')) {
5 - exit;
6 - }
12 +namespace forge12\contactform7\CF7DoubleOptIn;
7 13
8 - /**
9 - * Class Frontend
10 - * Responsible to handle the frontend of the Double OptIn
11 - *
12 - * @package forge12\contactform7\CF7DoubleOptIn
13 - */
14 - class OptIn
15 - {
16 - /**
17 - * Stores the DB ID for the entry
18 - *
19 - * @var int
20 - */
21 - private $id = 0;
14 +use Forge12\DoubleOptIn\Container\Container;
15 +use Forge12\DoubleOptIn\Entity\OptIn as OptInEntity;
16 +use Forge12\DoubleOptIn\Repository\OptInRepositoryInterface;
17 +use Forge12\Shared\Logger;
18 +use Forge12\Shared\LoggerInterface;
22 19
23 - /**
24 - * Stores the ID of the contact form 7 form.
25 - *
26 - * @var int
27 - */
28 - private $cf_form_id = 0;
29 - /**
30 - * Stores if the optin has been confirmed (1) or not (0)
31 - *
32 - * @var int
33 - */
34 - private $doubleoptin = 0;
35 - /**
36 - * Stores the content of the contact form 7 form content as a serialized string.
37 - *
38 - * @var string
39 - */
40 - private $content = "";
41 - /**
42 - * Stores the unique hash used to identify the opt-ins
43 - *
44 - * @var string
45 - */
46 - private $hash = "";
47 - /**
48 - * Stores the timestamp the opt-in mail has been sent to the user.
49 - *
50 - * @var string
51 - */
52 - private $createtime = "";
53 - /**
54 - * Stores the timestamp the opt-in has been confirmed by the user.
55 - */
56 - private $updatetime = "";
57 - /**
58 - * IP Address used to trigger the opt-in mail.
59 - *
60 - * @var string
61 - */
62 - private $ipaddr_register = "";
63 - /**
64 - * IP Address used to confirm the opt-in mail.
65 - */
66 - private $ipaddr_confirmation = "";
67 - /**
68 - * Stores the files used within this form as a serialized string.
69 - */
70 - private $files = "";
71 - /**
72 - * Store the Category ID
73 - */
74 - private $category = 0;
75 - /**
76 - * Stores the form as html
77 - */
78 - private $form = '';
79 - /**
80 - * Stores the Mail send for the optin
81 - *
82 - * @var string
83 - */
84 - private $mail_optin = '';
85 - /**
86 - * Stores the E-Mail
87 - *
88 - * @var string
89 - */
90 - private $email = '';
20 +if ( ! defined( 'ABSPATH' ) ) {
21 + exit;
22 +}
91 23
92 - /**
93 - * The constructor
94 - */
95 - public function __construct(array $properties)
96 - {
97 - foreach ($properties as $name => $value) {
98 - if (isset($this->{$name})) {
99 - $this->{$name} = $value;
100 - }
101 - }
102 - }
24 +/**
25 + * Class OptIn
26 + *
27 + * This class provides backward compatibility with the legacy API
28 + * while using the new Repository pattern internally.
29 + */
30 +class OptIn {
103 31
104 - /**
105 - * @param int $cf_form_id
106 - */
107 - public function set_cf_form_id($cf_form_id)
108 - {
109 - $this->cf_form_id = (int)$cf_form_id;
110 - }
32 + private LoggerInterface $logger;
33 + private OptInEntity $entity;
111 34
112 - /**
113 - * @return int
114 - */
115 - public function get_cf_form_id()
116 - {
117 - return $this->cf_form_id;
118 - }
35 + /**
36 + * Constructor.
37 + *
38 + * @param LoggerInterface $logger The logger instance.
39 + * @param array $properties Optional properties to initialize.
40 + */
41 + public function __construct( LoggerInterface $logger, array $properties = [] ) {
42 + $this->logger = $logger;
119 43
120 - /**
121 - * Return the ID of the Optin
122 - *
123 - * @return int
124 - */
125 - public function get_id()
126 - {
127 - return $this->id;
128 - }
44 + if ( ! empty( $properties ) ) {
45 + $this->entity = OptInEntity::fromArray( $this->mapToEntityArray( $properties ) );
46 + } else {
47 + $this->entity = OptInEntity::create();
48 + }
129 49
130 - /**
131 - * Return the hash of the optin.
132 - *
133 - * @return string
134 - */
135 - public function get_hash()
136 - {
137 - return $this->hash;
138 - }
50 + $this->logger->debug( 'OptIn facade initialized', [
51 + 'plugin' => 'double-opt-in',
52 + 'id' => $this->entity->getId(),
53 + ] );
54 + }
139 55
140 - /**
141 - * Return true|false depending on confirm status
142 - *
143 - * @return bool
144 - */
145 - public function is_confirmed()
146 - {
147 - return (bool)$this->get_doubleoptin();
148 - }
56 + /**
57 + * Get the logger instance.
58 + *
59 + * @return LoggerInterface
60 + */
61 + public function get_logger(): LoggerInterface {
62 + return $this->logger;
63 + }
149 64
150 - /**
151 - * update the optin value
152 - *
153 - * @param int $confirmed
154 - */
155 - public function set_doubleoptin($confirmed)
156 - {
157 - $this->doubleoptin = (int)$confirmed;
158 - }
65 + /**
66 + * Get the underlying entity.
67 + *
68 + * @return OptInEntity
69 + */
70 + public function getEntity(): OptInEntity {
71 + return $this->entity;
72 + }
159 73
160 - /**
161 - * Return the optin value for the form.
162 - *
163 - * @return int
164 - */
165 - public function get_doubleoptin()
166 - {
167 - return $this->doubleoptin;
168 - }
74 + // =========================================================================
75 + // STATIC FACTORY METHODS
76 + // =========================================================================
169 77
170 - /**
171 - * @param string $timestamp
172 - */
173 - public function set_createtime($timestamp)
174 - {
175 - $this->createtime = $timestamp;
176 - }
78 + /**
79 + * Get OptIn by hash.
80 + *
81 + * @param string $hash The hash.
82 + *
83 + * @return OptIn|null
84 + */
85 + public static function get_by_hash( string $hash ): ?OptIn {
86 + $logger = Logger::getInstance();
87 + $logger->debug( 'get_by_hash called', [
88 + 'plugin' => 'double-opt-in',
89 + 'hash' => $hash,
90 + ] );
177 91
178 - /**
179 - * Return the Date how long the opt in will be valid
180 - * @return string
181 - */
182 - public function get_valid_until(){
183 - $settings = CF7DoubleOptIn::getInstance()->getSettings();
92 + try {
93 + $repository = self::getRepository();
94 + $entity = $repository->findByHash( $hash );
184 95
185 - $createtime = $this->get_createtime();
186 - $dt = new \DateTime();
187 - $dt->setTimestamp($createtime);
96 + if ( ! $entity ) {
97 + return null;
98 + }
188 99
189 - if($this->is_confirmed()){
190 - $month = (int)$settings['delete'];
191 - $period = $settings['delete_period'];
192 - }else{
193 - $month = (int)$settings['delete_unconfirmed'];
194 - $period = $settings['delete_unconfirmed_period'];
195 - }
100 + $optIn = new self( $logger );
101 + $optIn->entity = $entity;
196 102
197 - $dt->modify('+'.(int)$month.' '.$period);
198 - $dt->modify('+1 day');
103 + return $optIn;
104 + } catch ( \Exception $e ) {
105 + $logger->error( 'Failed to get OptIn by hash', [
106 + 'plugin' => 'double-opt-in',
107 + 'hash' => $hash,
108 + 'error' => $e->getMessage(),
109 + ] );
110 + return null;
111 + }
112 + }
199 113
200 - return $dt->format('d.m.Y');
201 - }
114 + /**
115 + * Get count by form ID.
116 + *
117 + * @param int $formId The form ID.
118 + *
119 + * @return int
120 + */
121 + public static function get_count( int $formId ): int {
122 + try {
123 + return self::getRepository()->countByFormId( $formId );
124 + } catch ( \Exception $e ) {
125 + return 0;
126 + }
127 + }
202 128
203 - /**
204 - * Return a timestamp
205 - *
206 - * @param string $view Select how to return the content. raw is the stored db value. use formatted to return a
207 - * formatted string.
208 - *
209 - * @return string
210 - */
211 - public function get_createtime($view = "raw")
212 - {
213 - if ($view != "raw") {
214 - if (!is_numeric($this->createtime)) {
215 - $date = date('d.m.Y', strtotime($this->createtime));
216 - $time = date('H:i:s', strtotime($this->createtime));
217 - } else {
218 - $date = date('d.m.Y', $this->createtime);
219 - $time = date('H:i:s', $this->createtime);
220 - }
129 + /**
130 + * Get list of OptIns with pagination.
131 + *
132 + * @param array $atts Query attributes.
133 + * @param int|null $numberOfPages Reference to store page count.
134 + *
135 + * @return array<OptIn>
136 + */
137 + public static function get_list( array $atts = [], ?int &$numberOfPages = null ): array {
138 + $logger = Logger::getInstance();
221 139
222 - return $date . ' ' . __('/', 'double-opt-in') . ' ' . $time;
223 - } else {
224 - return $this->createtime;
225 - }
226 - }
140 + try {
141 + $repository = self::getRepository();
142 + $entities = $repository->findAll( $atts, $numberOfPages );
227 143
228 - /**
229 - * @param string $timestamp
230 - */
231 - public function set_updatetime($timestamp)
232 - {
233 - $this->updatetime = $timestamp;
234 - }
144 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
145 + $optIn = new self( $logger );
146 + $optIn->entity = $entity;
147 + return $optIn;
148 + }, $entities );
149 + } catch ( \Exception $e ) {
150 + $logger->error( 'Failed to get OptIn list', [
151 + 'plugin' => 'double-opt-in',
152 + 'error' => $e->getMessage(),
153 + ] );
154 + return [];
155 + }
156 + }
235 157
236 - /**
237 - * Return a timestamp
238 - *
239 - * @param string $view Select how to return the content. raw is the stored db value. use formatted to return a
240 - * formatted string.
241 - *
242 - * @return string
243 - */
244 - public function get_updatetime($view = 'raw')
245 - {
246 - if ($view != "raw") {
247 - if (!is_numeric($this->updatetime)) {
248 - $date = date('d.m.Y', strtotime($this->updatetime));
249 - $time = date('H:i:s', strtotime($this->updatetime));
250 - } else {
251 - $date = date('d.m.Y', $this->updatetime);
252 - $time = date('H:i:s', $this->updatetime);
253 - }
254 - return $date . ' ' . __('/', 'double-opt-in') . ' ' . $time;
255 - } else {
256 - return $this->updatetime;
257 - }
258 - }
158 + /**
159 + * Get list by category ID.
160 + *
161 + * @param int $categoryId The category ID.
162 + * @param array $atts Query attributes.
163 + * @param int|null $numberOfPages Reference to store page count.
164 + *
165 + * @return array<OptIn>
166 + */
167 + public static function get_list_by_category_id( int $categoryId, array $atts = [], ?int &$numberOfPages = null ): array {
168 + $logger = Logger::getInstance();
259 169
260 - /**
261 - * @param string $ip_addr
262 - */
263 - public function set_ipaddr_register($ip_addr)
264 - {
265 - $this->ipaddr_register = $ip_addr;
266 - }
170 + try {
171 + $repository = self::getRepository();
267 172
268 - /**
269 - * @return string
270 - */
271 - public function get_ipaddr_register()
272 - {
273 - return $this->ipaddr_register;
274 - }
173 + // Get total count for pagination
174 + if ( $numberOfPages !== null ) {
175 + $keyword = $atts['keyword'] ?? '';
176 + $perPage = max( 1, (int) ( $atts['perPage'] ?? 10 ) );
177 + $total = $repository->countByCategory( $categoryId, $keyword );
178 + $numberOfPages = $total > 0 ? (int) ceil( $total / $perPage ) : 0;
275 179
276 - /**
277 - * @param string $ip_addr
278 - */
279 - public function set_ipaddr_confirmation($ip_addr)
280 - {
281 - $this->ipaddr_confirmation = $ip_addr;
282 - }
180 + if ( $numberOfPages === 0 ) {
181 + return [];
182 + }
183 + }
283 184
284 - /**
285 - * @return string
286 - */
287 - public function get_ipaddr_confirmation()
288 - {
289 - return $this->ipaddr_confirmation;
290 - }
185 + $entities = $repository->findByCategory( $categoryId, $atts );
291 186
292 - /**
293 - * Return a serialized string
294 - *
295 - * @return mixed
296 - */
297 - public function get_content()
298 - {
299 - return $this->content;
300 - }
187 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
188 + $optIn = new self( $logger );
189 + $optIn->entity = $entity;
190 + return $optIn;
191 + }, $entities );
192 + } catch ( \Exception $e ) {
193 + $logger->error( 'Failed to get OptIn list by category', [
194 + 'plugin' => 'double-opt-in',
195 + 'category' => $categoryId,
196 + 'error' => $e->getMessage(),
197 + ] );
198 + return [];
199 + }
200 + }
301 201
302 - /**
303 - * @param string - a serialized string
304 - */
305 - public function set_content($content)
306 - {
307 - $this->content = $content;
308 - }
202 + /**
203 + * Get list by email.
204 + *
205 + * @param string $email The email address.
206 + *
207 + * @return array<OptIn>
208 + */
209 + public static function get_list_by_email( string $email ): array {
210 + $logger = Logger::getInstance();
309 211
310 - /**
311 - * @param string - a serialized string
312 - */
313 - public function set_files($files)
314 - {
315 - $this->files = $files;
316 - }
212 + try {
213 + $repository = self::getRepository();
214 + $entities = $repository->findByEmail( $email );
317 215
318 - /**
319 - * @return string use maybe_unserialize to deserialize the string
320 - */
321 - public function get_files()
322 - {
323 - return $this->files;
324 - }
216 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
217 + $optIn = new self( $logger );
218 + $optIn->entity = $entity;
219 + return $optIn;
220 + }, $entities );
221 + } catch ( \Exception $e ) {
222 + return [];
223 + }
224 + }
325 225
326 - /**
327 - * Return the assigned Category ID
328 - *
329 - * @return int
330 - */
331 - public function get_category()
332 - {
333 - if (!is_numeric($this->category)) {
334 - return 0;
335 - }
336 - return $this->category;
337 - }
226 + /**
227 + * Get confirmed list by email.
228 + *
229 + * @param string $email The email address.
230 + *
231 + * @return array<OptIn>
232 + */
233 + public static function get_list_by_email_confirmed( string $email ): array {
234 + $logger = Logger::getInstance();
338 235
339 - /**
340 - * Assign a ID to the category
341 - *
342 - * @param int $id
343 - *
344 - * @return void
345 - */
346 - public function set_category($id)
347 - {
348 - $this->category = $id;
349 - }
236 + try {
237 + $repository = self::getRepository();
238 + $entities = $repository->findConfirmedByEmail( $email );
350 239
351 - /**
352 - * Check if the given Opt In was created by Contact Form 7
353 - *
354 - * @return bool
355 - */
356 - public function isTypeCF7()
357 - {
358 - $form_id = $this->get_cf_form_id();
359 - if (class_exists('\WPCF7_ContactForm')) {
360 - $form = \WPCF7_ContactForm::get_instance($form_id);
240 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
241 + $optIn = new self( $logger );
242 + $optIn->entity = $entity;
243 + return $optIn;
244 + }, $entities );
245 + } catch ( \Exception $e ) {
246 + return [];
247 + }
248 + }
361 249
362 - if ($form) {
363 - return true;
364 - }
365 - }
366 - return false;
367 - }
250 + /**
251 + * Get unconfirmed list by email.
252 + *
253 + * @param string $email The email address.
254 + *
255 + * @return array<OptIn>
256 + */
257 + public static function get_list_by_email_unconfirmed( string $email ): array {
258 + $logger = Logger::getInstance();
368 259
369 - /**
370 - * Check if the given Opt In was created by Avada
371 - *
372 - * @return bool|void
373 - */
374 - public function isTypeAvada()
375 - {
376 - $form_id = $this->get_cf_form_id();
377 - if (function_exists('Avada')) {
378 - $form = get_post($form_id);
379 - if ($form) {
380 - return true;
381 - }
382 - }
383 - }
260 + try {
261 + $repository = self::getRepository();
262 + $entities = $repository->findUnconfirmedByEmail( $email );
384 263
385 - /**
386 - * Check if the Opt In was created by 'cf7' or 'avada'.
387 - *
388 - * @param string $type use either 'cf7' or 'avada'
389 - *
390 - * @return bool
391 - */
392 - public function isType($type)
393 - {
394 - if ($type == 'cf7') {
395 - return $this->isTypeCF7();
396 - } else {
397 - return (!$this->isTypeCF7() && $this->isTypeAvada());
398 - }
399 - }
264 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
265 + $optIn = new self( $logger );
266 + $optIn->entity = $entity;
267 + return $optIn;
268 + }, $entities );
269 + } catch ( \Exception $e ) {
270 + return [];
271 + }
272 + }
400 273
401 - /**
402 - * Load an OptIn by the given hash code. Returns either an Object of Type OptIn or null if not found.
403 - *
404 - * @param string $hash
405 - *
406 - * @return OptIn|null
407 - */
408 - public static function get_by_hash(string $hash)
409 - {
410 - global $wpdb;
411 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
274 + /**
275 + * Bulk update category.
276 + *
277 + * @param int $fromId The source category ID.
278 + * @param int $toId The target category ID.
279 + *
280 + * @return int
281 + */
282 + public static function bulk_update_category( int $fromId, int $toId ): int {
283 + try {
284 + return self::getRepository()->bulkUpdateCategory( $fromId, $toId );
285 + } catch ( \Exception $e ) {
286 + return 0;
287 + }
288 + }
412 289
413 - if (null == $wpdb) {
414 - return null;
415 - }
290 + /**
291 + * Update category by OptIn ID.
292 + *
293 + * @param int $optInId The OptIn ID.
294 + * @param int $categoryId The new category ID.
295 + *
296 + * @return bool
297 + */
298 + public static function update_category_by_id( int $optInId, int $categoryId ): bool {
299 + try {
300 + return self::getRepository()->updateCategoryById( $optInId, $categoryId );
301 + } catch ( \Exception $e ) {
302 + return false;
303 + }
304 + }
416 305
417 - $row = $wpdb->get_row('SELECT * FROM ' . $table . ' WHERE hash="' . $hash . '"', ARRAY_A);
306 + // =========================================================================
307 + // GETTERS
308 + // =========================================================================
418 309
419 - if (null == $row) {
420 - return null;
421 - }
310 + public function get_id(): int {
311 + return $this->entity->getId();
312 + }
422 313
423 - return new OptIn($row);
424 - }
314 + public function get_hash(): string {
315 + return $this->entity->getHash();
316 + }
425 317
426 - /**
427 - * Get number of optins for the given post.
428 - *
429 - * @param $post_id
430 - *
431 - * @return int
432 - */
433 - public static function get_count($post_id)
434 - {
435 - global $wpdb;
318 + public function get_cf_form_id(): int {
319 + return $this->entity->getFormId();
320 + }
436 321
437 - if (!$wpdb) {
438 - return 0;
439 - }
322 + public function is_confirmed(): bool {
323 + return $this->entity->isConfirmed();
324 + }
440 325
441 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
442 - $result = $wpdb->get_results($wpdb->prepare('SELECT count(*) AS counter FROM ' . $tableName . ' WHERE cf_form_id=%d ', $post_id));
326 + public function get_doubleoptin(): int {
327 + return $this->entity->isConfirmed() ? 1 : 0;
328 + }
443 329
444 - if (is_array($result)) {
445 - return $result[0]->counter;
446 - }
447 - return 0;
448 - }
330 + public function is_optout(): bool {
331 + return $this->entity->isOptedOut();
332 + }
449 333
450 - /**
451 - * Return a list of opt ins stored in the database.
452 - *
453 - * @param int $category_id
454 - *
455 - * @param array $atts array (
456 - * 'perPage' => 10, // Posts per page
457 - * 'page' => 0, // Current page
458 - * 'order' => DESC, // Order (ASC/DESC)
459 - * );
460 - *
461 - * @param null $numberOfPages - Stores the number of pages found if limited
462 - *
463 - * @return array
464 - */
465 - public static function get_list_by_category_id($category_id, $atts = array(), &$numberOfPages = null)
466 - {
467 - global $wpdb;
334 + public function get_content(): string {
335 + return $this->entity->getContent();
336 + }
468 337
469 - $attr = array(
470 - 'perPage' => 10,
471 - 'page' => 1,
472 - 'order' => 'DESC',
473 - 'keyword' => ''
474 - );
338 + public function get_createtime( string $view = 'raw' ): string {
339 + if ( $view !== 'raw' ) {
340 + return $this->entity->getCreateTimeFormatted( 'd.m.Y / H:i:s' );
341 + }
342 + return (string) $this->entity->getCreateTime();
343 + }
475 344
476 - /**
477 - * Parameter
478 - */
479 - foreach ($atts as $key => $value) {
480 - if (isset($attr[$key])) {
481 - $attr[$key] = $atts[$key];
482 - }
483 - }
345 + public function get_updatetime( string $view = 'raw' ): string {
346 + if ( $view !== 'raw' ) {
347 + return $this->entity->getUpdateTimeFormatted( 'd.m.Y / H:i:s' );
348 + }
349 + return (string) $this->entity->getUpdateTime();
350 + }
484 351
485 - /*
486 - * Update keyword
487 - */
488 - $where = '';
489 - if (!empty($attr['keyword'])) {
490 - $where = ' AND content LIKE "%%' . $attr['keyword'] . '%%"';
491 - }
352 + public function get_optouttime( string $view = 'raw' ): string {
353 + $time = $this->entity->getOptOutTime();
354 + if ( $view !== 'raw' && $time > 0 ) {
355 + return wp_date( 'd.m.Y / H:i:s', $time );
356 + }
357 + return (string) $time;
358 + }
492 359
360 + /**
361 + * Get create time as ISO 8601 string.
362 + *
363 + * @return string
364 + */
365 + public function get_createtime_iso(): string {
366 + return $this->entity->getCreateTimeISO();
367 + }
493 368
494 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
495 - $pageNum = (int)$attr['page'] - 1;
369 + /**
370 + * Get update time as ISO 8601 string.
371 + *
372 + * @return string
373 + */
374 + public function get_updatetime_iso(): string {
375 + return $this->entity->getUpdateTimeISO();
376 + }
496 377
497 - if ($numberOfPages !== null) {
498 - $result = $wpdb->get_results($wpdb->prepare('SELECT count(*) AS counter FROM ' . $tableName . ' WHERE category=%d ' . $where, $category_id));
378 + /**
379 + * Get opt-out time as ISO 8601 string.
380 + *
381 + * @return string
382 + */
383 + public function get_optouttime_iso(): string {
384 + return $this->entity->getOptOutTimeISO();
385 + }
499 386
500 - $itemCounter = 0;
387 + public function get_ipaddr_register(): string {
388 + return $this->entity->getIpRegister();
389 + }
501 390
502 - if (is_array($result)) {
503 - $itemCounter = $result[0]->counter;
504 - }
391 + public function get_ipaddr_confirmation(): string {
392 + return $this->entity->getIpConfirmation();
393 + }
505 394
506 - $numberOfPages = 0;
395 + public function get_ipaddr_optout(): string {
396 + return $this->entity->getIpOptOut();
397 + }
507 398
508 - if ($itemCounter != 0) {
509 - $numberOfPages = ceil($itemCounter / (int)$attr['perPage']);
510 - }
511 - }
399 + public function get_files(): string {
400 + return $this->entity->getFiles();
401 + }
512 402
513 - $list = array();
403 + public function get_category(): int {
404 + return $this->entity->getCategory();
405 + }
514 406
515 - if ($numberOfPages == 0) {
516 - return $list;
517 - }
407 + public function get_email(): string {
408 + return $this->entity->getEmail();
409 + }
518 410
519 - $offset = $pageNum * (int)$attr['perPage'];
411 + public function get_mail_optin(): string {
412 + return $this->entity->getMailOptIn();
413 + }
520 414
521 - $rows = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $tableName . ' WHERE category=%d ' . $where . ' ORDER BY ID ' . $attr['order'] . ' LIMIT ' . $attr['perPage'] . ' OFFSET %d', $category_id, $offset), ARRAY_A);
415 + public function get_consent_text(): string {
416 + return $this->entity->getConsentText();
417 + }
522 418
523 - foreach ($rows as $row) {
524 - $list[] = new OptIn($row);
525 - }
526 - return $list;
527 - }
419 + public function get_form( bool $encoded = false ): string {
420 + $form = $this->entity->getForm();
421 + if ( $encoded ) {
422 + return base64_encode( $form );
423 + }
424 + return $form;
425 + }
528 426
529 - /**
530 - * Return a list of opt ins stored in the database.
531 - *
532 - * @param string $email
533 - * @param int $confirmed Either recieve confirmed or unconfirmed only
534 - *
535 - * @return array
536 - */
537 - public static function get_list_by_email($email)
538 - {
539 - global $wpdb;
427 + /**
428 + * Get the validity end date.
429 + *
430 + * @return string
431 + */
432 + public function get_valid_until(): string {
433 + $settings = CF7DoubleOptIn::getInstance()->getSettings();
434 + $dt = new \DateTime();
435 + $dt->setTimestamp( $this->entity->getCreateTime() );
540 436
541 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
437 + if ( $this->is_confirmed() ) {
438 + $amount = (int) ( $settings['delete'] ?? 0 );
439 + $period = $settings['delete_period'] ?? 'months';
440 + } else {
441 + $amount = (int) ( $settings['delete_unconfirmed'] ?? 0 );
442 + $period = $settings['delete_unconfirmed_period'] ?? 'months';
443 + }
542 444
543 - $list = array();
445 + $dt->modify( '+' . $amount . ' ' . $period );
446 + $dt->modify( '+1 day' );
544 447
545 - $rows = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $tableName . ' WHERE email=%s', $email), ARRAY_A);
448 + return $dt->format( 'd.m.Y' );
449 + }
546 450
547 - foreach ($rows as $row) {
548 - $list[] = new OptIn($row);
549 - }
550 - return $list;
551 - }
451 + // =========================================================================
452 + // SETTERS
453 + // =========================================================================
552 454
553 - /**
554 - * Return a list of opt ins stored in the database.
555 - *
556 - * @param $atts array (
557 - * 'perPage' => 10, // Posts per page
558 - * 'page' => 0, // Current page
559 - * 'order' => DESC, // Order (ASC/DESC)
560 - * );
561 - *
562 - * @param null $numberOfPages - Stores the number of pages found if limited
563 - *
564 - * @return array
565 - */
566 - public static function get_list($atts = array(), &$numberOfPages = null)
567 - {
568 - global $wpdb;
455 + public function set_cf_form_id( int $formId ): void {
456 + $this->entity = $this->entity->withFormId( $formId );
457 + }
569 458
570 - $attr = array(
571 - 'perPage' => 10,
572 - 'page' => 1,
573 - 'order' => 'DESC',
574 - 'keyword' => '',
575 - 'cf_form_id' => '',
576 - );
459 + public function set_doubleoptin( int $confirmed ): void {
460 + $this->entity->setConfirmed( (bool) $confirmed );
461 + }
577 462
578 - /*
579 - * Update atts
580 - */
581 - foreach ($atts as $key => $value) {
582 - if (isset($attr[$key])) {
583 - $attr[$key] = $atts[$key];
584 - }
585 - }
463 + public function set_createtime( $timestamp ): void {
464 + $this->entity = $this->entity->withCreateTime( (int) $timestamp );
465 + }
586 466
587 - /*
588 - * Update WHERE for Query
589 - */
590 - $where = '';
591 - $where_condition = [];
467 + public function set_updatetime( $timestamp ): void {
468 + $this->entity->setUpdateTime( (int) $timestamp );
469 + }
592 470
593 - /*
594 - * Add Keyword
595 - */
596 - if (!empty($attr['keyword'])) {
597 - $where_condition[] = 'content LIKE "%%' . $attr['keyword'] . '%%"';
598 - }
471 + public function set_optouttime( $timestamp ): void {
472 + $this->entity = $this->entity->withOptOutTime( (int) $timestamp );
473 + }
599 474
600 - /*
601 - * Add Form ID
602 - */
603 - if (!empty($attr['cf_form_id'])) {
604 - $where_condition[] = 'cf_form_id="' . $attr['cf_form_id'] . '"';
605 - }
475 + public function set_ipaddr_register( string $ip ): void {
476 + $this->entity = $this->entity->withIpRegister( $ip );
477 + }
606 478
607 - /*
608 - * build where string
609 - */
610 - if (!empty($where_condition)) {
611 - $where = ' WHERE ' . implode(' AND ', $where_condition);
612 - }
479 + public function set_ipaddr_confirmation( string $ip ): void {
480 + $this->entity->setIpConfirmation( $ip );
481 + }
613 482
614 - /*
615 - * set table
616 - */
617 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
618 - $pageNum = (int)$attr['page'] - 1;
483 + public function set_ipaddr_optout( string $ip ): void {
484 + $this->entity = $this->entity->withIpOptOut( $ip );
485 + }
619 486
620 - if ($numberOfPages !== null) {
621 - $result = $wpdb->get_results('SELECT count(*) as counter FROM ' . $tableName . $where);
487 + public function set_content( string $content ): void {
488 + $this->entity = $this->entity->withContent( $content );
489 + }
622 490
623 - $itemCounter = 0;
491 + public function set_files( string $files ): void {
492 + $this->entity = $this->entity->withFiles( $files );
493 + }
624 494
625 - if (is_array($result)) {
626 - $itemCounter = $result[0]->counter;
627 - }
495 + public function set_category( int $categoryId ): void {
496 + $this->entity = $this->entity->withCategory( $categoryId );
497 + }
628 498
629 - $numberOfPages = 0;
499 + public function set_form( string $form ): void {
500 + $this->entity = $this->entity->withForm( $form );
501 + }
630 502
631 - if ($itemCounter != 0) {
632 - $numberOfPages = ceil($itemCounter / (int)$attr['perPage']);
633 - }
634 - }
503 + public function set_email( string $email ): void {
504 + $this->entity = $this->entity->withEmail( $email );
505 + }
635 506
636 - $list = array();
507 + public function set_mail_optin( string $mail ): void {
508 + $this->entity = $this->entity->withMailOptIn( $mail );
509 + }
637 510
638 - if ($numberOfPages == 0) {
639 - return $list;
640 - }
511 + public function set_consent_text( string $text ): void {
512 + $this->entity = $this->entity->withConsentText( $text );
513 + }
641 514
642 - $offset = $pageNum * (int)$attr['perPage'];
515 + // =========================================================================
516 + // TYPE CHECKS
517 + // =========================================================================
643 518
644 - $rows = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $tableName . $where . ' ORDER BY ID ' . $attr['order'] . ' LIMIT ' . $attr['perPage'] . ' OFFSET % d', $offset), ARRAY_A);
519 + public function isTypeCF7(): bool {
520 + return $this->isType( 'cf7' );
521 + }
645 522
646 - foreach ($rows as $row) {
647 - $list[] = new OptIn($row);
648 - }
649 - return $list;
650 - }
523 + public function isTypeAvada(): bool {
524 + return $this->isType( 'avada' );
525 + }
651 526
652 - /**
653 - * This will update all categories from A to B, e.g.: All Opt-Ins with id 1 to category id 2.
654 - *
655 - * @param int $from_id The origin Category ID
656 - * @param int $to_id The new Category ID
657 - *
658 - * @return bool|int|\mysqli_result|resource|null Return the number of rows affected by the query.
659 - */
660 - public static function bulk_update_category($from_id, $to_id)
661 - {
662 - global $wpdb;
663 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
527 + public function isTypeElementor(): bool {
528 + return $this->isType( 'elementor' );
529 + }
664 530
665 - return $wpdb->query($wpdb->prepare('UPDATE ' . $table . ' SET category =%d WHERE category =%d', $to_id, $from_id));
666 - }
531 + public function isTypeWPForms(): bool {
532 + return $this->isType( 'wpforms' );
533 + }
667 534
668 - /**
669 - * Update the category of the given opt in id.
670 - *
671 - * @param int $optin_id The ID of the OptIn
672 - * @param int $category_id The ID of the Category
673 - *
674 - * @return bool|int|\mysqli_result|resource|null
675 - */
676 - public static function update_category_by_id($optin_id, $category_id)
677 - {
678 - global $wpdb;
679 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
535 + public function isTypeGravityForms(): bool {
536 + return $this->isType( 'gravityforms' );
537 + }
680 538
681 - return $wpdb->query($wpdb->prepare('UPDATE ' . $table . ' SET category =%d WHERE id =%d', $category_id, $optin_id));
682 - }
539 + public function isType( string $type ): bool {
540 + $formId = $this->get_cf_form_id();
683 541
684 - /**
685 - * Update or create the given Object
686 - */
687 - public function save()
688 - {
689 - global $wpdb;
690 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
542 + switch ( $type ) {
543 + case 'cf7':
544 + return get_post_type( $formId ) === 'wpcf7_contact_form';
545 + case 'avada':
546 + return get_post_type( $formId ) === 'fusion_form';
547 + case 'elementor':
548 + // Elementor forms are embedded in pages/posts, not stored as separate post types.
549 + // Check if the post has Elementor data AND DOI settings configured.
550 + $hasElementorData = ! empty( get_post_meta( $formId, '_elementor_data', true ) );
551 + $hasDoiSettings = ! empty( get_post_meta( $formId, 'f12-cf7-doubleoptin', true ) );
552 + return $hasElementorData && $hasDoiSettings;
553 + case 'wpforms':
554 + return get_post_type( $formId ) === 'wpforms';
555 + case 'gravityforms':
556 + // Gravity Forms stores forms in a custom table, not as posts.
557 + // Check if the form ID exists in the GF forms table.
558 + if ( class_exists( 'GFAPI' ) ) {
559 + $form = \GFAPI::get_form( $formId );
560 + return $form !== false && ! is_wp_error( $form );
561 + }
562 + return false;
563 + default:
564 + return false;
565 + }
566 + }
691 567
692 - if (!empty($this->get_hash())) {
693 - return $wpdb->update($table, array(
694 - 'doubleoptin' => $this->get_doubleoptin(),
695 - 'createtime' => $this->get_createtime(),
696 - 'updatetime' => $this->get_updatetime(),
697 - 'ipaddr_confirmation' => $this->get_ipaddr_confirmation(),
698 - 'ipaddr_register' => $this->get_ipaddr_register(),
699 - 'cf_form_id' => $this->get_cf_form_id(),
700 - 'content' => $this->get_content(),
701 - 'files' => $this->get_files(),
702 - 'category' => $this->get_category(),
703 - 'mail_optin' => $this->get_mail_optin(),
704 - ), array(
705 - 'hash' => $this->get_hash()
706 - ));
707 - } else {
708 - $result = $wpdb->insert($table, array(
709 - 'cf_form_id' => $this->get_cf_form_id(),
710 - 'doubleoptin' => $this->get_doubleoptin(),
711 - 'createtime' => $this->get_createtime(),
712 - 'updatetime' => $this->get_updatetime(),
713 - 'ipaddr_confirmation' => $this->get_ipaddr_confirmation(),
714 - 'ipaddr_register' => $this->get_ipaddr_register(),
715 - 'content' => $this->get_content(),
716 - 'files' => $this->get_files(),
717 - 'category' => $this->get_category(),
718 - 'form' => $this->get_form(),
719 - 'mail_optin' => $this->get_mail_optin(),
720 - 'email' => $this->get_email()
721 - ));
568 + // =========================================================================
569 + // PERSISTENCE
570 + // =========================================================================
722 571
723 - if ($result > 0) {
724 - $this->id = $wpdb->insert_id;
725 - return $this->createHash();
726 - }
727 - }
728 - return false;
729 - }
572 + /**
573 + * Save the OptIn to the database.
574 + *
575 + * @return bool|int
576 + */
577 + public function save() {
578 + $this->logger->info( 'Saving OptIn', [
579 + 'plugin' => 'double-opt-in',
580 + 'id' => $this->entity->getId(),
581 + 'hash' => $this->entity->getHash(),
582 + ] );
730 583
731 - /**
732 - * Creates a unique hash code and adds it to the current OptIn Object
733 - */
734 - private function createHash()
735 - {
736 - global $wpdb;
584 + try {
585 + $repository = self::getRepository();
586 + $this->entity = $repository->save( $this->entity );
737 587
738 - if (null == $wpdb) {
739 - return false;
740 - }
588 + $this->logger->info( 'OptIn saved successfully', [
589 + 'plugin' => 'double-opt-in',
590 + 'id' => $this->entity->getId(),
591 + 'hash' => $this->entity->getHash(),
592 + ] );
741 593
742 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
594 + return true;
595 + } catch ( \Exception $e ) {
596 + $this->logger->error( 'Failed to save OptIn', [
597 + 'plugin' => 'double-opt-in',
598 + 'error' => $e->getMessage(),
599 + ] );
600 + return false;
601 + }
602 + }
743 603
744 - // add the hash to the element
604 + // =========================================================================
605 + // LINK GENERATION
606 + // =========================================================================
745 607
746 - $this->hash = base64_encode($this->get_createtime() . $this->get_id() . $this->get_cf_form_id());
608 + /**
609 + * Get the opt-in confirmation link.
610 + *
611 + * @param array $parameter Additional parameters.
612 + * @param int $formId Optional form ID override.
613 + *
614 + * @return string
615 + */
616 + public function get_link_optin( array $parameter = [], int $formId = 0 ): string {
617 + $formId = $formId > 0 ? $formId : $this->get_cf_form_id();
747 618
748 - return $wpdb->update($table, array(
749 - 'hash' => $this->hash,
750 - ), array(
751 - 'id' => $this->get_id()
752 - ));
753 - }
619 + $formParameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
620 + $pageId = (int) ( $formParameter['page'] ?? 0 );
754 621
755 - /**
756 - * Return the Form
757 - *
758 - * @return string
759 - */
760 - public function get_form()
761 - {
762 - return $this->form;
763 - }
622 + if ( $pageId <= 0 ) {
623 + return home_url( '?optin=' . $this->get_hash() );
624 + }
764 625
765 - /**
766 - * Set the Form as HTML
767 - *
768 - * @param string $form
769 - *
770 - * @return void
771 - */
772 - public function set_form($form)
773 - {
774 - $this->form = $form;
775 - }
626 + $pageUrl = get_permalink( $pageId );
627 + if ( ! $pageUrl ) {
628 + return home_url( '?optin=' . $this->get_hash() );
629 + }
776 630
777 - /**
778 - * Return the Form
779 - *
780 - * @return string
781 - */
782 - public function get_mail_optin()
783 - {
784 - return $this->mail_optin;
785 - }
631 + $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
786 632
787 - /**
788 - * Return the E-mail
789 - *
790 - * @return string
791 - */
792 - public function get_email()
793 - {
794 - return $this->email;
795 - }
633 + return $pageUrl . $separator . 'optin=' . $this->get_hash();
634 + }
796 635
797 - /**
798 - * Store the email
799 - *
800 - * @param string $email
801 - *
802 - * @return void
803 - */
804 - public function set_email($email)
805 - {
806 - $this->email = $email;
807 - }
636 + /**
637 + * Get the opt-out link.
638 + *
639 + * @return string
640 + */
641 + public function get_link_optout(): string {
642 + $settings = CF7DoubleOptIn::getInstance()->getSettings();
643 + $pageId = (int) ( $settings['optout_page'] ?? 0 );
808 644
809 - /**
810 - * Set the Form as HTML
811 - *
812 - * @param string $form
813 - *
814 - * @return void
815 - */
816 - public function set_mail_optin($mail_html)
817 - {
818 - $this->mail_optin = $mail_html;
819 - }
645 + if ( $pageId <= 0 ) {
646 + return home_url( '?optout=' . $this->get_hash() );
647 + }
820 648
821 - /**
822 - * Return the link to the detail view
823 - *
824 - * @return string
825 - */
826 - public function get_link_ui()
827 - {
828 - return admin_url('admin.php?page=f12-cf7-doubleoptin_optin_view&hash=' . $this->get_hash());
829 - }
649 + $pageUrl = get_permalink( $pageId );
650 + if ( ! $pageUrl ) {
651 + return home_url( '?optout=' . $this->get_hash() );
652 + }
830 653
831 - /**
832 - * Return the link to delete
833 - *
834 - * @return string
835 - */
836 - public function get_link_delete()
837 - {
838 - return admin_url('admin.php?page=f12-cf7-doubleoptin&option=delete&hash=' . $this->get_hash());
839 - }
654 + $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
840 655
841 - /**
842 - * Return the link to the optin
843 - *
844 - * @return string
845 - */
846 - public function get_link_optin()
847 - {
848 - if ($this->get_cf_form_id() == 0) {
849 - return get_home_url() . '?optin=' . $this->get_hash();
850 - }
656 + return $pageUrl . $separator . 'optout=' . $this->get_hash();
657 + }
851 658
852 - $parameter = CF7DoubleOptIn::getInstance()->getParameter($this->get_cf_form_id());
659 + /**
660 + * Get the UI edit link.
661 + *
662 + * @return string
663 + */
664 + public function get_link_ui(): string {
665 + return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&view=single&hash=' . $this->get_hash() );
666 + }
853 667
854 - $page_id = $parameter['page'];
668 + /**
669 + * Get the delete link.
670 + *
671 + * @return string
672 + */
673 + public function get_link_delete(): string {
674 + $nonce = wp_create_nonce( 'delete_optin_' . $this->get_hash() );
675 + return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&action=delete&hash=' . $this->get_hash() . '&_wpnonce=' . $nonce );
676 + }
855 677
856 - if ($page_id == -1 || !$page_id || $page_id == 0) {
857 - $link = get_home_url() . '?optin=' . $this->get_hash();
858 - } else {
859 - $link = get_permalink($page_id) . '?optin=' . $this->get_hash();
860 - }
678 + /**
679 + * Get the form name.
680 + *
681 + * @return string
682 + */
683 + public function get_form_name(): string {
684 + $formId = $this->get_cf_form_id();
861 685
862 - return $link;
863 - }
864 - }
865 -}
686 + if ( $formId <= 0 ) {
687 + return __( 'Unknown', 'double-opt-in' );
688 + }
689 +
690 + $post = get_post( $formId );
691 + if ( ! $post ) {
692 + return __( 'Deleted Form', 'double-opt-in' ) . ' (ID: ' . $formId . ')';
693 + }
694 +
695 + return $post->post_title ?: __( 'Untitled Form', 'double-opt-in' );
696 + }
697 +
698 + /**
699 + * Get the form edit link.
700 + *
701 + * @return string
702 + */
703 + public function get_form_link(): string {
704 + $formId = $this->get_cf_form_id();
705 + $postType = get_post_type( $formId );
706 +
707 + if ( $postType === 'wpcf7_contact_form' ) {
708 + return admin_url( 'admin.php?page=wpcf7&post=' . $formId . '&action=edit' );
709 + }
710 +
711 + if ( $postType === 'fusion_form' ) {
712 + return admin_url( 'post.php?post=' . $formId . '&action=edit' );
713 + }
714 +
715 + return admin_url( 'post.php?post=' . $formId . '&action=edit' );
716 + }
717 +
718 + /**
719 + * Get form settings.
720 + *
721 + * @return array
722 + */
723 + public function get_form_settings(): array {
724 + return CF7DoubleOptIn::getInstance()->getParameter( $this->get_cf_form_id() );
725 + }
726 +
727 + // =========================================================================
728 + // HELPERS
729 + // =========================================================================
730 +
731 + /**
732 + * Get the repository instance.
733 + *
734 + * @return OptInRepositoryInterface
735 + */
736 + private static function getRepository(): OptInRepositoryInterface {
737 + $container = Container::getInstance();
738 + return $container->get( OptInRepositoryInterface::class );
739 + }
740 +
741 + /**
742 + * Map legacy property names to entity array keys.
743 + *
744 + * @param array $properties Legacy properties.
745 + *
746 + * @return array
747 + */
748 + private function mapToEntityArray( array $properties ): array {
749 + $mapping = [
750 + 'cf_form_id' => 'cf_form_id',
751 + 'doubleoptin' => 'doubleoptin',
752 + 'content' => 'content',
753 + 'hash' => 'hash',
754 + 'createtime' => 'createtime',
755 + 'updatetime' => 'updatetime',
756 + 'optouttime' => 'optouttime',
757 + 'ipaddr_register' => 'ipaddr_register',
758 + 'ipaddr_confirmation' => 'ipaddr_confirmation',
759 + 'ipaddr_optout' => 'ipaddr_optout',
760 + 'files' => 'files',
761 + 'category' => 'category',
762 + 'form' => 'form',
763 + 'mail_optin' => 'mail_optin',
764 + 'email' => 'email',
765 + 'id' => 'id',
766 + 'consent_text' => 'consent_text',
767 + 'consent_field' => 'consent_field',
768 + ];
769 +
770 + $result = [];
771 + foreach ( $mapping as $legacy => $entity ) {
772 + if ( isset( $properties[ $legacy ] ) ) {
773 + $result[ $entity ] = $properties[ $legacy ];
774 + }
775 + }
776 +
777 + return $result;
778 + }
779 +}