PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 3.1.0 All 34 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.5.0, at core/OptIn.class.php

780 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 count by form ID.
116 *
117 * @param int $formId The form ID.
118 *
119 * @return int
120 */
121 public static function get_count( int $formId ): int {
122 try {
123 return self::getRepository()->countByFormId( $formId );
124 } catch ( \Exception $e ) {
125 return 0;
126 }
127 }
128
129 /**
130 * Get list of OptIns with pagination.
131 *
132 * @param array $atts Query attributes.
133 * @param int|null $numberOfPages Reference to store page count.
134 *
135 * @return array<OptIn>
136 */
137 public static function get_list( array $atts = [], ?int &$numberOfPages = null ): array {
138 $logger = Logger::getInstance();
139
140 try {
141 $repository = self::getRepository();
142 $entities = $repository->findAll( $atts, $numberOfPages );
143
144 return array_map( function ( OptInEntity $entity ) use ( $logger ) {
145 $optIn = new self( $logger );
146 $optIn->entity = $entity;
147 return $optIn;
148 }, $entities );
149 } catch ( \Exception $e ) {
150 $logger->error( 'Failed to get OptIn list', [
151 'plugin' => 'double-opt-in',
152 'error' => $e->getMessage(),
153 ] );
154 return [];
155 }
156 }
157
158 /**
159 * Get list by category ID.
160 *
161 * @param int $categoryId The category ID.
162 * @param array $atts Query attributes.
163 * @param int|null $numberOfPages Reference to store page count.
164 *
165 * @return array<OptIn>
166 */
167 public static function get_list_by_category_id( int $categoryId, array $atts = [], ?int &$numberOfPages = null ): array {
168 $logger = Logger::getInstance();
169
170 try {
171 $repository = self::getRepository();
172
173 // Get total count for pagination
174 if ( $numberOfPages !== null ) {
175 $keyword = $atts['keyword'] ?? '';
176 $perPage = max( 1, (int) ( $atts['perPage'] ?? 10 ) );
177 $total = $repository->countByCategory( $categoryId, $keyword );
178 $numberOfPages = $total > 0 ? (int) ceil( $total / $perPage ) : 0;
179
180 if ( $numberOfPages === 0 ) {
181 return [];
182 }
183 }
184
185 $entities = $repository->findByCategory( $categoryId, $atts );
186
187 return array_map( function ( OptInEntity $entity ) use ( $logger ) {
188 $optIn = new self( $logger );
189 $optIn->entity = $entity;
190 return $optIn;
191 }, $entities );
192 } catch ( \Exception $e ) {
193 $logger->error( 'Failed to get OptIn list by category', [
194 'plugin' => 'double-opt-in',
195 'category' => $categoryId,
196 'error' => $e->getMessage(),
197 ] );
198 return [];
199 }
200 }
201
202 /**
203 * Get list by email.
204 *
205 * @param string $email The email address.
206 *
207 * @return array<OptIn>
208 */
209 public static function get_list_by_email( string $email ): array {
210 $logger = Logger::getInstance();
211
212 try {
213 $repository = self::getRepository();
214 $entities = $repository->findByEmail( $email );
215
216 return array_map( function ( OptInEntity $entity ) use ( $logger ) {
217 $optIn = new self( $logger );
218 $optIn->entity = $entity;
219 return $optIn;
220 }, $entities );
221 } catch ( \Exception $e ) {
222 return [];
223 }
224 }
225
226 /**
227 * Get confirmed list by email.
228 *
229 * @param string $email The email address.
230 *
231 * @return array<OptIn>
232 */
233 public static function get_list_by_email_confirmed( string $email ): array {
234 $logger = Logger::getInstance();
235
236 try {
237 $repository = self::getRepository();
238 $entities = $repository->findConfirmedByEmail( $email );
239
240 return array_map( function ( OptInEntity $entity ) use ( $logger ) {
241 $optIn = new self( $logger );
242 $optIn->entity = $entity;
243 return $optIn;
244 }, $entities );
245 } catch ( \Exception $e ) {
246 return [];
247 }
248 }
249
250 /**
251 * Get unconfirmed list by email.
252 *
253 * @param string $email The email address.
254 *
255 * @return array<OptIn>
256 */
257 public static function get_list_by_email_unconfirmed( string $email ): array {
258 $logger = Logger::getInstance();
259
260 try {
261 $repository = self::getRepository();
262 $entities = $repository->findUnconfirmedByEmail( $email );
263
264 return array_map( function ( OptInEntity $entity ) use ( $logger ) {
265 $optIn = new self( $logger );
266 $optIn->entity = $entity;
267 return $optIn;
268 }, $entities );
269 } catch ( \Exception $e ) {
270 return [];
271 }
272 }
273
274 /**
275 * Bulk update category.
276 *
277 * @param int $fromId The source category ID.
278 * @param int $toId The target category ID.
279 *
280 * @return int
281 */
282 public static function bulk_update_category( int $fromId, int $toId ): int {
283 try {
284 return self::getRepository()->bulkUpdateCategory( $fromId, $toId );
285 } catch ( \Exception $e ) {
286 return 0;
287 }
288 }
289
290 /**
291 * Update category by OptIn ID.
292 *
293 * @param int $optInId The OptIn ID.
294 * @param int $categoryId The new category ID.
295 *
296 * @return bool
297 */
298 public static function update_category_by_id( int $optInId, int $categoryId ): bool {
299 try {
300 return self::getRepository()->updateCategoryById( $optInId, $categoryId );
301 } catch ( \Exception $e ) {
302 return false;
303 }
304 }
305
306 // =========================================================================
307 // GETTERS
308 // =========================================================================
309
310 public function get_id(): int {
311 return $this->entity->getId();
312 }
313
314 public function get_hash(): string {
315 return $this->entity->getHash();
316 }
317
318 public function get_cf_form_id(): int {
319 return $this->entity->getFormId();
320 }
321
322 public function is_confirmed(): bool {
323 return $this->entity->isConfirmed();
324 }
325
326 public function get_doubleoptin(): int {
327 return $this->entity->isConfirmed() ? 1 : 0;
328 }
329
330 public function is_optout(): bool {
331 return $this->entity->isOptedOut();
332 }
333
334 public function get_content(): string {
335 return $this->entity->getContent();
336 }
337
338 public function get_createtime( string $view = 'raw' ): string {
339 if ( $view !== 'raw' ) {
340 return $this->entity->getCreateTimeFormatted( 'd.m.Y / H:i:s' );
341 }
342 return (string) $this->entity->getCreateTime();
343 }
344
345 public function get_updatetime( string $view = 'raw' ): string {
346 if ( $view !== 'raw' ) {
347 return $this->entity->getUpdateTimeFormatted( 'd.m.Y / H:i:s' );
348 }
349 return (string) $this->entity->getUpdateTime();
350 }
351
352 public function get_optouttime( string $view = 'raw' ): string {
353 $time = $this->entity->getOptOutTime();
354 if ( $view !== 'raw' && $time > 0 ) {
355 return wp_date( 'd.m.Y / H:i:s', $time );
356 }
357 return (string) $time;
358 }
359
360 /**
361 * Get create time as ISO 8601 string.
362 *
363 * @return string
364 */
365 public function get_createtime_iso(): string {
366 return $this->entity->getCreateTimeISO();
367 }
368
369 /**
370 * Get update time as ISO 8601 string.
371 *
372 * @return string
373 */
374 public function get_updatetime_iso(): string {
375 return $this->entity->getUpdateTimeISO();
376 }
377
378 /**
379 * Get opt-out time as ISO 8601 string.
380 *
381 * @return string
382 */
383 public function get_optouttime_iso(): string {
384 return $this->entity->getOptOutTimeISO();
385 }
386
387 public function get_ipaddr_register(): string {
388 return $this->entity->getIpRegister();
389 }
390
391 public function get_ipaddr_confirmation(): string {
392 return $this->entity->getIpConfirmation();
393 }
394
395 public function get_ipaddr_optout(): string {
396 return $this->entity->getIpOptOut();
397 }
398
399 public function get_files(): string {
400 return $this->entity->getFiles();
401 }
402
403 public function get_category(): int {
404 return $this->entity->getCategory();
405 }
406
407 public function get_email(): string {
408 return $this->entity->getEmail();
409 }
410
411 public function get_mail_optin(): string {
412 return $this->entity->getMailOptIn();
413 }
414
415 public function get_consent_text(): string {
416 return $this->entity->getConsentText();
417 }
418
419 public function get_form( bool $encoded = false ): string {
420 $form = $this->entity->getForm();
421 if ( $encoded ) {
422 return base64_encode( $form );
423 }
424 return $form;
425 }
426
427 /**
428 * Get the validity end date.
429 *
430 * @return string
431 */
432 public function get_valid_until(): string {
433 $settings = CF7DoubleOptIn::getInstance()->getSettings();
434 $dt = new \DateTime();
435 $dt->setTimestamp( $this->entity->getCreateTime() );
436
437 if ( $this->is_confirmed() ) {
438 $amount = (int) ( $settings['delete'] ?? 0 );
439 $period = $settings['delete_period'] ?? 'months';
440 } else {
441 $amount = (int) ( $settings['delete_unconfirmed'] ?? 0 );
442 $period = $settings['delete_unconfirmed_period'] ?? 'months';
443 }
444
445 $dt->modify( '+' . $amount . ' ' . $period );
446 $dt->modify( '+1 day' );
447
448 return $dt->format( 'd.m.Y' );
449 }
450
451 // =========================================================================
452 // SETTERS
453 // =========================================================================
454
455 public function set_cf_form_id( int $formId ): void {
456 $this->entity = $this->entity->withFormId( $formId );
457 }
458
459 public function set_doubleoptin( int $confirmed ): void {
460 $this->entity->setConfirmed( (bool) $confirmed );
461 }
462
463 public function set_createtime( $timestamp ): void {
464 $this->entity = $this->entity->withCreateTime( (int) $timestamp );
465 }
466
467 public function set_updatetime( $timestamp ): void {
468 $this->entity->setUpdateTime( (int) $timestamp );
469 }
470
471 public function set_optouttime( $timestamp ): void {
472 $this->entity = $this->entity->withOptOutTime( (int) $timestamp );
473 }
474
475 public function set_ipaddr_register( string $ip ): void {
476 $this->entity = $this->entity->withIpRegister( $ip );
477 }
478
479 public function set_ipaddr_confirmation( string $ip ): void {
480 $this->entity->setIpConfirmation( $ip );
481 }
482
483 public function set_ipaddr_optout( string $ip ): void {
484 $this->entity = $this->entity->withIpOptOut( $ip );
485 }
486
487 public function set_content( string $content ): void {
488 $this->entity = $this->entity->withContent( $content );
489 }
490
491 public function set_files( string $files ): void {
492 $this->entity = $this->entity->withFiles( $files );
493 }
494
495 public function set_category( int $categoryId ): void {
496 $this->entity = $this->entity->withCategory( $categoryId );
497 }
498
499 public function set_form( string $form ): void {
500 $this->entity = $this->entity->withForm( $form );
501 }
502
503 public function set_email( string $email ): void {
504 $this->entity = $this->entity->withEmail( $email );
505 }
506
507 public function set_mail_optin( string $mail ): void {
508 $this->entity = $this->entity->withMailOptIn( $mail );
509 }
510
511 public function set_consent_text( string $text ): void {
512 $this->entity = $this->entity->withConsentText( $text );
513 }
514
515 // =========================================================================
516 // TYPE CHECKS
517 // =========================================================================
518
519 public function isTypeCF7(): bool {
520 return $this->isType( 'cf7' );
521 }
522
523 public function isTypeAvada(): bool {
524 return $this->isType( 'avada' );
525 }
526
527 public function isTypeElementor(): bool {
528 return $this->isType( 'elementor' );
529 }
530
531 public function isTypeWPForms(): bool {
532 return $this->isType( 'wpforms' );
533 }
534
535 public function isTypeGravityForms(): bool {
536 return $this->isType( 'gravityforms' );
537 }
538
539 public function isType( string $type ): bool {
540 $formId = $this->get_cf_form_id();
541
542 switch ( $type ) {
543 case 'cf7':
544 return get_post_type( $formId ) === 'wpcf7_contact_form';
545 case 'avada':
546 return get_post_type( $formId ) === 'fusion_form';
547 case 'elementor':
548 // Elementor forms are embedded in pages/posts, not stored as separate post types.
549 // Check if the post has Elementor data AND DOI settings configured.
550 $hasElementorData = ! empty( get_post_meta( $formId, '_elementor_data', true ) );
551 $hasDoiSettings = ! empty( get_post_meta( $formId, 'f12-cf7-doubleoptin', true ) );
552 return $hasElementorData && $hasDoiSettings;
553 case 'wpforms':
554 return get_post_type( $formId ) === 'wpforms';
555 case 'gravityforms':
556 // Gravity Forms stores forms in a custom table, not as posts.
557 // Check if the form ID exists in the GF forms table.
558 if ( class_exists( 'GFAPI' ) ) {
559 $form = \GFAPI::get_form( $formId );
560 return $form !== false && ! is_wp_error( $form );
561 }
562 return false;
563 default:
564 return false;
565 }
566 }
567
568 // =========================================================================
569 // PERSISTENCE
570 // =========================================================================
571
572 /**
573 * Save the OptIn to the database.
574 *
575 * @return bool|int
576 */
577 public function save() {
578 $this->logger->info( 'Saving OptIn', [
579 'plugin' => 'double-opt-in',
580 'id' => $this->entity->getId(),
581 'hash' => $this->entity->getHash(),
582 ] );
583
584 try {
585 $repository = self::getRepository();
586 $this->entity = $repository->save( $this->entity );
587
588 $this->logger->info( 'OptIn saved successfully', [
589 'plugin' => 'double-opt-in',
590 'id' => $this->entity->getId(),
591 'hash' => $this->entity->getHash(),
592 ] );
593
594 return true;
595 } catch ( \Exception $e ) {
596 $this->logger->error( 'Failed to save OptIn', [
597 'plugin' => 'double-opt-in',
598 'error' => $e->getMessage(),
599 ] );
600 return false;
601 }
602 }
603
604 // =========================================================================
605 // LINK GENERATION
606 // =========================================================================
607
608 /**
609 * Get the opt-in confirmation link.
610 *
611 * @param array $parameter Additional parameters.
612 * @param int $formId Optional form ID override.
613 *
614 * @return string
615 */
616 public function get_link_optin( array $parameter = [], int $formId = 0 ): string {
617 $formId = $formId > 0 ? $formId : $this->get_cf_form_id();
618
619 $formParameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
620 $pageId = (int) ( $formParameter['page'] ?? 0 );
621
622 if ( $pageId <= 0 ) {
623 return home_url( '?optin=' . $this->get_hash() );
624 }
625
626 $pageUrl = get_permalink( $pageId );
627 if ( ! $pageUrl ) {
628 return home_url( '?optin=' . $this->get_hash() );
629 }
630
631 $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
632
633 return $pageUrl . $separator . 'optin=' . $this->get_hash();
634 }
635
636 /**
637 * Get the opt-out link.
638 *
639 * @return string
640 */
641 public function get_link_optout(): string {
642 $settings = CF7DoubleOptIn::getInstance()->getSettings();
643 $pageId = (int) ( $settings['optout_page'] ?? 0 );
644
645 if ( $pageId <= 0 ) {
646 return home_url( '?optout=' . $this->get_hash() );
647 }
648
649 $pageUrl = get_permalink( $pageId );
650 if ( ! $pageUrl ) {
651 return home_url( '?optout=' . $this->get_hash() );
652 }
653
654 $separator = strpos( $pageUrl, '?' ) !== false ? '&' : '?';
655
656 return $pageUrl . $separator . 'optout=' . $this->get_hash();
657 }
658
659 /**
660 * Get the UI edit link.
661 *
662 * @return string
663 */
664 public function get_link_ui(): string {
665 return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&view=single&hash=' . $this->get_hash() );
666 }
667
668 /**
669 * Get the delete link.
670 *
671 * @return string
672 */
673 public function get_link_delete(): string {
674 $nonce = wp_create_nonce( 'delete_optin_' . $this->get_hash() );
675 return admin_url( 'admin.php?page=' . FORGE12_OPTIN_SLUG . '&action=delete&hash=' . $this->get_hash() . '&_wpnonce=' . $nonce );
676 }
677
678 /**
679 * Get the form name.
680 *
681 * @return string
682 */
683 public function get_form_name(): string {
684 $formId = $this->get_cf_form_id();
685
686 if ( $formId <= 0 ) {
687 return __( 'Unknown', 'double-opt-in' );
688 }
689
690 $post = get_post( $formId );
691 if ( ! $post ) {
692 return __( 'Deleted Form', 'double-opt-in' ) . ' (ID: ' . $formId . ')';
693 }
694
695 return $post->post_title ?: __( 'Untitled Form', 'double-opt-in' );
696 }
697
698 /**
699 * Get the form edit link.
700 *
701 * @return string
702 */
703 public function get_form_link(): string {
704 $formId = $this->get_cf_form_id();
705 $postType = get_post_type( $formId );
706
707 if ( $postType === 'wpcf7_contact_form' ) {
708 return admin_url( 'admin.php?page=wpcf7&post=' . $formId . '&action=edit' );
709 }
710
711 if ( $postType === 'fusion_form' ) {
712 return admin_url( 'post.php?post=' . $formId . '&action=edit' );
713 }
714
715 return admin_url( 'post.php?post=' . $formId . '&action=edit' );
716 }
717
718 /**
719 * Get form settings.
720 *
721 * @return array
722 */
723 public function get_form_settings(): array {
724 return CF7DoubleOptIn::getInstance()->getParameter( $this->get_cf_form_id() );
725 }
726
727 // =========================================================================
728 // HELPERS
729 // =========================================================================
730
731 /**
732 * Get the repository instance.
733 *
734 * @return OptInRepositoryInterface
735 */
736 private static function getRepository(): OptInRepositoryInterface {
737 $container = Container::getInstance();
738 return $container->get( OptInRepositoryInterface::class );
739 }
740
741 /**
742 * Map legacy property names to entity array keys.
743 *
744 * @param array $properties Legacy properties.
745 *
746 * @return array
747 */
748 private function mapToEntityArray( array $properties ): array {
749 $mapping = [
750 'cf_form_id' => 'cf_form_id',
751 'doubleoptin' => 'doubleoptin',
752 'content' => 'content',
753 'hash' => 'hash',
754 'createtime' => 'createtime',
755 'updatetime' => 'updatetime',
756 'optouttime' => 'optouttime',
757 'ipaddr_register' => 'ipaddr_register',
758 'ipaddr_confirmation' => 'ipaddr_confirmation',
759 'ipaddr_optout' => 'ipaddr_optout',
760 'files' => 'files',
761 'category' => 'category',
762 'form' => 'form',
763 'mail_optin' => 'mail_optin',
764 'email' => 'email',
765 'id' => 'id',
766 'consent_text' => 'consent_text',
767 'consent_field' => 'consent_field',
768 ];
769
770 $result = [];
771 foreach ( $mapping as $legacy => $entity ) {
772 if ( isset( $properties[ $legacy ] ) ) {
773 $result[ $entity ] = $properties[ $legacy ];
774 }
775 }
776
777 return $result;
778 }
779 }
780