PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.12.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.12.0
5.12.0 5.13.0 5.13.1 5.11.0 5.10.0 5.9.0 5.8.0 5.8.1 5.7.0 5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 All 47 releases
double-opt-in / core / OptIn.class.php

OptIn.class.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.12.0, at core/OptIn.class.php

829 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 /**
363 * Instance of the form inside the form id (e.g. the Elementor widget);
364 * empty for records stored before 5.12.0 and for forms without instances.
365 */
366 public function get_form_ref(): string {
367 return $this->entity->getFormRef();
368 }
369
370 public function is_confirmed(): bool {
371 return $this->entity->isConfirmed();
372 }
373
374 public function get_doubleoptin(): int {
375 return $this->entity->isConfirmed() ? 1 : 0;
376 }
377
378 public function is_optout(): bool {
379 return $this->entity->isOptedOut();
380 }
381
382 public function get_content(): string {
383 return $this->entity->getContent();
384 }
385
386 public function get_createtime( string $view = 'raw' ): string {
387 if ( $view !== 'raw' ) {
388 return $this->entity->getCreateTimeFormatted( 'd.m.Y / H:i:s' );
389 }
390 return (string) $this->entity->getCreateTime();
391 }
392
393 public function get_updatetime( string $view = 'raw' ): string {
394 if ( $view !== 'raw' ) {
395 return $this->entity->getUpdateTimeFormatted( 'd.m.Y / H:i:s' );
396 }
397 return (string) $this->entity->getUpdateTime();
398 }
399
400 public function get_optouttime( string $view = 'raw' ): string {
401 $time = $this->entity->getOptOutTime();
402 if ( $view !== 'raw' && $time > 0 ) {
403 return wp_date( 'd.m.Y / H:i:s', $time );
404 }
405 return (string) $time;
406 }
407
408 /**
409 * Get create time as ISO 8601 string.
410 *
411 * @return string
412 */
413 public function get_createtime_iso(): string {
414 return $this->entity->getCreateTimeISO();
415 }
416
417 /**
418 * Get update time as ISO 8601 string.
419 *
420 * @return string
421 */
422 public function get_updatetime_iso(): string {
423 return $this->entity->getUpdateTimeISO();
424 }
425
426 /**
427 * Get opt-out time as ISO 8601 string.
428 *
429 * @return string
430 */
431 public function get_optouttime_iso(): string {
432 return $this->entity->getOptOutTimeISO();
433 }
434
435 public function get_ipaddr_register(): string {
436 return $this->entity->getIpRegister();
437 }
438
439 public function get_ipaddr_confirmation(): string {
440 return $this->entity->getIpConfirmation();
441 }
442
443 public function get_ipaddr_optout(): string {
444 return $this->entity->getIpOptOut();
445 }
446
447 public function get_files(): string {
448 return $this->entity->getFiles();
449 }
450
451 public function get_category(): int {
452 return $this->entity->getCategory();
453 }
454
455 public function get_email(): string {
456 return $this->entity->getEmail();
457 }
458
459 public function get_mail_optin(): string {
460 return $this->entity->getMailOptIn();
461 }
462
463 public function get_consent_text(): string {
464 return $this->entity->getConsentText();
465 }
466
467 public function get_form( bool $encoded = false ): string {
468 $form = $this->entity->getForm();
469 if ( $encoded ) {
470 return base64_encode( $form );
471 }
472 return $form;
473 }
474
475 /**
476 * Get the validity end date.
477 *
478 * @return string
479 */
480 public function get_valid_until(): string {
481 $settings = CF7DoubleOptIn::getInstance()->getSettings();
482 $dt = new \DateTime();
483 $dt->setTimestamp( $this->entity->getCreateTime() );
484
485 if ( $this->is_confirmed() ) {
486 $amount = (int) ( $settings['delete'] ?? 0 );
487 $period = $settings['delete_period'] ?? 'months';
488 } else {
489 $amount = (int) ( $settings['delete_unconfirmed'] ?? 0 );
490 $period = $settings['delete_unconfirmed_period'] ?? 'months';
491 }
492
493 $dt->modify( '+' . $amount . ' ' . $period );
494 $dt->modify( '+1 day' );
495
496 return $dt->format( 'd.m.Y' );
497 }
498
499 // =========================================================================
500 // SETTERS
501 // =========================================================================
502
503 public function set_cf_form_id( int $formId ): void {
504 $this->entity = $this->entity->withFormId( $formId );
505 }
506
507 public function set_doubleoptin( int $confirmed ): void {
508 $this->entity->setConfirmed( (bool) $confirmed );
509 }
510
511 public function set_createtime( $timestamp ): void {
512 $this->entity = $this->entity->withCreateTime( (int) $timestamp );
513 }
514
515 public function set_updatetime( $timestamp ): void {
516 $this->entity->setUpdateTime( (int) $timestamp );
517 }
518
519 public function set_optouttime( $timestamp ): void {
520 $this->entity = $this->entity->withOptOutTime( (int) $timestamp );
521 }
522
523 public function set_ipaddr_register( string $ip ): void {
524 $this->entity = $this->entity->withIpRegister( $ip );
525 }
526
527 public function set_ipaddr_confirmation( string $ip ): void {
528 $this->entity->setIpConfirmation( $ip );
529 }
530
531 public function set_ipaddr_optout( string $ip ): void {
532 $this->entity = $this->entity->withIpOptOut( $ip );
533 }
534
535 public function set_content( string $content ): void {
536 $this->entity = $this->entity->withContent( $content );
537 }
538
539 public function set_files( string $files ): void {
540 $this->entity = $this->entity->withFiles( $files );
541 }
542
543 public function set_category( int $categoryId ): void {
544 $this->entity = $this->entity->withCategory( $categoryId );
545 }
546
547 public function set_form( string $form ): void {
548 $this->entity = $this->entity->withForm( $form );
549 }
550
551 public function set_email( string $email ): void {
552 $this->entity = $this->entity->withEmail( $email );
553 }
554
555 public function set_mail_optin( string $mail ): void {
556 $this->entity = $this->entity->withMailOptIn( $mail );
557 }
558
559 public function set_consent_text( string $text ): void {
560 $this->entity = $this->entity->withConsentText( $text );
561 }
562
563 // =========================================================================
564 // TYPE CHECKS
565 // =========================================================================
566
567 public function isTypeCF7(): bool {
568 return $this->isType( 'cf7' );
569 }
570
571 public function isTypeAvada(): bool {
572 return $this->isType( 'avada' );
573 }
574
575 public function isTypeElementor(): bool {
576 return $this->isType( 'elementor' );
577 }
578
579 public function isTypeWPForms(): bool {
580 return $this->isType( 'wpforms' );
581 }
582
583 public function isTypeGravityForms(): bool {
584 return $this->isType( 'gravityforms' );
585 }
586
587 public function isType( string $type ): bool {
588 $formId = $this->get_cf_form_id();
589
590 switch ( $type ) {
591 case 'cf7':
592 return get_post_type( $formId ) === 'wpcf7_contact_form';
593 case 'avada':
594 return get_post_type( $formId ) === 'fusion_form';
595 case 'elementor':
596 // Elementor forms are embedded in pages/posts, not stored as separate post types.
597 // Check if the post has Elementor data AND DOI settings configured.
598 $hasElementorData = ! empty( get_post_meta( $formId, '_elementor_data', true ) );
599 $hasDoiSettings = ! empty( get_post_meta( $formId, 'f12-cf7-doubleoptin', true ) );
600 return $hasElementorData && $hasDoiSettings;
601 case 'wpforms':
602 return get_post_type( $formId ) === 'wpforms';
603 case 'gravityforms':
604 // Gravity Forms stores forms in a custom table, not as posts.
605 // Check if the form ID exists in the GF forms table.
606 if ( class_exists( 'GFAPI' ) ) {
607 $form = \GFAPI::get_form( $formId );
608 return $form !== false && ! is_wp_error( $form );
609 }
610 return false;
611 default:
612 return false;
613 }
614 }
615
616 // =========================================================================
617 // PERSISTENCE
618 // =========================================================================
619
620 /**
621 * Save the OptIn to the database.
622 *
623 * @return bool|int
624 */
625 public function save() {
626 $this->logger->info( 'Saving OptIn', [
627 'plugin' => 'double-opt-in',
628 'id' => $this->entity->getId(),
629 'hash' => $this->entity->getHash(),
630 ] );
631
632 try {
633 $repository = self::getRepository();
634 $this->entity = $repository->save( $this->entity );
635
636 $this->logger->info( 'OptIn saved successfully', [
637 'plugin' => 'double-opt-in',
638 'id' => $this->entity->getId(),
639 'hash' => $this->entity->getHash(),
640 ] );
641
642 return true;
643 } catch ( \Exception $e ) {
644 $this->logger->error( 'Failed to save OptIn', [
645 'plugin' => 'double-opt-in',
646 'error' => $e->getMessage(),
647 ] );
648 return false;
649 }
650 }
651
652 // =========================================================================
653 // LINK GENERATION
654 // =========================================================================
655
656 /**
657 * Get the opt-in confirmation link.
658 *
659 * @param array $parameter Additional parameters.
660 * @param int $formId Optional form ID override.
661 *
662 * @return string
663 */
664 public function get_link_optin( array $parameter = [], int $formId = 0 ): string {
665 $formId = $formId > 0 ? $formId : $this->get_cf_form_id();
666
667 $formParameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
668 $pageId = (int) ( $formParameter['page'] ?? 0 );
669
670 if ( $pageId <= 0 ) {
671 return home_url( '?optin=' . $this->get_hash() );
672 }
673
674 $pageUrl = get_permalink( $pageId );
675 if ( ! $pageUrl ) {
676 return home_url( '?optin=' . $this->get_hash() );
677 }
678
679 $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
680
681 return $pageUrl . $separator . 'optin=' . $this->get_hash();
682 }
683
684 /**
685 * Get the opt-out link.
686 *
687 * @return string
688 */
689 public function get_link_optout(): string {
690 $settings = CF7DoubleOptIn::getInstance()->getSettings();
691 $pageId = (int) ( $settings['optout_page'] ?? 0 );
692
693 if ( $pageId <= 0 ) {
694 return home_url( '?optout=' . $this->get_hash() );
695 }
696
697 $pageUrl = get_permalink( $pageId );
698 if ( ! $pageUrl ) {
699 return home_url( '?optout=' . $this->get_hash() );
700 }
701
702 $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
703
704 return $pageUrl . $separator . 'optout=' . $this->get_hash();
705 }
706
707 /**
708 * Get the UI edit link.
709 *
710 * @return string
711 */
712 public function get_link_ui(): string {
713 return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&view=single&hash=' . $this->get_hash() );
714 }
715
716 /**
717 * Get the delete link.
718 *
719 * @return string
720 */
721 public function get_link_delete(): string {
722 $nonce = wp_create_nonce( 'delete_optin_' . $this->get_hash() );
723 return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&action=delete&hash=' . $this->get_hash() . '&_wpnonce=' . $nonce );
724 }
725
726 /**
727 * Get the form name.
728 *
729 * @return string
730 */
731 public function get_form_name(): string {
732 $formId = $this->get_cf_form_id();
733
734 if ( $formId <= 0 ) {
735 return __( 'Unknown', 'double-opt-in' );
736 }
737
738 $post = get_post( $formId );
739 if ( ! $post ) {
740 return __( 'Deleted Form', 'double-opt-in' ) . ' (ID: ' . $formId . ')';
741 }
742
743 return $post->post_title ?: __( 'Untitled Form', 'double-opt-in' );
744 }
745
746 /**
747 * Get the form edit link.
748 *
749 * @return string
750 */
751 public function get_form_link(): string {
752 $formId = $this->get_cf_form_id();
753 $postType = get_post_type( $formId );
754
755 if ( $postType === 'wpcf7_contact_form' ) {
756 return admin_url( 'admin.php?page=wpcf7&post=' . $formId . '&action=edit' );
757 }
758
759 if ( $postType === 'fusion_form' ) {
760 return admin_url( 'post.php?post=' . $formId . '&action=edit' );
761 }
762
763 return admin_url( 'post.php?post=' . $formId . '&action=edit' );
764 }
765
766 /**
767 * Get form settings.
768 *
769 * @return array
770 */
771 public function get_form_settings(): array {
772 return CF7DoubleOptIn::getInstance()->getParameter( $this->get_cf_form_id() );
773 }
774
775 // =========================================================================
776 // HELPERS
777 // =========================================================================
778
779 /**
780 * Get the repository instance.
781 *
782 * @return OptInRepositoryInterface
783 */
784 private static function getRepository(): OptInRepositoryInterface {
785 $container = Container::getInstance();
786 return $container->get( OptInRepositoryInterface::class );
787 }
788
789 /**
790 * Map legacy property names to entity array keys.
791 *
792 * @param array $properties Legacy properties.
793 *
794 * @return array
795 */
796 private function mapToEntityArray( array $properties ): array {
797 $mapping = [
798 'cf_form_id' => 'cf_form_id',
799 'doubleoptin' => 'doubleoptin',
800 'content' => 'content',
801 'hash' => 'hash',
802 'createtime' => 'createtime',
803 'updatetime' => 'updatetime',
804 'optouttime' => 'optouttime',
805 'ipaddr_register' => 'ipaddr_register',
806 'ipaddr_confirmation' => 'ipaddr_confirmation',
807 'ipaddr_optout' => 'ipaddr_optout',
808 'files' => 'files',
809 'category' => 'category',
810 'form' => 'form',
811 'mail_optin' => 'mail_optin',
812 'email' => 'email',
813 'id' => 'id',
814 'consent_text' => 'consent_text',
815 'consent_field' => 'consent_field',
816 'form_ref' => 'form_ref',
817 ];
818
819 $result = [];
820 foreach ( $mapping as $legacy => $entity ) {
821 if ( isset( $properties[ $legacy ] ) ) {
822 $result[ $entity ] = $properties[ $legacy ];
823 }
824 }
825
826 return $result;
827 }
828 }
829