PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.1.6
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.1.6
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 / CF7DoubleOptIn.class.php

CF7DoubleOptIn.class.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.1.6, at CF7DoubleOptIn.class.php

705 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace forge12\contactform7\CF7DoubleOptIn {
4
5 use Forge12\Shared\Logger;
6 use Forge12\Shared\LoggerInterface;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Plugin Name: Double Opt-In (Contact Form 7, Avada) - GDPR Ready
14 * Plugin URI: https://www.forge12.com/blog/so-verwendest-du-das-double-opt-in-fuer-contact-form-7/
15 * Description: This plugin allows you to add a double OptIn System to your Contact Form 7 & Avada Forms.
16 * Text Domain: double-opt-in
17 * Domain Path: /languages
18 * Version: 5.1.6
19 * Requires at least: 6.0
20 * Requires PHP: 7.4
21 * Author: Forge12 Interactive GmbH
22 * Author URI: https://www.forge12.com
23 */
24
25 /**
26 * Minimum-PHP fail-safe.
27 *
28 * The "Requires PHP" header above makes WordPress refuse *activation* and
29 * *updates* on an unsupported version, but it is not re-checked when a host
30 * later moves an already-active site to an older PHP. Without this guard the
31 * next request would fatal on 7.4-only syntax inside the files required
32 * below, leaving the site with a white screen and no explanation.
33 *
34 * Everything above this point must stay parseable by old PHP — a parse error
35 * happens before any code runs, so a guard in an unparseable file is dead
36 * weight. That is also why CF7DoubleOptIn::$logger carries its type in a
37 * DocBlock instead of a native (PHP 7.4) property type.
38 */
39 if ( PHP_VERSION_ID < 70400 ) {
40 add_action(
41 'admin_notices',
42 function () {
43 echo '<div class="notice notice-error"><p>';
44 echo esc_html(
45 sprintf(
46 /* translators: 1: minimum required PHP version, 2: PHP version currently running */
47 __( 'Double Opt-In requires PHP %1$s or newer. This server is running PHP %2$s, so the plugin was stopped to prevent a fatal error. Please ask your host to update PHP.', 'double-opt-in' ),
48 '7.4',
49 PHP_VERSION
50 )
51 );
52 echo '</p></div>';
53 }
54 );
55
56 return;
57 }
58
59 if ( ! defined( 'FORGE12_OPTIN_VERSION' ) ) {
60 define( 'FORGE12_OPTIN_VERSION', '5.1.6' );
61 }
62
63 // Addon API version — semver-independent from the plugin's marketing
64 // version. Bumped only on breaking changes to the Addon API surface
65 // (AddonInterface, AddonRegistry, AddonLicenseRegistry, FormIntegrationInterface,
66 // event payloads). Addons declare their requirement against this constant,
67 // not FORGE12_OPTIN_VERSION.
68 if ( ! defined( 'F12_DOI_CORE_API_VERSION' ) ) {
69 define( 'F12_DOI_CORE_API_VERSION', '4.3.0' );
70 }
71 if ( ! defined( 'FORGE12_OPTIN_SLUG' ) ) {
72 define( 'FORGE12_OPTIN_SLUG', 'f12-cf7-doubleoptin' );
73 }
74 if ( ! defined( 'FORGE12_OPTIN_BASENAME' ) ) {
75 define( 'FORGE12_OPTIN_BASENAME', plugin_basename( __FILE__ ) );
76 }
77 if ( ! defined( 'F12_DOUBLEOPTIN_PLUGIN_FILE' ) ) {
78 define( 'F12_DOUBLEOPTIN_PLUGIN_FILE', __FILE__ );
79 }
80
81
82 /**
83 * Dependencies
84 */
85 require_once 'logger/logger.php';
86 require_once 'core/helpers/uuid.php';
87 require_once 'core/telemetry.php';
88 require_once 'core/review.php';
89 require_once 'core/cron.php';
90 require_once 'core/BaseController.class.php';
91
92 require_once 'OnActivation.php';
93 require_once 'OnDeactivation.php';
94 require_once 'OnUpdate.php';
95 require_once 'compatibility/OptInFrontend.class.php';
96 require_once 'core/SpamMechanics.class.php';
97
98 require_once 'core/Messages.class.php';
99 require_once 'core/TemplateHandler.class.php';
100 require_once 'core/IPHelper.class.php';
101 require_once 'core/SanitizeHelper.class.php';
102 require_once 'core/Ajax.class.php';
103 require_once 'core/Compatibility.class.php';
104 require_once 'core/CleanUp.class.php';
105 require_once 'core/HTMLSelect.class.php';
106 require_once 'core/OptIn.class.php';
107 require_once 'core/OptInLimitFilter.class.php';
108 require_once 'core/OptInSearchFilter.class.php';
109 require_once 'core/Category.class.php';
110 require_once 'core/CategoryOptions.class.php';
111 require_once 'core/Pagination.class.php';
112 if ( file_exists( __DIR__ . '/core/TestEmailBlocker.class.php' ) ) {
113 require_once 'core/TestEmailBlocker.class.php';
114 }
115
116 /**
117 * PSR-4 Autoloader for new Enterprise Architecture (v4.0+)
118 */
119 require_once 'autoload.php';
120
121 /**
122 * Class CF7DoubleOptIn
123 * Controller for the Custom Links.
124 *
125 * @package forge12\contactform7
126 */
127 class CF7DoubleOptIn {
128 /**
129 * Deliberately untyped: a native property type is PHP 7.4 syntax and
130 * would make this file unparseable on older PHP, which would defeat the
131 * minimum-PHP guard at the top of this file.
132 *
133 * @var LoggerInterface
134 */
135 private $logger;
136 /**
137 * @var CF7DoubleOptIn|Null
138 */
139 private static $_instance = null;
140
141 /**
142 * @var TemplateHandler|null
143 */
144 private $TemplateHandler = null;
145
146 /**
147 * Get the singleton instance of CF7DoubleOptIn.
148 *
149 * @return CF7DoubleOptIn The singleton instance.
150 */
151 public static function getInstance() {
152 if ( self::$_instance == null ) {
153 self::$_instance = new self();
154 }
155
156 return self::$_instance;
157 }
158
159 /**
160 * Return a list containing the array with all data stored within the form
161 *
162 * @param int $postID
163 *
164 * @formatter:off
165 *
166 * @return {
167 * @type int $enable The Status of the OptIn, either 1 for enabled or 0 for disabled. Default: 0
168 * @type string $sender The E-Mail of the sender of the optIn mail
169 * @type string $subject The Subject of the OptIn Mail
170 * @type string $body The Content of the OptIn Mail
171 * @type string $recipient The Field that contains the E-Mail of the Recipient.
172 * @type int $page The Post ID of the confirmation page. Default: -1
173 * @type string $conditions Additional condition to dynamically enable / disable the optin.
174 * Default: disabled
175 * @type string $template The Template used for the OptIn Mail
176 * @type int $category The Category the OptIns will be assigned to.
177 * }
178 * @formatter:on
179 */
180 public function getParameter( $postID ) {
181 $this->get_logger()->debug(
182 'Fetching parameters',
183 array(
184 'plugin' => 'double-opt-in',
185 'class' => __CLASS__,
186 'method' => __METHOD__,
187 'post_id' => $postID,
188 )
189 );
190
191 $data = array(
192 'enable' => 0,
193 'sender' => get_bloginfo( 'admin_email' ),
194 'sender_name' => '',
195 'subject' => '',
196 'body' => '',
197 'recipient' => '',
198 'page' => - 1,
199 'conditions' => 'disabled',
200 'template' => '',
201 'category' => 0,
202 );
203
204 $data = apply_filters( 'f12_cf7_doubleoptin_get_parameter', $data );
205
206 if ( ! $postID ) {
207 $this->get_logger()->debug(
208 'No postID provided, returning defaults',
209 array(
210 'plugin' => 'double-opt-in',
211 )
212 );
213
214 return $data;
215 }
216
217 $options = get_post_meta( $postID, 'f12-cf7-doubleoptin', true );
218
219 if ( ! $options ) {
220 $this->get_logger()->debug(
221 'No options found for postID, returning defaults',
222 array(
223 'plugin' => 'double-opt-in',
224 'post_id' => $postID,
225 )
226 );
227
228 return $data;
229 }
230
231 $this->get_logger()->debug(
232 'Options merged with defaults',
233 array(
234 'plugin' => 'double-opt-in',
235 'post_id' => $postID,
236 )
237 );
238
239 return array_merge( $data, $options );
240 }
241
242 /**
243 * Private constructor to prevent direct instantiation.
244 */
245 private function __construct() {
246 $this->logger = Logger::getInstance();
247
248 // Initialize test email blocker (blocks @example.com during E2E tests)
249 if ( class_exists( __NAMESPACE__ . '\\TestEmailBlocker' ) ) {
250 TestEmailBlocker::init();
251 }
252
253 // Initialize the DI Container and Service Providers (v4.0+ Enterprise Architecture)
254 $this->initializeContainer();
255
256 // Register the Avada deprecation notice + grandfather-license claim flow.
257 // Covers the migration of Avada support out of Core into the paid
258 // addon-avada plugin planned for 5.0. The notice only renders on
259 // sites that actually use DOI with an Avada form.
260 \Forge12\DoubleOptIn\Migration\AvadaDeprecationNotice::register();
261
262 if ( ! get_option( 'f12_cf7_doubleoptin_installed_at' ) ) {
263 update_option( 'f12_cf7_doubleoptin_installed_at', time() );
264 }
265
266 // Handle Spam Mechanics
267 $SpamMechanics = new SpamMechanics( $this->logger );
268
269 // Resend Confirmation Mail (Admin AJAX)
270 new \Forge12\DoubleOptIn\Admin\ResendController( $this->logger );
271
272 $this->get_logger()->info(
273 'Initialization of Forge12 Double Opt-In started',
274 array(
275 'plugin' => 'double-opt-in',
276 'class' => __CLASS__,
277 'method' => __METHOD__,
278 )
279 );
280
281 add_action(
282 'init',
283 function () {
284 load_plugin_textdomain(
285 'double-opt-in',
286 false,
287 dirname( plugin_basename( __FILE__ ) ) . '/languages'
288 );
289 $this->get_logger()->debug(
290 'Textdomain loaded',
291 array(
292 'plugin' => 'double-opt-in',
293 'domain' => 'double-opt-in',
294 )
295 );
296 }
297 );
298
299 do_action( 'f12_cf7_doubleoptin_init', $this );
300 $this->get_logger()->debug(
301 'Action f12_cf7_doubleoptin_init executed',
302 array(
303 'plugin' => 'double-opt-in',
304 )
305 );
306
307 $this->TemplateHandler = TemplateHandler::getInstance();
308 $this->get_logger()->debug(
309 'TemplateHandler initialized',
310 array(
311 'plugin' => 'double-opt-in',
312 )
313 );
314
315 // Settings-defaults filter — historically registered by the legacy
316 // admin UI (UISettings::getSettings). Registered here at runtime so
317 // getSettings() keeps its default key set (and the whitelist it builds
318 // from it) even without the legacy admin. The test-override mu-plugin
319 // and any addon still layer on top of the filter chain.
320 add_filter( 'f12_cf7_doubleoptin_settings', array( $this, 'injectDefaultSettings' ) );
321
322 // Legacy admin UI (the `f12-cf7-doubleoptin` menu + its list-table
323 // screens) removed 2026-07-02 — the React SPA (`f12-doi-admin`,
324 // AdminPageController) is the sole admin UI. Runtime opt-in processing
325 // (OptIn, CleanUp, OptInFrontend, the CF7 flow) is unaffected.
326
327 add_action( 'after_setup_theme', array( $this, 'init' ) );
328 $this->get_logger()->debug(
329 'Hook after_setup_theme registered',
330 array(
331 'plugin' => 'double-opt-in',
332 )
333 );
334
335 $Compatibility = new Compatibility( $this );
336 $this->get_logger()->debug(
337 'Compatibility initialized',
338 array(
339 'plugin' => 'double-opt-in',
340 )
341 );
342
343 $CleanUp = new CleanUp( $this->get_logger() );
344 $this->get_logger()->debug(
345 'CleanUp initialized',
346 array(
347 'plugin' => 'double-opt-in',
348 )
349 );
350
351 // Pagination
352 Pagination::getInstance();
353 $this->get_logger()->debug(
354 'Pagination initialized',
355 array(
356 'plugin' => 'double-opt-in',
357 )
358 );
359
360 // initialize filter
361 CategoryOptions::getInstance();
362 $this->get_logger()->debug(
363 'CategoryOptions initialized',
364 array(
365 'plugin' => 'double-opt-in',
366 )
367 );
368
369 OptInLimitFilter::getInstance();
370 $this->get_logger()->debug(
371 'OptInLimitFilter initialized',
372 array(
373 'plugin' => 'double-opt-in',
374 )
375 );
376
377 OptInSearchFilter::getInstance();
378 $this->get_logger()->debug(
379 'OptInSearchFilter initialized',
380 array(
381 'plugin' => 'double-opt-in',
382 )
383 );
384
385 $this->get_logger()->info(
386 'Initialization of Forge12 Double Opt-In completed',
387 array(
388 'plugin' => 'double-opt-in',
389 'class' => __CLASS__,
390 'method' => __METHOD__,
391 )
392 );
393 }
394
395 public function get_logger() {
396 return $this->logger;
397 }
398
399 /**
400 * Initialize the DI Container and register Service Providers.
401 *
402 * @since 4.0.0
403 * @return void
404 */
405 private function initializeContainer(): void {
406 $container = \Forge12\DoubleOptIn\Container\Container::getInstance();
407
408 // Register core services
409 $container->addProvider( new \Forge12\DoubleOptIn\Providers\CoreServiceProvider() );
410
411 // Register event system
412 $container->addProvider( new \Forge12\DoubleOptIn\Providers\EventServiceProvider() );
413
414 // Register repositories and services
415 $container->addProvider( new \Forge12\DoubleOptIn\Providers\RepositoryServiceProvider() );
416
417 // Register email template services
418 $container->addProvider( new \Forge12\DoubleOptIn\Providers\EmailTemplateServiceProvider() );
419
420 // Register form integration system (v4.0+ Event-based Architecture)
421 $container->addProvider( new \Forge12\DoubleOptIn\Providers\IntegrationServiceProvider() );
422
423 // Register form settings services (v4.1+ Central Form Management)
424 $container->addProvider( new \Forge12\DoubleOptIn\Providers\FormSettingsServiceProvider() );
425
426 // Register GDPR compliance services (v3.2.0+)
427 $container->addProvider( new \Forge12\DoubleOptIn\Providers\GdprServiceProvider() );
428
429 // Register admin REST API and audit services (v4.2.0+)
430 $container->addProvider( new \Forge12\DoubleOptIn\Providers\AdminServiceProvider() );
431
432 // Register licensing registry (v4.3.0+ — entitlement state for paid addons)
433 $container->addProvider( new \Forge12\DoubleOptIn\Providers\LicensingServiceProvider() );
434
435 // Register migration registry (v4.3.0+ — runs pending DB migrations on admin_init)
436 $container->addProvider( new \Forge12\DoubleOptIn\Providers\MigrationServiceProvider() );
437
438 // Register addon system (v4.3.0+ — public Addon API)
439 $container->addProvider( new \Forge12\DoubleOptIn\Providers\AddonServiceProvider() );
440
441 // Register RateLimiter as singleton
442 $container->singleton(
443 \Forge12\DoubleOptIn\Service\RateLimiter::class,
444 function () {
445 return new \Forge12\DoubleOptIn\Service\RateLimiter();
446 }
447 );
448
449 // Boot all providers
450 $container->boot();
451
452 $this->get_logger()->info(
453 'DI Container initialized with Service Providers',
454 array(
455 'plugin' => 'double-opt-in',
456 'component' => 'container',
457 )
458 );
459 }
460
461 /**
462 * Get the DI Container instance.
463 *
464 * @since 4.0.0
465 * @return \Forge12\DoubleOptIn\Container\Container
466 */
467 public function getContainer(): \Forge12\DoubleOptIn\Container\Container {
468 return \Forge12\DoubleOptIn\Container\Container::getInstance();
469 }
470
471 /**
472 * Retrieve the template handler instance.
473 *
474 * @return TemplateHandler The template handler instance.
475 */
476 public function get_template_handler() {
477 $this->get_logger()->debug(
478 'TemplateHandler retrieved',
479 array(
480 'plugin' => 'double-opt-in',
481 'class' => __CLASS__,
482 'method' => __METHOD__,
483 )
484 );
485
486 return $this->TemplateHandler;
487 }
488
489 /**
490 * @private WordPress Hook
491 */
492 public function init() {
493 $this->get_logger()->debug(
494 'Init started',
495 array(
496 'plugin' => 'double-opt-in',
497 'class' => __CLASS__,
498 'method' => __METHOD__,
499 )
500 );
501
502 do_action( 'f12_cf7_doubleoptin_register_implementations' );
503
504 $this->get_logger()->debug(
505 'Action f12_cf7_doubleoptin_register_implementations executed',
506 array(
507 'plugin' => 'double-opt-in',
508 )
509 );
510 }
511
512
513 /**
514 * Return the settings for the optin.
515 *
516 * @param string $single The Key of the setting to return only the required setting
517 *
518 * @formatter:off
519 * @return {
520 * // Returns the Settings for the DOI
521 *
522 * @type string $optout_subject The Subject for the OptOut Mail
523 * @type string $optout_body The Content for the OptOut Mail
524 * @type int $optout_page The Post ID for the OptOut Page
525 * @type int $support Defines if the Support link will be added to the footer
526 * @type int $delete An integer from 1 to 30
527 * @type int $delete_unconfirmed An integer from 1 to 30
528 * @type string $delete_period The time period, either months, days, years
529 * @type string $delete_unconfirmed_period The time period, either months, days, years
530 * }
531 * @formatter:on
532 */
533
534 /**
535 * Inject the core settings defaults onto the f12_cf7_doubleoptin_settings
536 * filter. Relocated from the legacy admin UI (UISettings::getSettings) so
537 * the default key set survives without the legacy admin. Defaults are the
538 * base; any value already on the filter (saved settings, test overrides,
539 * addon contributions) wins via array_merge.
540 *
541 * @param array $settings Settings collected so far on the filter.
542 * @return array
543 */
544 public function injectDefaultSettings( $settings ) {
545 $default_settings = array(
546 'telemetry' => 1,
547 'delete' => 12,
548 'delete_unconfirmed' => 7,
549 'delete_period' => 'months',
550 'delete_unconfirmed_period' => 'months',
551 'privacy_policy_page' => 0,
552 'token_expiry_hours' => 48,
553 'rate_limit_ip' => 5,
554 'rate_limit_email' => 3,
555 'rate_limit_window' => 60,
556 'reminder_enabled' => 0,
557 'reminder_delay' => 24,
558 'reminder_template' => '',
559 'reminder_subject' => '',
560 'mx_validation_enabled' => 0,
561 'mx_validation_behavior' => 'silent',
562 'mx_validation_message' => '',
563 'domain_blocklist_enabled' => 0,
564 'domain_blocklist' => '',
565 'domain_blocklist_behavior' => 'silent',
566 'domain_blocklist_message' => '',
567 );
568
569 return array_merge( $default_settings, is_array( $settings ) ? $settings : array() );
570 }
571
572 public function getSettings( $single = '', $container = null ) {
573 $this->get_logger()->debug(
574 'Fetching settings',
575 array(
576 'plugin' => 'double-opt-in',
577 'class' => __CLASS__,
578 'method' => __METHOD__,
579 'single' => $single,
580 'container' => $container,
581 )
582 );
583
584 $default = array();
585
586 $default = apply_filters( 'f12_cf7_doubleoptin_settings', $default );
587
588 $settings = get_option( 'f12-doi-settings' );
589
590 if ( ! is_array( $settings ) ) {
591 $this->get_logger()->debug(
592 'No settings found in options, using empty array',
593 array(
594 'plugin' => 'double-opt-in',
595 )
596 );
597 $settings = array();
598 }
599
600 foreach ( $default as $key => $data ) {
601 if ( isset( $settings[ $key ] ) ) {
602 if ( is_array( $default[ $key ] ) ) {
603 $default[ $key ] = array_merge( $default[ $key ], $settings[ $key ] );
604 } else {
605 $default[ $key ] = $settings[ $key ];
606 }
607 $this->get_logger()->debug(
608 'Merged settings for key',
609 array(
610 'plugin' => 'double-opt-in',
611 'key' => $key,
612 )
613 );
614 }
615 }
616
617 $settings = $default;
618
619 if ( ! empty( $single ) ) {
620 if ( $container != null ) {
621 if ( isset( $settings[ $container ] ) && isset( $settings[ $container ][ $single ] ) ) {
622 $this->get_logger()->debug(
623 'Returning single setting from container',
624 array(
625 'plugin' => 'double-opt-in',
626 'container' => $container,
627 'single' => $single,
628 )
629 );
630 $settings = $settings[ $container ][ $single ];
631 }
632 }
633 } elseif ( isset( $settings[ $single ] ) ) {
634 $this->get_logger()->debug(
635 'Returning single setting',
636 array(
637 'plugin' => 'double-opt-in',
638 'single' => $single,
639 )
640 );
641 $settings = $settings[ $single ];
642 }
643
644 return $settings;
645 }
646 }
647
648
649 add_action(
650 'plugins_loaded',
651 function () {
652 add_cron_jobs();
653 CF7DoubleOptIn::getInstance();
654 }
655 );
656
657 /**
658 * Display upgrade notice in plugin list when updating to major versions.
659 *
660 * @param array $data Plugin update data.
661 * @param object $response Response object from WordPress.org API.
662 */
663 add_action(
664 'in_plugin_update_message-' . FORGE12_OPTIN_BASENAME,
665 function ( $data, $response ) {
666 $upgrade_notice = '';
667
668 // Check if this is a major update (e.g., 3.1.x -> 3.2.x)
669 $current_version = FORGE12_OPTIN_VERSION;
670 $new_version = $response->new_version ?? '';
671
672 if ( empty( $new_version ) ) {
673 return;
674 }
675
676 // Extract major.minor from versions
677 $current_parts = explode( '.', $current_version );
678 $new_parts = explode( '.', $new_version );
679
680 $current_minor = ( $current_parts[0] ?? '0' ) . '.' . ( $current_parts[1] ?? '0' );
681 $new_minor = ( $new_parts[0] ?? '0' ) . '.' . ( $new_parts[1] ?? '0' );
682
683 // Show warning for major/minor version changes
684 if ( version_compare( $new_minor, $current_minor, '>' ) ) {
685 $upgrade_notice = sprintf(
686 '</p><div class="notice inline notice-warning notice-alt" style="margin: 10px 0; padding: 10px; border-left-color: #ffb900;"><p><strong>%s</strong></p><p>%s</p></div><p style="display:none;">',
687 esc_html__( '⚠️ Important: Major Update – Please backup before updating!', 'double-opt-in' ),
688 esc_html__( 'This version includes significant changes to the form management system, email templates, and database structure. We strongly recommend creating a full site backup before updating.', 'double-opt-in' )
689 );
690
691 echo wp_kses_post( $upgrade_notice );
692 }
693
694 // Avada deprecation notice is handled by
695 // Forge12\DoubleOptIn\Migration\AvadaDeprecationNotice (registered in
696 // __construct). That class renders a proper admin notice on every
697 // admin page with a grandfather-license claim button, rather than
698 // a one-shot message at update time.
699 },
700 10,
701 2
702 );
703
704 }
705