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

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