PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.3
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.3
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
double-opt-in / core / OptIn.class.php

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

820 lines 20.3 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 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 }
820