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 / CF7DoubleOptIn.class.php

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

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