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

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

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