PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.13.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.13.0
5.12.0 5.13.0 5.13.1 5.11.0 5.10.0 5.9.0 5.8.0 5.8.1 5.7.0 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 All 47 releases
double-opt-in / CF7DoubleOptIn.class.php

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

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