PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.8.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.8.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 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 All 41 releases
double-opt-in / CF7DoubleOptIn.class.php

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

760 lines 23.7 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.8.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.8.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 // Confirmation mail delivery status (v5.8.0+)
465 $container->addProvider( new \Forge12\DoubleOptIn\Providers\MailStatusServiceProvider() );
466
467 // Register licensing registry (v4.3.0+ — entitlement state for paid addons)
468 $container->addProvider( new \Forge12\DoubleOptIn\Providers\LicensingServiceProvider() );
469
470 // Register migration registry (v4.3.0+ — runs pending DB migrations on admin_init)
471 $container->addProvider( new \Forge12\DoubleOptIn\Providers\MigrationServiceProvider() );
472
473 // Register follow-up coordinator (v5.6.0+ — status + retry of
474 // post-confirmation actions). Before AddonServiceProvider so
475 // the adapter registry exists when the form addons boot.
476 $container->addProvider( new \Forge12\DoubleOptIn\Providers\FollowUpServiceProvider() );
477
478 // Register addon system (v4.3.0+ — public Addon API)
479 $container->addProvider( new \Forge12\DoubleOptIn\Providers\AddonServiceProvider() );
480
481 // Register health checks (v5.3.0+ — Site Health surfaces for
482 // broken runtime preconditions such as a missing DB table).
483 // After AddonServiceProvider so addon-contributed checks are
484 // picked up by the registry's filter pass.
485 $container->addProvider( new \Forge12\DoubleOptIn\Providers\HealthServiceProvider() );
486
487 // Register RateLimiter as singleton
488 $container->singleton(
489 \Forge12\DoubleOptIn\Service\RateLimiter::class,
490 function () {
491 return new \Forge12\DoubleOptIn\Service\RateLimiter();
492 }
493 );
494
495 // Boot all providers
496 $container->boot();
497
498 $this->get_logger()->info(
499 'DI Container initialized with Service Providers',
500 array(
501 'plugin' => 'double-opt-in',
502 'component' => 'container',
503 )
504 );
505 }
506
507 /**
508 * Get the DI Container instance.
509 *
510 * @since 4.0.0
511 * @return \Forge12\DoubleOptIn\Container\Container
512 */
513 public function getContainer(): \Forge12\DoubleOptIn\Container\Container {
514 return \Forge12\DoubleOptIn\Container\Container::getInstance();
515 }
516
517 /**
518 * Retrieve the template handler instance.
519 *
520 * @return TemplateHandler The template handler instance.
521 */
522 public function get_template_handler() {
523 $this->get_logger()->debug(
524 'TemplateHandler retrieved',
525 array(
526 'plugin' => 'double-opt-in',
527 'class' => __CLASS__,
528 'method' => __METHOD__,
529 )
530 );
531
532 return $this->TemplateHandler;
533 }
534
535 /**
536 * @private WordPress Hook
537 */
538 public function init() {
539 $this->get_logger()->debug(
540 'Init started',
541 array(
542 'plugin' => 'double-opt-in',
543 'class' => __CLASS__,
544 'method' => __METHOD__,
545 )
546 );
547
548 do_action( 'f12_cf7_doubleoptin_register_implementations' );
549
550 $this->get_logger()->debug(
551 'Action f12_cf7_doubleoptin_register_implementations executed',
552 array(
553 'plugin' => 'double-opt-in',
554 )
555 );
556 }
557
558
559 /**
560 * Return the settings for the optin.
561 *
562 * @param string $single The Key of the setting to return only the required setting
563 *
564 * @formatter:off
565 * @return {
566 * // Returns the Settings for the DOI
567 *
568 * @type string $optout_subject The Subject for the OptOut Mail
569 * @type string $optout_body The Content for the OptOut Mail
570 * @type int $optout_page The Post ID for the OptOut Page
571 * @type int $support Defines if the Support link will be added to the footer
572 * @type int $delete An integer from 1 to 30
573 * @type int $delete_unconfirmed An integer from 1 to 30
574 * @type string $delete_period The time period, either months, days, years
575 * @type string $delete_unconfirmed_period The time period, either months, days, years
576 * }
577 * @formatter:on
578 */
579
580 /**
581 * Inject the core settings defaults onto the f12_cf7_doubleoptin_settings
582 * filter. Relocated from the legacy admin UI (UISettings::getSettings) so
583 * the default key set survives without the legacy admin. Defaults are the
584 * base; any value already on the filter (saved settings, test overrides,
585 * addon contributions) wins via array_merge.
586 *
587 * @param array $settings Settings collected so far on the filter.
588 * @return array
589 */
590 public function injectDefaultSettings( $settings ) {
591 $default_settings = array(
592 'telemetry' => 1,
593 'delete' => 12,
594 'delete_unconfirmed' => 7,
595 'delete_period' => 'months',
596 'delete_unconfirmed_period' => 'months',
597 'privacy_policy_page' => 0,
598 // Must be listed even though the opt-out addon owns the
599 // feature: getSettings() rebuilds its return value from
600 // THIS array and silently drops any stored key that is
601 // missing here. Without the entry, every consumer of
602 // getSettings()['optout_page'] — OptInLinkGenerator and
603 // OptIn::get_link_optout(), i.e. the `[doubleoptoutlink]`
604 // placeholder — fell back to home_url() no matter what
605 // the admin had configured.
606 'optout_page' => 0,
607 'token_expiry_hours' => 48,
608 'rate_limit_ip' => 5,
609 'rate_limit_email' => 3,
610 'rate_limit_window' => 60,
611 'reminder_enabled' => 0,
612 'reminder_delay' => 24,
613 'reminder_template' => '',
614 'reminder_subject' => '',
615 'mx_validation_enabled' => 0,
616 'mx_validation_behavior' => 'silent',
617 'mx_validation_message' => '',
618 'domain_blocklist_enabled' => 0,
619 'domain_blocklist' => '',
620 'domain_blocklist_behavior' => 'silent',
621 'domain_blocklist_message' => '',
622 );
623
624 return array_merge( $default_settings, is_array( $settings ) ? $settings : array() );
625 }
626
627 public function getSettings( $single = '', $container = null ) {
628 $this->get_logger()->debug(
629 'Fetching settings',
630 array(
631 'plugin' => 'double-opt-in',
632 'class' => __CLASS__,
633 'method' => __METHOD__,
634 'single' => $single,
635 'container' => $container,
636 )
637 );
638
639 $default = array();
640
641 $default = apply_filters( 'f12_cf7_doubleoptin_settings', $default );
642
643 $settings = get_option( 'f12-doi-settings' );
644
645 if ( ! is_array( $settings ) ) {
646 $this->get_logger()->debug(
647 'No settings found in options, using empty array',
648 array(
649 'plugin' => 'double-opt-in',
650 )
651 );
652 $settings = array();
653 }
654
655 foreach ( $default as $key => $data ) {
656 if ( isset( $settings[ $key ] ) ) {
657 if ( is_array( $default[ $key ] ) ) {
658 $default[ $key ] = array_merge( $default[ $key ], $settings[ $key ] );
659 } else {
660 $default[ $key ] = $settings[ $key ];
661 }
662 $this->get_logger()->debug(
663 'Merged settings for key',
664 array(
665 'plugin' => 'double-opt-in',
666 'key' => $key,
667 )
668 );
669 }
670 }
671
672 $settings = $default;
673
674 if ( ! empty( $single ) ) {
675 if ( $container != null ) {
676 if ( isset( $settings[ $container ] ) && isset( $settings[ $container ][ $single ] ) ) {
677 $this->get_logger()->debug(
678 'Returning single setting from container',
679 array(
680 'plugin' => 'double-opt-in',
681 'container' => $container,
682 'single' => $single,
683 )
684 );
685 $settings = $settings[ $container ][ $single ];
686 }
687 }
688 } elseif ( isset( $settings[ $single ] ) ) {
689 $this->get_logger()->debug(
690 'Returning single setting',
691 array(
692 'plugin' => 'double-opt-in',
693 'single' => $single,
694 )
695 );
696 $settings = $settings[ $single ];
697 }
698
699 return $settings;
700 }
701 }
702
703
704 add_action(
705 'plugins_loaded',
706 function () {
707 add_cron_jobs();
708 CF7DoubleOptIn::getInstance();
709 }
710 );
711
712 /**
713 * Display upgrade notice in plugin list when updating to major versions.
714 *
715 * @param array $data Plugin update data.
716 * @param object $response Response object from WordPress.org API.
717 */
718 add_action(
719 'in_plugin_update_message-' . FORGE12_OPTIN_BASENAME,
720 function ( $data, $response ) {
721 $upgrade_notice = '';
722
723 // Check if this is a major update (e.g., 3.1.x -> 3.2.x)
724 $current_version = FORGE12_OPTIN_VERSION;
725 $new_version = $response->new_version ?? '';
726
727 if ( empty( $new_version ) ) {
728 return;
729 }
730
731 // Extract major.minor from versions
732 $current_parts = explode( '.', $current_version );
733 $new_parts = explode( '.', $new_version );
734
735 $current_minor = ( $current_parts[0] ?? '0' ) . '.' . ( $current_parts[1] ?? '0' );
736 $new_minor = ( $new_parts[0] ?? '0' ) . '.' . ( $new_parts[1] ?? '0' );
737
738 // Show warning for major/minor version changes
739 if ( version_compare( $new_minor, $current_minor, '>' ) ) {
740 $upgrade_notice = sprintf(
741 '</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;">',
742 esc_html__( 'Important: Major Update – Please backup before updating!', 'double-opt-in' ),
743 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' )
744 );
745
746 echo wp_kses_post( $upgrade_notice );
747 }
748
749 // Avada deprecation notice is handled by
750 // Forge12\DoubleOptIn\Migration\AvadaDeprecationNotice (registered in
751 // __construct). That class renders a proper admin notice on every
752 // admin page with a grandfather-license claim button, rather than
753 // a one-shot message at update time.
754 },
755 10,
756 2
757 );
758
759 }
760