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

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