PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.2
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.2
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 3.0.60 3.0.61 3.0.62 All 38 releases
← All changes | core/OptIn.class.php +819 -1488 3.0.61 → 5.6.2 View file →
@@ -1,1488 +1,819 @@
1 -<?php
2 -
3 -namespace forge12\contactform7\CF7DoubleOptIn {
4 - if (!defined('ABSPATH')) {
5 - exit;
6 - }
7 -
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;
22 -
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 - * Stores the timestamp the opt-out has been confirmed by the user.
59 - */
60 - private $optouttime = "";
61 - /**
62 - * IP Address used to trigger the opt-in mail.
63 - *
64 - * @var string
65 - */
66 - private $ipaddr_register = "";
67 - /**
68 - * IP Address used to confirm the opt-in mail.
69 - */
70 - private $ipaddr_confirmation = "";
71 - /**
72 - * IP Address used to output.
73 - */
74 - private $ipaddr_optout = "";
75 - /**
76 - * Stores the files used within this form as a serialized string.
77 - */
78 - private $files = "";
79 - /**
80 - * Store the Category ID
81 - */
82 - private $category = 0;
83 - /**
84 - * Stores the form as html
85 - */
86 - private $form = '';
87 - /**
88 - * Stores the Mail send for the optin
89 - *
90 - * @var string
91 - */
92 - private $mail_optin = '';
93 - /**
94 - * Stores the E-Mail
95 - *
96 - * @var string
97 - */
98 - private $email = '';
99 -
100 - /**
101 - * The constructor
102 - */
103 - public function __construct(array $properties)
104 - {
105 - foreach ($properties as $name => $value) {
106 - if (isset($this->{$name})) {
107 - $this->{$name} = $value;
108 - }
109 - }
110 - }
111 -
112 - /**
113 - * @param int $cf_form_id
114 - */
115 - public function set_cf_form_id($cf_form_id)
116 - {
117 - $this->cf_form_id = (int)$cf_form_id;
118 - }
119 -
120 - /**
121 - * @return int
122 - */
123 - public function get_cf_form_id()
124 - {
125 - return $this->cf_form_id;
126 - }
127 -
128 - /**
129 - * Return the ID of the Optin
130 - *
131 - * @return int
132 - */
133 - public function get_id()
134 - {
135 - return $this->id;
136 - }
137 -
138 - /**
139 - * Return the hash of the optin.
140 - *
141 - * @return string
142 - */
143 - public function get_hash()
144 - {
145 - return $this->hash;
146 - }
147 -
148 - /**
149 - * Return true|false depending on confirm status
150 - *
151 - * @return bool
152 - */
153 - public function is_confirmed()
154 - {
155 - return (bool)$this->get_doubleoptin();
156 - }
157 -
158 - /**
159 - * Return true|false wether the form has been confirmed or not.
160 - *
161 - * @return bool
162 - */
163 - public function is_optout()
164 - {
165 - if (true != $this->get_doubleoptin() && !empty($this->get_ipaddr_optout()) && !empty($this->get_optouttime())) {
166 - return true;
167 - }
168 -
169 - return false;
170 - }
171 -
172 - /**
173 - * update the optin value
174 - *
175 - * @param int $confirmed
176 - */
177 - public function set_doubleoptin($confirmed)
178 - {
179 - $this->doubleoptin = (int)$confirmed;
180 - }
181 -
182 - /**
183 - * Return the optin value for the form.
184 - *
185 - * @return int
186 - */
187 - public function get_doubleoptin()
188 - {
189 - return $this->doubleoptin;
190 - }
191 -
192 - /**
193 - * @param string $timestamp
194 - */
195 - public function set_createtime($timestamp)
196 - {
197 - $this->createtime = $timestamp;
198 - }
199 -
200 - /**
201 - * Return the Date how long the opt in will be valid
202 - *
203 - * @return string
204 - */
205 - public function get_valid_until()
206 - {
207 - $settings = CF7DoubleOptIn::getInstance()->getSettings();
208 -
209 - $createtime = $this->get_createtime();
210 - $dt = new \DateTime();
211 - $dt->setTimestamp($createtime);
212 -
213 - if ($this->is_confirmed()) {
214 - $month = (int)$settings['delete'];
215 - $period = $settings['delete_period'];
216 - } else {
217 - $month = (int)$settings['delete_unconfirmed'];
218 - $period = $settings['delete_unconfirmed_period'];
219 - }
220 -
221 - $dt->modify('+' . (int)$month . ' ' . $period);
222 - $dt->modify('+1 day');
223 -
224 - return $dt->format('d.m.Y');
225 - }
226 -
227 - /**
228 - * Return a timestamp
229 - *
230 - * @param string $view Select how to return the content. raw is the stored db value. use formatted to return a
231 - * formatted string.
232 - *
233 - * @return string
234 - */
235 - public function get_createtime($view = "raw")
236 - {
237 - if ($view != "raw") {
238 - if (!is_numeric($this->createtime)) {
239 - $date = date('d.m.Y', strtotime($this->createtime));
240 - $time = date('H:i:s', strtotime($this->createtime));
241 - } else {
242 - $date = date('d.m.Y', $this->createtime);
243 - $time = date('H:i:s', $this->createtime);
244 - }
245 -
246 - return $date . ' ' . __('/', 'double-opt-in') . ' ' . $time;
247 - } else {
248 - return $this->createtime;
249 - }
250 - }
251 -
252 - /**
253 - * @param string $timestamp
254 - */
255 - public function set_updatetime($timestamp)
256 - {
257 - $this->updatetime = $timestamp;
258 - }
259 -
260 - /**
261 - * Return a timestamp
262 - *
263 - * @param string $view Select how to return the content. raw is the stored db value. use formatted to return a
264 - * formatted string.
265 - *
266 - * @return string
267 - */
268 - public function get_updatetime($view = 'raw')
269 - {
270 - if ($view != "raw") {
271 - if (!is_numeric($this->updatetime)) {
272 - $date = date('d.m.Y', strtotime($this->updatetime));
273 - $time = date('H:i:s', strtotime($this->updatetime));
274 - } else {
275 - $date = date('d.m.Y', $this->updatetime);
276 - $time = date('H:i:s', $this->updatetime);
277 - }
278 -
279 - return $date . ' ' . __('/', 'double-opt-in') . ' ' . $time;
280 - } else {
281 - return $this->updatetime;
282 - }
283 - }
284 -
285 - /**
286 - * @param string $timestamp
287 - */
288 - public function set_optouttime($timestamp)
289 - {
290 - $this->optouttime = $timestamp;
291 - }
292 -
293 - /**
294 - * Return a timestamp
295 - *
296 - * @param string $view Select how to return the content. raw is the stored db value. use formatted to return a
297 - * formatted string.
298 - *
299 - * @return string
300 - */
301 - public function get_optouttime($view = 'raw')
302 - {
303 - if ($view != "raw") {
304 - if (!is_numeric($this->optouttime)) {
305 - $date = date('d.m.Y', strtotime($this->optouttime));
306 - $time = date('H:i:s', strtotime($this->optouttime));
307 - } else {
308 - $date = date('d.m.Y', $this->optouttime);
309 - $time = date('H:i:s', $this->optouttime);
310 - }
311 -
312 - return $date . ' ' . __('/', 'f12-cf7-doubleoptin') . ' ' . $time;
313 - } else {
314 - return $this->optouttime;
315 - }
316 - }
317 -
318 - /**
319 - * @param string $ip_addr
320 - */
321 - public function set_ipaddr_optout($ip_addr)
322 - {
323 - $this->ipaddr_optout = $ip_addr;
324 - }
325 -
326 - /**
327 - * @return string
328 - */
329 - public function get_ipaddr_optout()
330 - {
331 - return $this->ipaddr_optout;
332 - }
333 -
334 - /**
335 - * @param string $ip_addr
336 - */
337 - public function set_ipaddr_register($ip_addr)
338 - {
339 - $this->ipaddr_register = $ip_addr;
340 - }
341 -
342 - /**
343 - * @return string
344 - */
345 - public function get_ipaddr_register()
346 - {
347 - return $this->ipaddr_register;
348 - }
349 -
350 - /**
351 - * @param string $ip_addr
352 - */
353 - public function set_ipaddr_confirmation($ip_addr)
354 - {
355 - $this->ipaddr_confirmation = $ip_addr;
356 - }
357 -
358 - /**
359 - * @return string
360 - */
361 - public function get_ipaddr_confirmation()
362 - {
363 - return $this->ipaddr_confirmation;
364 - }
365 -
366 - /**
367 - * Return a serialized string
368 - *
369 - * @return mixed
370 - */
371 - public function get_content()
372 - {
373 - return $this->content;
374 - }
375 -
376 - /**
377 - * @param string - a serialized string
378 - */
379 - public function set_content($content)
380 - {
381 - $this->content = $content;
382 - }
383 -
384 - /**
385 - * @param string - a serialized string
386 - */
387 - public function set_files($files)
388 - {
389 - $this->files = $files;
390 - }
391 -
392 - /**
393 - * @return string use maybe_unserialize to deserialize the string
394 - */
395 - public function get_files()
396 - {
397 - return $this->files;
398 - }
399 -
400 - /**
401 - * Return the assigned Category ID
402 - *
403 - * @return int
404 - */
405 - public function get_category()
406 - {
407 - if (!is_numeric($this->category)) {
408 - return 0;
409 - }
410 -
411 - return $this->category;
412 - }
413 -
414 - /**
415 - * Assign a ID to the category
416 - *
417 - * @param int $id
418 - *
419 - * @return void
420 - */
421 - public function set_category($id)
422 - {
423 - $this->category = $id;
424 - }
425 -
426 - /**
427 - * Check if the given Opt In was created by Contact Form 7
428 - *
429 - * @return bool
430 - */
431 - public function isTypeCF7()
432 - {
433 - $form_id = $this->get_cf_form_id();
434 - $form = get_post($form_id);
435 -
436 - if ('wpcf7_contact_form' === $form->post_type) {
437 - return true;
438 - }
439 -
440 - return false;
441 - }
442 -
443 - /**
444 - * Check if the given Opt In was created by Elementor
445 - *
446 - * @return bool
447 - */
448 - public function isTypeElementor()
449 - {
450 - if (!class_exists('ElementorPro\Plugin')) {
451 - return false;
452 - }
453 -
454 - $page_id = $this->get_cf_form_id();
455 -
456 - $post = get_post($page_id);
457 -
458 - // skip if post not existing anymore
459 - if (null === $post) {
460 - return false;
461 - }
462 -
463 - // skip if page is not build with elementor
464 - if (!\Elementor\Plugin::$instance->documents->get($page_id)->is_built_with_elementor()) {
465 - return false;
466 - }
467 -
468 - // Load the page's data
469 - $document = \Elementor\Plugin::$instance->documents->get($page_id);
470 -
471 - // Skip if document not exist
472 - if (!$document) {
473 - return false;
474 - }
475 -
476 - // Loop through the elements
477 - $elements_data = $document->get_elements_data();
478 -
479 - $content = maybe_unserialize($this->get_content());
480 -
481 - // skip if form id not available
482 - if (!isset($content['form_id'])) {
483 - return false;
484 - }
485 -
486 - $form_id = $content['form_id'];
487 -
488 - // Find the Form Element
489 - foreach ($elements_data as $element) {
490 - if (isset($element['elements'])) {
491 - foreach ($element['elements'] as $el) {
492 - if ($form_id !== $el['id'] || 'form' !== $el['widgetType']) {
493 - continue;
494 - }
495 -
496 - return true;
497 - }
498 - return true;
499 - }
500 -
501 - // Check if this is the form widget
502 - if (isset($element['widgetType']) && isset($element['id']) && $element['widgetType'] === 'form' && $element['id'] === $form_id) {
503 - // Return the form settings
504 - return true;
505 - }
506 - }
507 - return false;
508 - }
509 -
510 - /**
511 - * Check if the given Opt In was created by Avada
512 - *
513 - * @return bool|void
514 - */
515 - public function isTypeAvada()
516 - {
517 - $form_id = $this->get_cf_form_id();
518 - $form = get_post($form_id);
519 -
520 - if ('fusion_form' === $form->post_type) {
521 - return true;
522 - }
523 -
524 - return false;
525 - }
526 -
527 - /**
528 - * Check if the Opt In was created by 'cf7' or 'avada'.
529 - *
530 - * @param string $type use either 'cf7' or 'avada'
531 - *
532 - * @return bool
533 - */
534 - public function isType($type)
535 - {
536 - if ($type == 'cf7') {
537 - return $this->isTypeCF7();
538 - } elseif ($type == 'elementor') {
539 - return $this->isTypeElementor();
540 - } else {
541 - return $this->isTypeAvada();
542 - }
543 - }
544 -
545 - /**
546 - * Load an OptIn by the given hash code. Returns either an Object of Type OptIn or null if not found.
547 - *
548 - * @param string $hash
549 - *
550 - * @return OptIn|null
551 - */
552 - public static function get_by_hash(string $hash)
553 - {
554 - global $wpdb;
555 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
556 -
557 - if (null == $wpdb) {
558 - return null;
559 - }
560 -
561 - $row = $wpdb->get_row('SELECT * FROM ' . $table . ' WHERE hash="' . $hash . '"', ARRAY_A);
562 -
563 - if (null == $row) {
564 - return null;
565 - }
566 -
567 - return new OptIn($row);
568 - }
569 -
570 - /**
571 - * Get number of optins for the given post.
572 - *
573 - * @param $post_id
574 - *
575 - * @return int
576 - */
577 - public static function get_count($post_id)
578 - {
579 - global $wpdb;
580 -
581 - if (!$wpdb) {
582 - return 0;
583 - }
584 -
585 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
586 - $result = $wpdb->get_results($wpdb->prepare('SELECT count(*) AS counter FROM ' . $tableName . ' WHERE cf_form_id=%d ', $post_id));
587 -
588 - if (is_array($result)) {
589 - return $result[0]->counter;
590 - }
591 -
592 - return 0;
593 - }
594 -
595 - /**
596 - * Return a list of opt ins stored in the database.
597 - *
598 - * @param int $category_id
599 - *
600 - * @param array $atts array (
601 - * 'perPage' => 10, // Posts per page
602 - * 'page' => 0, // Current page
603 - * 'order' => DESC, // Order (ASC/DESC)
604 - * );
605 - *
606 - * @param null $numberOfPages - Stores the number of pages found if limited
607 - *
608 - * @return array
609 - */
610 - public static function get_list_by_category_id($category_id, $atts = [], &$numberOfPages = null)
611 - {
612 - global $wpdb;
613 -
614 - $attr = [
615 - 'perPage' => 10,
616 - 'page' => 1,
617 - 'order' => 'DESC',
618 - 'keyword' => ''
619 - ];
620 -
621 - /**
622 - * Parameter
623 - */
624 - foreach ($atts as $key => $value) {
625 - if (isset($attr[$key])) {
626 - $attr[$key] = $atts[$key];
627 - }
628 - }
629 -
630 - /*
631 - * Update keyword
632 - */
633 - $where = '';
634 - if (!empty($attr['keyword'])) {
635 - $where = ' AND content LIKE "%%' . $attr['keyword'] . '%%"';
636 - }
637 -
638 -
639 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
640 - $pageNum = (int)$attr['page'] - 1;
641 -
642 - if ($numberOfPages !== null) {
643 - $result = $wpdb->get_results($wpdb->prepare('SELECT count(*) AS counter FROM ' . $tableName . ' WHERE category=%d ' . $where, $category_id));
644 -
645 - $itemCounter = 0;
646 -
647 - if (is_array($result)) {
648 - $itemCounter = $result[0]->counter;
649 - }
650 -
651 - $numberOfPages = 0;
652 -
653 - if ($itemCounter != 0) {
654 - $numberOfPages = ceil($itemCounter / (int)$attr['perPage']);
655 - }
656 - }
657 -
658 - $list = [];
659 -
660 - if ($numberOfPages == 0) {
661 - return $list;
662 - }
663 -
664 - $offset = $pageNum * (int)$attr['perPage'];
665 -
666 - $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);
667 -
668 - foreach ($rows as $row) {
669 - $list[] = new OptIn($row);
670 - }
671 -
672 - return $list;
673 - }
674 -
675 - /**
676 - * Return a list of opt ins stored in the database.
677 - *
678 - * @param string $email
679 - * @param int $confirmed Either recieve confirmed or unconfirmed only
680 - *
681 - * @return array
682 - */
683 - public static function get_list_by_email($email)
684 - {
685 - global $wpdb;
686 -
687 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
688 -
689 - $list = [];
690 -
691 - $rows = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $tableName . ' WHERE email=%s', $email), ARRAY_A);
692 -
693 - foreach ($rows as $row) {
694 - $list[] = new OptIn($row);
695 - }
696 -
697 - return $list;
698 - }
699 -
700 - /**
701 - * Return a list of opt ins stored in the database.
702 - *
703 - * @param $atts array (
704 - * 'perPage' => 10, // Posts per page
705 - * 'page' => 0, // Current page
706 - * 'order' => DESC, // Order (ASC/DESC)
707 - * );
708 - *
709 - * @param null $numberOfPages - Stores the number of pages found if limited
710 - *
711 - * @return array
712 - */
713 - public static function get_list($atts = [], &$numberOfPages = null)
714 - {
715 - global $wpdb;
716 -
717 - $attr = [
718 - 'perPage' => 10,
719 - 'page' => 1,
720 - 'order' => 'DESC',
721 - 'keyword' => '',
722 - 'cf_form_id' => '',
723 - ];
724 -
725 - /*
726 - * Update atts
727 - */
728 - foreach ($atts as $key => $value) {
729 - if (isset($attr[$key])) {
730 - $attr[$key] = $atts[$key];
731 - }
732 - }
733 -
734 - /*
735 - * Update WHERE for Query
736 - */
737 - $where = '';
738 - $where_condition = [];
739 -
740 - /*
741 - * Add Keyword
742 - */
743 - if (!empty($attr['keyword'])) {
744 - $where_condition[] = 'content LIKE "%%' . $attr['keyword'] . '%%"';
745 - }
746 -
747 - /*
748 - * Add Form ID
749 - */
750 - if (!empty($attr['cf_form_id'])) {
751 - $where_condition[] = 'cf_form_id="' . $attr['cf_form_id'] . '"';
752 - }
753 -
754 - /*
755 - * build where string
756 - */
757 - if (!empty($where_condition)) {
758 - $where = ' WHERE ' . implode(' AND ', $where_condition);
759 - }
760 -
761 - /*
762 - * set table
763 - */
764 - $tableName = $wpdb->prefix . 'f12_cf7_doubleoptin';
765 - $pageNum = (int)$attr['page'] - 1;
766 -
767 - if ($numberOfPages !== null) {
768 - $result = $wpdb->get_results('SELECT count(*) as counter FROM ' . $tableName . $where);
769 -
770 - $itemCounter = 0;
771 -
772 - if (is_array($result)) {
773 - $itemCounter = $result[0]->counter;
774 - }
775 -
776 - $numberOfPages = 0;
777 -
778 - if ($itemCounter != 0) {
779 - $numberOfPages = ceil($itemCounter / (int)$attr['perPage']);
780 - }
781 - }
782 -
783 - $list = [];
784 -
785 - if ($numberOfPages == 0) {
786 - return $list;
787 - }
788 -
789 - $offset = $pageNum * (int)$attr['perPage'];
790 -
791 - $rows = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $tableName . $where . ' ORDER BY ID ' . $attr['order'] . ' LIMIT ' . $attr['perPage'] . ' OFFSET % d', $offset), ARRAY_A);
792 -
793 - foreach ($rows as $row) {
794 - $list[] = new OptIn($row);
795 - }
796 -
797 - return $list;
798 - }
799 -
800 - /**
801 - * This will update all categories from A to B, e.g.: All Opt-Ins with id 1 to category id 2.
802 - *
803 - * @param int $from_id The origin Category ID
804 - * @param int $to_id The new Category ID
805 - *
806 - * @return bool|int|\mysqli_result|resource|null Return the number of rows affected by the query.
807 - */
808 - public static function bulk_update_category($from_id, $to_id)
809 - {
810 - global $wpdb;
811 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
812 -
813 - return $wpdb->query($wpdb->prepare('UPDATE ' . $table . ' SET category =%d WHERE category =%d', $to_id, $from_id));
814 - }
815 -
816 - /**
817 - * Update the category of the given opt in id.
818 - *
819 - * @param int $optin_id The ID of the OptIn
820 - * @param int $category_id The ID of the Category
821 - *
822 - * @return bool|int|\mysqli_result|resource|null
823 - */
824 - public static function update_category_by_id($optin_id, $category_id)
825 - {
826 - global $wpdb;
827 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
828 -
829 - return $wpdb->query($wpdb->prepare('UPDATE ' . $table . ' SET category =%d WHERE id =%d', $category_id, $optin_id));
830 - }
831 -
832 - /**
833 - * Update or create the given Object
834 - */
835 - public function save()
836 - {
837 - global $wpdb;
838 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
839 -
840 - f12_debug_log('save method called', [
841 - 'table' => $table,
842 - 'hash' => $this->get_hash(),
843 - ]);
844 -
845 - /**
846 - * Wenn ein Hash existiert, wird die Zeile aktualisiert
847 - */
848 - if (!empty($this->get_hash())) {
849 - f12_debug_log('Updating existing record', [
850 - 'hash' => $this->get_hash(),
851 - 'cf_form_id' => $this->get_cf_form_id(),
852 - 'doubleoptin' => $this->get_doubleoptin(),
853 - ]);
854 -
855 - $result = $wpdb->update($table, [
856 - 'doubleoptin' => $this->get_doubleoptin(),
857 - 'createtime' => $this->get_createtime(),
858 - 'updatetime' => $this->get_updatetime(),
859 - 'ipaddr_confirmation' => $this->get_ipaddr_confirmation(),
860 - 'ipaddr_register' => $this->get_ipaddr_register(),
861 - 'cf_form_id' => $this->get_cf_form_id(),
862 - 'content' => $this->get_content(),
863 - 'files' => $this->get_files(),
864 - 'category' => $this->get_category(),
865 - 'optouttime' => $this->get_optouttime(),
866 - 'ipaddr_optout' => $this->get_ipaddr_optout(),
867 - 'mail_optin' => $this->get_mail_optin(),
868 - ], [
869 - 'hash' => $this->get_hash()
870 - ]);
871 -
872 - f12_debug_log('Update operation completed', ['result' => $result]);
873 -
874 - return $result;
875 - }
876 -
877 - /**
878 - * Wenn kein Hash existiert, wird ein neuer Datensatz eingefügt
879 - */
880 - f12_debug_log('Inserting new record', [
881 - 'cf_form_id' => $this->get_cf_form_id(),
882 - 'email' => $this->get_email(),
883 - 'doubleoptin' => $this->get_doubleoptin(),
884 - ]);
885 -
886 - $result = $wpdb->insert(
887 - $table,
888 - $data = [ // Daten in einer Variablen speichern, um sie anschließend zu loggen
889 - 'cf_form_id' => $this->get_cf_form_id(),
890 - 'doubleoptin' => $this->get_doubleoptin(),
891 - 'createtime' => $this->get_createtime(),
892 - 'updatetime' => $this->get_updatetime(),
893 - 'ipaddr_confirmation' => $this->get_ipaddr_confirmation(),
894 - 'ipaddr_register' => $this->get_ipaddr_register(),
895 - 'content' => $this->get_content(),
896 - 'files' => $this->get_files(),
897 - 'category' => $this->get_category(),
898 - 'form' => $this->get_form(true),
899 - 'mail_optin' => $this->get_mail_optin(),
900 - 'email' => $this->get_email(),
901 - ]
902 - );
903 -
904 - $formValue = $this->get_form();
905 - f12_debug_log('Form Data Information', [
906 - 'form_length' => strlen($formValue), // Länge des Inhalts
907 - 'form_data_sample' => substr($formValue, 0, 500), // Nur die ersten 500 Zeichen loggen
908 - ]);
909 -
910 - $log_data = $data;
911 - $log_data['form'] = substr($formValue, 0, 200);
912 -
913 - f12_debug_log('Preparing to insert new record', [
914 - 'table' => $table,
915 - 'data' => $log_data
916 - ]);
917 -
918 - if (false === $result) {
919 - f12_debug_log('Insert operation failed', [
920 - 'mysql_error' => $wpdb->last_error
921 - ]);
922 -
923 - $this->validateEncoding($formValue);
924 - $this->validateMaxAllowedPacket($formValue);
925 - $this->validateFormField();
926 -
927 - return false;
928 - }
929 -
930 - f12_debug_log('Insert operation completed', [
931 - 'result' => $result,
932 - 'insert_id' => $wpdb->insert_id,
933 - ]);
934 -
935 - if ($result > 0) {
936 - $this->id = $wpdb->insert_id;
937 - f12_debug_log('New record inserted, ID assigned', ['id' => $this->id]);
938 -
939 - $hashResult = $this->createHash();
940 - f12_debug_log('Hash created for new record', ['hashResult' => $hashResult]);
941 -
942 - return $hashResult;
943 - }
944 -
945 - f12_debug_log('Failed to insert new record');
946 - return false;
947 - }
948 -
949 - private function validateFormField()
950 - {
951 - global $wpdb;
952 - // SQL-Anfrage, um die Felddefinitionen abzurufen
953 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
954 -
955 - $columns = $wpdb->get_results("SHOW FIELDS FROM $table");
956 -
957 - // Überprüfung der Feldtypen
958 - foreach ($columns as $column) {
959 - f12_debug_log('Field Info', [
960 - 'Field' => $column->Field,
961 - 'Type' => $column->Type,
962 - 'Null' => $column->Null,
963 - 'Default' => $column->Default
964 - ]);
965 - }
966 - }
967 -
968 - private function validateEncoding(string $data)
969 - {
970 - if (!mb_check_encoding($data, 'UTF-8')) {
971 - f12_debug_log('Encoding Issue', ['form_data' => substr($data, 0, 500)]);
972 - return false;
973 - }
974 - f12_debug_log('Encoding OK', []);
975 - return true;
976 - }
977 -
978 - private function validateMaxAllowedPacket(string $data)
979 - {
980 - $maxAllowedPacketMB = $this->getMaxAllowedPacket();
981 -
982 - $dataSize = strlen($data); // Size in bytes
983 - $dataSizeMB = $dataSize / (1024 * 1024); // Convert bytes to MB
984 -
985 - f12_debug_log('Data Size Check', [
986 - 'data_size_bytes' => $dataSize,
987 - 'data_size_mb' => $dataSizeMB,
988 - ]);
989 -
990 - if ($dataSize > $maxAllowedPacketMB) {
991 - f12_debug_log('max_allowed_packet Too Small', [
992 - 'data_size_mb' => $dataSizeMB,
993 - 'max_allowed_packet_mb' => $maxAllowedPacketMB
994 - ]);
995 - return false;
996 - } else {
997 - f12_debug_log('max_allowed_packet OK', []);
998 - return true;
999 - }
1000 - }
1001 -
1002 - private function getMaxAllowedPacket()
1003 - {
1004 - global $wpdb;
1005 -
1006 - $result = $wpdb->get_row("SHOW VARIABLES LIKE 'max_allowed_packet'", OBJECT);
1007 -
1008 - /**
1009 - * Prüfen, ob die Variable erfolgreich abgerufen wurde
1010 - * Die zweite Spalte der Abfrage enthält den Wert (Value)
1011 - */
1012 - // Abfrage, um den aktuellen Wert von max_allowed_packet zu erhalten
1013 - if ($result) {
1014 - $maxAllowedPacket = $result->Value; // Wert aus der zweiten Spalte
1015 - f12_debug_log('MySQL max_allowed_packet Value', [
1016 - 'max_allowed_packet_bytes' => $maxAllowedPacket,
1017 - 'max_allowed_packet_mb' => $maxAllowedPacket / (1024 * 1024) // In MB
1018 - ]);
1019 - } else {
1020 - // Falls das Ergebnis leer ist (z. B. Fehler oder keine Berechtigung)
1021 - f12_debug_log('Error fetching max_allowed_packet', [
1022 - 'error' => $wpdb->last_error
1023 - ]);
1024 - return 0;
1025 - }
1026 - return $maxAllowedPacket;
1027 - }
1028 -
1029 - /**
1030 - * Creates a unique hash code and adds it to the current OptIn Object
1031 - */
1032 - private function createHash()
1033 - {
1034 - global $wpdb;
1035 -
1036 - if (null == $wpdb) {
1037 - return false;
1038 - }
1039 -
1040 - $table = $wpdb->prefix . 'f12_cf7_doubleoptin';
1041 -
1042 - // add the hash to the element
1043 -
1044 - $this->hash = base64_encode($this->get_createtime() . $this->get_id() . $this->get_cf_form_id());
1045 -
1046 - return $wpdb->update($table, [
1047 - 'hash' => $this->hash,
1048 - ], [
1049 - 'id' => $this->get_id()
1050 - ]);
1051 - }
1052 -
1053 - private function is_base64_encoded($string)
1054 - {
1055 - // Prüfen, ob der String Base64-kompatible Zeichen enthält
1056 - if (preg_match('/^[A-Za-z0-9+\/=]*$/', $string)) {
1057 - // Prüfen, ob die Länge des Strings ein Vielfaches von 4 ist
1058 - return strlen($string) % 4 === 0;
1059 - }
1060 - return false;
1061 - }
1062 -
1063 - /**
1064 - * Return the Form
1065 - *
1066 - * @return string
1067 - */
1068 - public function get_form($encoded = false)
1069 - {
1070 - if (!$encoded) {
1071 - if ($this->is_base64_encoded($this->form)) {
1072 - return base64_decode($this->form);
1073 - } else {
1074 - return $this->form;
1075 - }
1076 - } else {
1077 - if ($this->is_base64_encoded($this->form)) {
1078 - return $this->form;
1079 - } else {
1080 - return base64_encode($this->form);
1081 - }
1082 - }
1083 - }
1084 -
1085 - /**
1086 - * Returns the Name of the form if available
1087 - *
1088 - * @return string The name of the Form
1089 - * @since 2.4.0
1090 - */
1091 - public function get_form_name()
1092 - {
1093 - $post = get_post($this->get_cf_form_id());
1094 -
1095 - if (null === $post) {
1096 - return __('Deleted', 'double-opt-in');
1097 - }
1098 -
1099 - return $post->post_title;
1100 - }
1101 -
1102 - /**
1103 - * Returns the URL to the form depending on the Form Type.
1104 - *
1105 - * @return string The Backend URL to the Form, empty if the post is not found.
1106 - * @since 2.4.0
1107 - *
1108 - */
1109 - public function get_form_link()
1110 - {
1111 - if ($this->isTypeElementor()) {
1112 - return get_edit_post_link($this->get_cf_form_id());
1113 - } elseif ($this->isTypeAvada()) {
1114 - return get_edit_post_link($this->get_cf_form_id());
1115 - } elseif ($this->isTypeCF7()) {
1116 - $post = get_post($this->get_cf_form_id());
1117 -
1118 - if (!$post) {
1119 - return '';
1120 - }
1121 -
1122 - return admin_url('admin.php?page=wpcf7&post=' . esc_attr($this->get_cf_form_id()) . '&action=edit');
1123 - }
1124 -
1125 - return '';
1126 - }
1127 -
1128 -
1129 - /**
1130 - * Retrieves the form settings.
1131 - *
1132 - * If the form is created using Elementor, retrieves the settings using the `get_settings_by_elementor()`
1133 - * method.
1134 - *
1135 - * @return array The form settings.
1136 - * @since 2.4.0
1137 - *
1138 - */
1139 - public function get_form_settings()
1140 - {
1141 - if ($this->isTypeElementor()) {
1142 - return $this->get_settings_by_elementor();
1143 - } else if ($this->isTypeCF7()) {
1144 - return $this->get_settings_by_cf7();
1145 - } else if ($this->isTypeAvada()) {
1146 - return $this->get_settings_by_avada();
1147 - }
1148 -
1149 - return [];
1150 - }
1151 -
1152 - /**
1153 - * Retrieves the settings of a specific element using avada.
1154 - *
1155 - * @return array
1156 - * @since 2.4.0
1157 - *
1158 - * @see OptIn::maybe_normalize_settings() for details
1159 - */
1160 - private function get_settings_by_avada()
1161 - {
1162 -
1163 - $form_id = $this->get_cf_form_id();
1164 -
1165 - $settings = CF7DoubleOptIn::getInstance()->getParameter($form_id);
1166 -
1167 - return $this->maybe_normalize_settings($settings);
1168 - }
1169 -
1170 - /**
1171 - * Retrieves the settings of a specific element using cf7.
1172 - *
1173 - * @return array
1174 - * @since 2.4.0
1175 - *
1176 - * @see OptIn::maybe_normalize_settings() for details
1177 - */
1178 - private function get_settings_by_cf7()
1179 - {
1180 - $form_id = $this->get_cf_form_id();
1181 -
1182 - $settings = get_post_meta($form_id, 'f12-cf7-doubleoptin', true);
1183 -
1184 - if (null == $settings || !is_array($settings)) {
1185 - return [];
1186 - }
1187 -
1188 - $settings = $this->maybe_normalize_settings($settings);
1189 -
1190 - return $settings;
1191 - }
1192 -
1193 - /**
1194 - * Retrieves the settings of a specific element using Elementor.
1195 - *
1196 - * @return array
1197 - * @since 2.4.0
1198 - *
1199 - * @see OptIn::maybe_normalize_settings() for details
1200 - */
1201 - private function get_settings_by_elementor()
1202 - {
1203 - $page_id = $this->get_cf_form_id();
1204 -
1205 - $post = get_post($page_id);
1206 -
1207 - // skip if post not existing anymore
1208 - if (null === $post) {
1209 - return [];
1210 - }
1211 -
1212 - // skip if page is not build with elementor
1213 - if (!\Elementor\Plugin::$instance->documents->get($page_id)->is_built_with_elementor()) {
1214 - return [];
1215 - }
1216 -
1217 - // Load the page's data
1218 - $document = \Elementor\Plugin::$instance->documents->get($page_id);
1219 -
1220 - // Skip if document not exist
1221 - if (!$document) {
1222 - return [];
1223 - }
1224 -
1225 - // Loop through the elements
1226 - $elements_data = $document->get_elements_data();
1227 -
1228 - $content = maybe_unserialize($this->get_content());
1229 -
1230 - // skip if form id not available
1231 - if (!isset($content['form_id'])) {
1232 - return [];
1233 - }
1234 -
1235 - $form_id = $content['form_id'];
1236 -
1237 - $form_element = null;
1238 -
1239 - // Find the Form Element
1240 - foreach ($elements_data as $element) {
1241 - if (isset($element['elements'])) {
1242 - foreach ($element['elements'] as $el) {
1243 - if ($form_id !== $el['id'] || 'form' !== $el['widgetType']) {
1244 - continue;
1245 - }
1246 -
1247 - $form_element = $el;
1248 - }
1249 - }
1250 -
1251 - // Check if this is the form widget
1252 - if (isset($element['widgetType']) && isset($element['id']) && $element['widgetType'] === 'form' && $element['id'] === $form_id) {
1253 - // Return the form settings
1254 - $form_element = $element;
1255 - }
1256 - }
1257 -
1258 - // skip if form element not found on the page anymore
1259 - if ($form_element == null || !isset($form_element['settings'])) {
1260 - return [];
1261 - }
1262 -
1263 - // Normalize Settings
1264 - return $this->maybe_normalize_settings($form_element['settings']);
1265 - }
1266 -
1267 - /**
1268 - * Normalizes the settings by converting the value of the settings
1269 - *
1270 - * @param array $settings The settings array to be normalized.
1271 - * @formatter:off
1272 - *
1273 - * @return array {
1274 - * The settings of the element if found or an empty array if nothing was found.
1275 - * @type string $doi_email_to
1276 - * @type string $doi_email_subject
1277 - * @type string $doi_email_content
1278 - * @type string $doi_email_from
1279 - * @type string $doi_email_from_name
1280 - * @type string $doi_email_template
1281 - * @type string $doi_email_body
1282 - * }
1283 - *
1284 - * @formatter:on
1285 - */
1286 - private function maybe_normalize_settings($settings)
1287 - {
1288 - if (isset($settings['sender'])) {
1289 - $settings['doi_email_from'] = $settings['sender'];
1290 - }
1291 -
1292 - if (isset($settings['sender_name'])) {
1293 - $settings['doi_email_from_name'] = $settings['sender_name'];
1294 - }
1295 -
1296 - if (isset($settings['subject'])) {
1297 - $settings['doi_email_subject'] = $settings['subject'];
1298 - }
1299 -
1300 - if (isset($settings['recipient'])) {
1301 - $settings['doi_email_to'] = $settings['recipient'];
1302 - }
1303 -
1304 - if (isset($settings['template'])) {
1305 - $settings['doi_email_template'] = $settings['template'];
1306 - }
1307 -
1308 - if (isset($settings['body'])) {
1309 - $settings['doi_email_body'] = $settings['body'];
1310 - }
1311 -
1312 - /**
1313 - * Normalize the settings between the different form plugins
1314 - *
1315 - * @formatter:off
1316 - *
1317 - * @param array {
1318 - * @type string $doi_email_to
1319 - * @type string $doi_email_subject
1320 - * @type string $doi_email_content
1321 - * @type string $doi_email_from
1322 - * @type string $doi_email_from_name
1323 - * @type string $doi_email_template
1324 - * @type string $doi_email_body
1325 - * }
1326 - *
1327 - * @formatter:on
1328 - *
1329 - * @since 3.0.0
1330 - */
1331 - return apply_filters('f12_cf7_doubleoption_normalize_settings', $settings);
1332 - }
1333 -
1334 - /**
1335 - * Set the Form as HTML
1336 - *
1337 - * @param string $form
1338 - *
1339 - * @return void
1340 - */
1341 - public function set_form($form)
1342 - {
1343 - $this->form = $form;
1344 - }
1345 -
1346 - /**
1347 - * Return the Form
1348 - *
1349 - * @return string
1350 - */
1351 - public function get_mail_optin()
1352 - {
1353 - return $this->mail_optin;
1354 - }
1355 -
1356 - /**
1357 - * Return the E-mail
1358 - *
1359 - * @return string
1360 - */
1361 - public function get_email()
1362 - {
1363 - return $this->email;
1364 - }
1365 -
1366 - /**
1367 - * Store the email
1368 - *
1369 - * @param string $email
1370 - *
1371 - * @return void
1372 - */
1373 - public function set_email($email)
1374 - {
1375 - $this->email = $email;
1376 - }
1377 -
1378 - /**
1379 - * Set the Form as HTML
1380 - *
1381 - * @param string $form
1382 - *
1383 - * @return void
1384 - */
1385 - public function set_mail_optin($mail_html)
1386 - {
1387 - $this->mail_optin = $mail_html;
1388 - }
1389 -
1390 - /**
1391 - * Return the link to the detail view
1392 - *
1393 - * @return string
1394 - */
1395 - public function get_link_ui()
1396 - {
1397 - return admin_url('admin.php?page=f12-cf7-doubleoptin_optin_view&hash=' . $this->get_hash());
1398 - }
1399 -
1400 - /**
1401 - * Return the link to delete
1402 - *
1403 - * @return string
1404 - */
1405 - public function get_link_delete()
1406 - {
1407 - return admin_url('admin.php?page=f12-cf7-doubleoptin&option=delete&hash=' . $this->get_hash());
1408 - }
1409 -
1410 - /**
1411 - * Return the link to the export
1412 - *
1413 - * @return string
1414 - */
1415 - public function get_link_export()
1416 - {
1417 - return admin_url('admin.php?page=f12-cf7-doubleoptin&export=csv&id=' . $this->get_id());
1418 - }
1419 -
1420 - /**
1421 - * Return the link to the export
1422 - *
1423 - * @return string
1424 - */
1425 - public function get_link_export_v2()
1426 - {
1427 - return admin_url('admin.php?page=f12-cf7-doubleoptin&export=csv_v2&id=' . $this->get_id());
1428 - }
1429 -
1430 - /**
1431 - * Return the link to the optin
1432 - *
1433 - * @return string
1434 - */
1435 - public function get_link_optin($parameter = [])
1436 - {
1437 - if ($this->get_cf_form_id() == 0) {
1438 - return get_home_url() . '?optin=' . $this->get_hash();
1439 - }
1440 -
1441 - $parameter = array_merge(CF7DoubleOptIn::getInstance()->getParameter($this->get_cf_form_id()), $parameter);
1442 -
1443 - $page_id = $parameter['page'] ?? -1;
1444 -
1445 - if ($page_id == -1 || !$page_id || $page_id == 0) {
1446 - $link = get_home_url() . '?optin=' . $this->get_hash();
1447 - } else {
1448 -
1449 - $link = get_permalink($page_id);
1450 -
1451 - if (strpos($link, "?") !== false) {
1452 - $link .= '&optin=' . $this->get_hash();
1453 - } else {
1454 - $link .= '?optin=' . $this->get_hash();
1455 - }
1456 - }
1457 -
1458 - return $link;
1459 - }
1460 -
1461 - /**
1462 - * Return the link to the optout
1463 - *
1464 - * @return string
1465 - */
1466 - public function get_link_optout()
1467 - {
1468 - $settings = CF7DoubleOptIn::getInstance()->getSettings();
1469 -
1470 - if (!isset($settings['optout_page']) || $settings['optout_page'] == 0) {
1471 - return get_home_url() . '?optout=' . $this->get_hash();
1472 - } else {
1473 - $url = get_permalink($settings['optout_page']);
1474 -
1475 - if (str_contains($url, '?')) {
1476 - // If '?' exists in the URL, it means there are already parameters, so append with '&'
1477 - $url .= '&optout=' . $this->get_hash();
1478 - } else {
1479 - // Else, there are no parameters yet, so append with '?'
1480 - $url .= '?optout=' . $this->get_hash();
1481 - }
1482 -
1483 - return $url;
1484 -
1485 - }
1486 - }
1487 - }
1488 -}
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 + */
11 +
12 +namespace forge12\contactform7\CF7DoubleOptIn;
13 +
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;
19 +
20 +if ( ! defined( 'ABSPATH' ) ) {
21 + exit;
22 +}
23 +
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 {
31 +
32 + private LoggerInterface $logger;
33 + private OptInEntity $entity;
34 +
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;
43 +
44 + if ( ! empty( $properties ) ) {
45 + $this->entity = OptInEntity::fromArray( $this->mapToEntityArray( $properties ) );
46 + } else {
47 + $this->entity = OptInEntity::create();
48 + }
49 +
50 + $this->logger->debug( 'OptIn facade initialized', [
51 + 'plugin' => 'double-opt-in',
52 + 'id' => $this->entity->getId(),
53 + ] );
54 + }
55 +
56 + /**
57 + * Get the logger instance.
58 + *
59 + * @return LoggerInterface
60 + */
61 + public function get_logger(): LoggerInterface {
62 + return $this->logger;
63 + }
64 +
65 + /**
66 + * Get the underlying entity.
67 + *
68 + * @return OptInEntity
69 + */
70 + public function getEntity(): OptInEntity {
71 + return $this->entity;
72 + }
73 +
74 + // =========================================================================
75 + // STATIC FACTORY METHODS
76 + // =========================================================================
77 +
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 + ] );
91 +
92 + try {
93 + $repository = self::getRepository();
94 + $entity = $repository->findByHash( $hash );
95 +
96 + if ( ! $entity ) {
97 + return null;
98 + }
99 +
100 + $optIn = new self( $logger );
101 + $optIn->entity = $entity;
102 +
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 + }
113 +
114 + /**
115 + * Get OptIn by its database id.
116 + *
117 + * Server-side use only (cron, admin REST). Public links always
118 + * address an opt-in by its random hash, never by id.
119 + *
120 + * @param int $id The opt-in id.
121 + *
122 + * @return OptIn|null
123 + *
124 + * @since 5.6.0
125 + */
126 + public static function get_by_id( int $id ): ?OptIn {
127 + if ( $id <= 0 ) {
128 + return null;
129 + }
130 +
131 + $logger = Logger::getInstance();
132 +
133 + try {
134 + $entity = self::getRepository()->findById( $id );
135 +
136 + if ( ! $entity ) {
137 + return null;
138 + }
139 +
140 + $optIn = new self( $logger );
141 + $optIn->entity = $entity;
142 +
143 + return $optIn;
144 + } catch ( \Exception $e ) {
145 + $logger->error( 'Failed to get OptIn by id', [
146 + 'plugin' => 'double-opt-in',
147 + 'optin_id' => $id,
148 + 'error' => $e->getMessage(),
149 + ] );
150 + return null;
151 + }
152 + }
153 +
154 + /**
155 + * Get count by form ID.
156 + *
157 + * @param int $formId The form ID.
158 + *
159 + * @return int
160 + */
161 + public static function get_count( int $formId ): int {
162 + try {
163 + return self::getRepository()->countByFormId( $formId );
164 + } catch ( \Exception $e ) {
165 + return 0;
166 + }
167 + }
168 +
169 + /**
170 + * Get list of OptIns with pagination.
171 + *
172 + * @param array $atts Query attributes.
173 + * @param int|null $numberOfPages Reference to store page count.
174 + *
175 + * @return array<OptIn>
176 + */
177 + public static function get_list( array $atts = [], ?int &$numberOfPages = null ): array {
178 + $logger = Logger::getInstance();
179 +
180 + try {
181 + $repository = self::getRepository();
182 + $entities = $repository->findAll( $atts, $numberOfPages );
183 +
184 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
185 + $optIn = new self( $logger );
186 + $optIn->entity = $entity;
187 + return $optIn;
188 + }, $entities );
189 + } catch ( \Exception $e ) {
190 + $logger->error( 'Failed to get OptIn list', [
191 + 'plugin' => 'double-opt-in',
192 + 'error' => $e->getMessage(),
193 + ] );
194 + return [];
195 + }
196 + }
197 +
198 + /**
199 + * Get list by category ID.
200 + *
201 + * @param int $categoryId The category ID.
202 + * @param array $atts Query attributes.
203 + * @param int|null $numberOfPages Reference to store page count.
204 + *
205 + * @return array<OptIn>
206 + */
207 + public static function get_list_by_category_id( int $categoryId, array $atts = [], ?int &$numberOfPages = null ): array {
208 + $logger = Logger::getInstance();
209 +
210 + try {
211 + $repository = self::getRepository();
212 +
213 + // Get total count for pagination
214 + if ( $numberOfPages !== null ) {
215 + $keyword = $atts['keyword'] ?? '';
216 + $perPage = max( 1, (int) ( $atts['perPage'] ?? 10 ) );
217 + $total = $repository->countByCategory( $categoryId, $keyword );
218 + $numberOfPages = $total > 0 ? (int) ceil( $total / $perPage ) : 0;
219 +
220 + if ( $numberOfPages === 0 ) {
221 + return [];
222 + }
223 + }
224 +
225 + $entities = $repository->findByCategory( $categoryId, $atts );
226 +
227 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
228 + $optIn = new self( $logger );
229 + $optIn->entity = $entity;
230 + return $optIn;
231 + }, $entities );
232 + } catch ( \Exception $e ) {
233 + $logger->error( 'Failed to get OptIn list by category', [
234 + 'plugin' => 'double-opt-in',
235 + 'category' => $categoryId,
236 + 'error' => $e->getMessage(),
237 + ] );
238 + return [];
239 + }
240 + }
241 +
242 + /**
243 + * Get list by email.
244 + *
245 + * @param string $email The email address.
246 + *
247 + * @return array<OptIn>
248 + */
249 + public static function get_list_by_email( string $email ): array {
250 + $logger = Logger::getInstance();
251 +
252 + try {
253 + $repository = self::getRepository();
254 + $entities = $repository->findByEmail( $email );
255 +
256 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
257 + $optIn = new self( $logger );
258 + $optIn->entity = $entity;
259 + return $optIn;
260 + }, $entities );
261 + } catch ( \Exception $e ) {
262 + return [];
263 + }
264 + }
265 +
266 + /**
267 + * Get confirmed list by email.
268 + *
269 + * @param string $email The email address.
270 + *
271 + * @return array<OptIn>
272 + */
273 + public static function get_list_by_email_confirmed( string $email ): array {
274 + $logger = Logger::getInstance();
275 +
276 + try {
277 + $repository = self::getRepository();
278 + $entities = $repository->findConfirmedByEmail( $email );
279 +
280 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
281 + $optIn = new self( $logger );
282 + $optIn->entity = $entity;
283 + return $optIn;
284 + }, $entities );
285 + } catch ( \Exception $e ) {
286 + return [];
287 + }
288 + }
289 +
290 + /**
291 + * Get unconfirmed list by email.
292 + *
293 + * @param string $email The email address.
294 + *
295 + * @return array<OptIn>
296 + */
297 + public static function get_list_by_email_unconfirmed( string $email ): array {
298 + $logger = Logger::getInstance();
299 +
300 + try {
301 + $repository = self::getRepository();
302 + $entities = $repository->findUnconfirmedByEmail( $email );
303 +
304 + return array_map( function ( OptInEntity $entity ) use ( $logger ) {
305 + $optIn = new self( $logger );
306 + $optIn->entity = $entity;
307 + return $optIn;
308 + }, $entities );
309 + } catch ( \Exception $e ) {
310 + return [];
311 + }
312 + }
313 +
314 + /**
315 + * Bulk update category.
316 + *
317 + * @param int $fromId The source category ID.
318 + * @param int $toId The target category ID.
319 + *
320 + * @return int
321 + */
322 + public static function bulk_update_category( int $fromId, int $toId ): int {
323 + try {
324 + return self::getRepository()->bulkUpdateCategory( $fromId, $toId );
325 + } catch ( \Exception $e ) {
326 + return 0;
327 + }
328 + }
329 +
330 + /**
331 + * Update category by OptIn ID.
332 + *
333 + * @param int $optInId The OptIn ID.
334 + * @param int $categoryId The new category ID.
335 + *
336 + * @return bool
337 + */
338 + public static function update_category_by_id( int $optInId, int $categoryId ): bool {
339 + try {
340 + return self::getRepository()->updateCategoryById( $optInId, $categoryId );
341 + } catch ( \Exception $e ) {
342 + return false;
343 + }
344 + }
345 +
346 + // =========================================================================
347 + // GETTERS
348 + // =========================================================================
349 +
350 + public function get_id(): int {
351 + return $this->entity->getId();
352 + }
353 +
354 + public function get_hash(): string {
355 + return $this->entity->getHash();
356 + }
357 +
358 + public function get_cf_form_id(): int {
359 + return $this->entity->getFormId();
360 + }
361 +
362 + public function is_confirmed(): bool {
363 + return $this->entity->isConfirmed();
364 + }
365 +
366 + public function get_doubleoptin(): int {
367 + return $this->entity->isConfirmed() ? 1 : 0;
368 + }
369 +
370 + public function is_optout(): bool {
371 + return $this->entity->isOptedOut();
372 + }
373 +
374 + public function get_content(): string {
375 + return $this->entity->getContent();
376 + }
377 +
378 + public function get_createtime( string $view = 'raw' ): string {
379 + if ( $view !== 'raw' ) {
380 + return $this->entity->getCreateTimeFormatted( 'd.m.Y / H:i:s' );
381 + }
382 + return (string) $this->entity->getCreateTime();
383 + }
384 +
385 + public function get_updatetime( string $view = 'raw' ): string {
386 + if ( $view !== 'raw' ) {
387 + return $this->entity->getUpdateTimeFormatted( 'd.m.Y / H:i:s' );
388 + }
389 + return (string) $this->entity->getUpdateTime();
390 + }
391 +
392 + public function get_optouttime( string $view = 'raw' ): string {
393 + $time = $this->entity->getOptOutTime();
394 + if ( $view !== 'raw' && $time > 0 ) {
395 + return wp_date( 'd.m.Y / H:i:s', $time );
396 + }
397 + return (string) $time;
398 + }
399 +
400 + /**
401 + * Get create time as ISO 8601 string.
402 + *
403 + * @return string
404 + */
405 + public function get_createtime_iso(): string {
406 + return $this->entity->getCreateTimeISO();
407 + }
408 +
409 + /**
410 + * Get update time as ISO 8601 string.
411 + *
412 + * @return string
413 + */
414 + public function get_updatetime_iso(): string {
415 + return $this->entity->getUpdateTimeISO();
416 + }
417 +
418 + /**
419 + * Get opt-out time as ISO 8601 string.
420 + *
421 + * @return string
422 + */
423 + public function get_optouttime_iso(): string {
424 + return $this->entity->getOptOutTimeISO();
425 + }
426 +
427 + public function get_ipaddr_register(): string {
428 + return $this->entity->getIpRegister();
429 + }
430 +
431 + public function get_ipaddr_confirmation(): string {
432 + return $this->entity->getIpConfirmation();
433 + }
434 +
435 + public function get_ipaddr_optout(): string {
436 + return $this->entity->getIpOptOut();
437 + }
438 +
439 + public function get_files(): string {
440 + return $this->entity->getFiles();
441 + }
442 +
443 + public function get_category(): int {
444 + return $this->entity->getCategory();
445 + }
446 +
447 + public function get_email(): string {
448 + return $this->entity->getEmail();
449 + }
450 +
451 + public function get_mail_optin(): string {
452 + return $this->entity->getMailOptIn();
453 + }
454 +
455 + public function get_consent_text(): string {
456 + return $this->entity->getConsentText();
457 + }
458 +
459 + public function get_form( bool $encoded = false ): string {
460 + $form = $this->entity->getForm();
461 + if ( $encoded ) {
462 + return base64_encode( $form );
463 + }
464 + return $form;
465 + }
466 +
467 + /**
468 + * Get the validity end date.
469 + *
470 + * @return string
471 + */
472 + public function get_valid_until(): string {
473 + $settings = CF7DoubleOptIn::getInstance()->getSettings();
474 + $dt = new \DateTime();
475 + $dt->setTimestamp( $this->entity->getCreateTime() );
476 +
477 + if ( $this->is_confirmed() ) {
478 + $amount = (int) ( $settings['delete'] ?? 0 );
479 + $period = $settings['delete_period'] ?? 'months';
480 + } else {
481 + $amount = (int) ( $settings['delete_unconfirmed'] ?? 0 );
482 + $period = $settings['delete_unconfirmed_period'] ?? 'months';
483 + }
484 +
485 + $dt->modify( '+' . $amount . ' ' . $period );
486 + $dt->modify( '+1 day' );
487 +
488 + return $dt->format( 'd.m.Y' );
489 + }
490 +
491 + // =========================================================================
492 + // SETTERS
493 + // =========================================================================
494 +
495 + public function set_cf_form_id( int $formId ): void {
496 + $this->entity = $this->entity->withFormId( $formId );
497 + }
498 +
499 + public function set_doubleoptin( int $confirmed ): void {
500 + $this->entity->setConfirmed( (bool) $confirmed );
501 + }
502 +
503 + public function set_createtime( $timestamp ): void {
504 + $this->entity = $this->entity->withCreateTime( (int) $timestamp );
505 + }
506 +
507 + public function set_updatetime( $timestamp ): void {
508 + $this->entity->setUpdateTime( (int) $timestamp );
509 + }
510 +
511 + public function set_optouttime( $timestamp ): void {
512 + $this->entity = $this->entity->withOptOutTime( (int) $timestamp );
513 + }
514 +
515 + public function set_ipaddr_register( string $ip ): void {
516 + $this->entity = $this->entity->withIpRegister( $ip );
517 + }
518 +
519 + public function set_ipaddr_confirmation( string $ip ): void {
520 + $this->entity->setIpConfirmation( $ip );
521 + }
522 +
523 + public function set_ipaddr_optout( string $ip ): void {
524 + $this->entity = $this->entity->withIpOptOut( $ip );
525 + }
526 +
527 + public function set_content( string $content ): void {
528 + $this->entity = $this->entity->withContent( $content );
529 + }
530 +
531 + public function set_files( string $files ): void {
532 + $this->entity = $this->entity->withFiles( $files );
533 + }
534 +
535 + public function set_category( int $categoryId ): void {
536 + $this->entity = $this->entity->withCategory( $categoryId );
537 + }
538 +
539 + public function set_form( string $form ): void {
540 + $this->entity = $this->entity->withForm( $form );
541 + }
542 +
543 + public function set_email( string $email ): void {
544 + $this->entity = $this->entity->withEmail( $email );
545 + }
546 +
547 + public function set_mail_optin( string $mail ): void {
548 + $this->entity = $this->entity->withMailOptIn( $mail );
549 + }
550 +
551 + public function set_consent_text( string $text ): void {
552 + $this->entity = $this->entity->withConsentText( $text );
553 + }
554 +
555 + // =========================================================================
556 + // TYPE CHECKS
557 + // =========================================================================
558 +
559 + public function isTypeCF7(): bool {
560 + return $this->isType( 'cf7' );
561 + }
562 +
563 + public function isTypeAvada(): bool {
564 + return $this->isType( 'avada' );
565 + }
566 +
567 + public function isTypeElementor(): bool {
568 + return $this->isType( 'elementor' );
569 + }
570 +
571 + public function isTypeWPForms(): bool {
572 + return $this->isType( 'wpforms' );
573 + }
574 +
575 + public function isTypeGravityForms(): bool {
576 + return $this->isType( 'gravityforms' );
577 + }
578 +
579 + public function isType( string $type ): bool {
580 + $formId = $this->get_cf_form_id();
581 +
582 + switch ( $type ) {
583 + case 'cf7':
584 + return get_post_type( $formId ) === 'wpcf7_contact_form';
585 + case 'avada':
586 + return get_post_type( $formId ) === 'fusion_form';
587 + case 'elementor':
588 + // Elementor forms are embedded in pages/posts, not stored as separate post types.
589 + // Check if the post has Elementor data AND DOI settings configured.
590 + $hasElementorData = ! empty( get_post_meta( $formId, '_elementor_data', true ) );
591 + $hasDoiSettings = ! empty( get_post_meta( $formId, 'f12-cf7-doubleoptin', true ) );
592 + return $hasElementorData && $hasDoiSettings;
593 + case 'wpforms':
594 + return get_post_type( $formId ) === 'wpforms';
595 + case 'gravityforms':
596 + // Gravity Forms stores forms in a custom table, not as posts.
597 + // Check if the form ID exists in the GF forms table.
598 + if ( class_exists( 'GFAPI' ) ) {
599 + $form = \GFAPI::get_form( $formId );
600 + return $form !== false && ! is_wp_error( $form );
601 + }
602 + return false;
603 + default:
604 + return false;
605 + }
606 + }
607 +
608 + // =========================================================================
609 + // PERSISTENCE
610 + // =========================================================================
611 +
612 + /**
613 + * Save the OptIn to the database.
614 + *
615 + * @return bool|int
616 + */
617 + public function save() {
618 + $this->logger->info( 'Saving OptIn', [
619 + 'plugin' => 'double-opt-in',
620 + 'id' => $this->entity->getId(),
621 + 'hash' => $this->entity->getHash(),
622 + ] );
623 +
624 + try {
625 + $repository = self::getRepository();
626 + $this->entity = $repository->save( $this->entity );
627 +
628 + $this->logger->info( 'OptIn saved successfully', [
629 + 'plugin' => 'double-opt-in',
630 + 'id' => $this->entity->getId(),
631 + 'hash' => $this->entity->getHash(),
632 + ] );
633 +
634 + return true;
635 + } catch ( \Exception $e ) {
636 + $this->logger->error( 'Failed to save OptIn', [
637 + 'plugin' => 'double-opt-in',
638 + 'error' => $e->getMessage(),
639 + ] );
640 + return false;
641 + }
642 + }
643 +
644 + // =========================================================================
645 + // LINK GENERATION
646 + // =========================================================================
647 +
648 + /**
649 + * Get the opt-in confirmation link.
650 + *
651 + * @param array $parameter Additional parameters.
652 + * @param int $formId Optional form ID override.
653 + *
654 + * @return string
655 + */
656 + public function get_link_optin( array $parameter = [], int $formId = 0 ): string {
657 + $formId = $formId > 0 ? $formId : $this->get_cf_form_id();
658 +
659 + $formParameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
660 + $pageId = (int) ( $formParameter['page'] ?? 0 );
661 +
662 + if ( $pageId <= 0 ) {
663 + return home_url( '?optin=' . $this->get_hash() );
664 + }
665 +
666 + $pageUrl = get_permalink( $pageId );
667 + if ( ! $pageUrl ) {
668 + return home_url( '?optin=' . $this->get_hash() );
669 + }
670 +
671 + $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
672 +
673 + return $pageUrl . $separator . 'optin=' . $this->get_hash();
674 + }
675 +
676 + /**
677 + * Get the opt-out link.
678 + *
679 + * @return string
680 + */
681 + public function get_link_optout(): string {
682 + $settings = CF7DoubleOptIn::getInstance()->getSettings();
683 + $pageId = (int) ( $settings['optout_page'] ?? 0 );
684 +
685 + if ( $pageId <= 0 ) {
686 + return home_url( '?optout=' . $this->get_hash() );
687 + }
688 +
689 + $pageUrl = get_permalink( $pageId );
690 + if ( ! $pageUrl ) {
691 + return home_url( '?optout=' . $this->get_hash() );
692 + }
693 +
694 + $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
695 +
696 + return $pageUrl . $separator . 'optout=' . $this->get_hash();
697 + }
698 +
699 + /**
700 + * Get the UI edit link.
701 + *
702 + * @return string
703 + */
704 + public function get_link_ui(): string {
705 + return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&view=single&hash=' . $this->get_hash() );
706 + }
707 +
708 + /**
709 + * Get the delete link.
710 + *
711 + * @return string
712 + */
713 + public function get_link_delete(): string {
714 + $nonce = wp_create_nonce( 'delete_optin_' . $this->get_hash() );
715 + return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&action=delete&hash=' . $this->get_hash() . '&_wpnonce=' . $nonce );
716 + }
717 +
718 + /**
719 + * Get the form name.
720 + *
721 + * @return string
722 + */
723 + public function get_form_name(): string {
724 + $formId = $this->get_cf_form_id();
725 +
726 + if ( $formId <= 0 ) {
727 + return __( 'Unknown', 'double-opt-in' );
728 + }
729 +
730 + $post = get_post( $formId );
731 + if ( ! $post ) {
732 + return __( 'Deleted Form', 'double-opt-in' ) . ' (ID: ' . $formId . ')';
733 + }
734 +
735 + return $post->post_title ?: __( 'Untitled Form', 'double-opt-in' );
736 + }
737 +
738 + /**
739 + * Get the form edit link.
740 + *
741 + * @return string
742 + */
743 + public function get_form_link(): string {
744 + $formId = $this->get_cf_form_id();
745 + $postType = get_post_type( $formId );
746 +
747 + if ( $postType === 'wpcf7_contact_form' ) {
748 + return admin_url( 'admin.php?page=wpcf7&post=' . $formId . '&action=edit' );
749 + }
750 +
751 + if ( $postType === 'fusion_form' ) {
752 + return admin_url( 'post.php?post=' . $formId . '&action=edit' );
753 + }
754 +
755 + return admin_url( 'post.php?post=' . $formId . '&action=edit' );
756 + }
757 +
758 + /**
759 + * Get form settings.
760 + *
761 + * @return array
762 + */
763 + public function get_form_settings(): array {
764 + return CF7DoubleOptIn::getInstance()->getParameter( $this->get_cf_form_id() );
765 + }
766 +
767 + // =========================================================================
768 + // HELPERS
769 + // =========================================================================
770 +
771 + /**
772 + * Get the repository instance.
773 + *
774 + * @return OptInRepositoryInterface
775 + */
776 + private static function getRepository(): OptInRepositoryInterface {
777 + $container = Container::getInstance();
778 + return $container->get( OptInRepositoryInterface::class );
779 + }
780 +
781 + /**
782 + * Map legacy property names to entity array keys.
783 + *
784 + * @param array $properties Legacy properties.
785 + *
786 + * @return array
787 + */
788 + private function mapToEntityArray( array $properties ): array {
789 + $mapping = [
790 + 'cf_form_id' => 'cf_form_id',
791 + 'doubleoptin' => 'doubleoptin',
792 + 'content' => 'content',
793 + 'hash' => 'hash',
794 + 'createtime' => 'createtime',
795 + 'updatetime' => 'updatetime',
796 + 'optouttime' => 'optouttime',
797 + 'ipaddr_register' => 'ipaddr_register',
798 + 'ipaddr_confirmation' => 'ipaddr_confirmation',
799 + 'ipaddr_optout' => 'ipaddr_optout',
800 + 'files' => 'files',
801 + 'category' => 'category',
802 + 'form' => 'form',
803 + 'mail_optin' => 'mail_optin',
804 + 'email' => 'email',
805 + 'id' => 'id',
806 + 'consent_text' => 'consent_text',
807 + 'consent_field' => 'consent_field',
808 + ];
809 +
810 + $result = [];
811 + foreach ( $mapping as $legacy => $entity ) {
812 + if ( isset( $properties[ $legacy ] ) ) {
813 + $result[ $entity ] = $properties[ $legacy ];
814 + }
815 + }
816 +
817 + return $result;
818 + }
819 +}