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.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 3.0.70 3.0.71 All 36 releases
← All changes | CF7DoubleOptIn.class.php +727 -262 3.0.715.5.0 View file →
@@ -1,262 +1,727 @@
1 -<?php
2 -
3 -namespace forge12\contactform7\CF7DoubleOptIn {
4 - if ( ! defined( 'ABSPATH' ) ) {
5 - exit;
6 - }
7 -
8 - /**
9 - * Plugin Name: Double Opt-In (Contact Form 7, Avada) - GDPR Ready
10 - * Plugin URI: https://www.forge12.com/blog/so-verwendest-du-das-double-opt-in-fuer-contact-form-7/
11 - * Description: This plugin allows you to add a double OptIn System to your Contact Form 7 & Avada Forms.
12 - * Text Domain: double-opt-in
13 - * Domain Path: /languages
14 - * Version: 3.0.71
15 - * Author: Forge12 Interactive GmbH
16 - * Author URI: https://www.forge12.com
17 - */
18 - define( 'FORGE12_OPTIN_VERSION', '3.0.71' );
19 - define( 'FORGE12_OPTIN_SLUG', 'f12-cf7-doubleoptin' );
20 - define( 'FORGE12_OPTIN_BASENAME', plugin_basename( __FILE__ ) );
21 -
22 -
23 - /**
24 - * Dependencies
25 - */
26 - require_once('core/Debug.php');
27 - require_once( 'core/BaseController.class.php' );
28 -
29 - require_once( 'OnActivation.php' );
30 - require_once( 'OnDeactivation.php' );
31 - require_once( 'OnUpdate.php' );
32 - require_once( 'compatibility/OptInFrontend.class.php' );
33 -
34 - require_once( 'core/Messages.class.php' );
35 - require_once( 'core/TemplateHandler.class.php' );
36 - require_once( 'core/IPHelper.class.php' );
37 - require_once( 'core/SanitizeHelper.class.php' );
38 - require_once( 'core/Ajax.class.php' );
39 - require_once( 'core/Compatibility.class.php' );
40 - require_once( 'core/CleanUp.class.php' );
41 - require_once( 'core/HTMLSelect.class.php' );
42 - require_once( 'core/UI.class.php' );
43 - require_once( 'core/UIPage.class.php' );
44 - require_once( 'core/UIPageForm.class.php' );
45 - require_once( 'core/OptIn.class.php' );
46 - require_once( 'core/OptInLimitFilter.class.php' );
47 - require_once( 'core/OptInSearchFilter.class.php' );
48 - require_once( 'core/Category.class.php' );
49 - require_once( 'core/CategoryOptions.class.php' );
50 - require_once( 'core/Pagination.class.php' );
51 - require_once( 'core/Support.class.php' );
52 -
53 - /**
54 - * Class CF7DoubleOptIn
55 - * Controller for the Custom Links.
56 - *
57 - * @package forge12\contactform7
58 - */
59 - class CF7DoubleOptIn {
60 - /**
61 - * @var CF7DoubleOptIn|Null
62 - */
63 - private static $_instance = null;
64 -
65 - /**
66 - * @var TemplateHandler|null
67 - */
68 - private $TemplateHandler = null;
69 -
70 - /**
71 - * Get the singleton instance of CF7DoubleOptIn.
72 - *
73 - * @return CF7DoubleOptIn The singleton instance.
74 - */
75 - public static function getInstance() {
76 - if ( self::$_instance == null ) {
77 - self::$_instance = new self();
78 - }
79 -
80 - return self::$_instance;
81 - }
82 -
83 - /**
84 - * Return a list containing the array with all data stored within the form
85 - *
86 - * @param int $postID
87 - *
88 - * @formatter:off
89 - *
90 - * @return {
91 - * @type int $enable The Status of the OptIn, either 1 for enabled or 0 for disabled. Default: 0
92 - * @type string $sender The E-Mail of the sender of the optIn mail
93 - * @type string $subject The Subject of the OptIn Mail
94 - * @type string $body The Content of the OptIn Mail
95 - * @type string $recipient The Field that contains the E-Mail of the Recipient.
96 - * @type int $page The Post ID of the confirmation page. Default: -1
97 - * @type string $conditions Additional condition to dynamically enable / disable the optin.
98 - * Default: disabled
99 - * @type string $template The Template used for the OptIn Mail
100 - * @type int $category The Category the OptIns will be assigned to.
101 - * }
102 - * @formatter:on
103 - */
104 - public function getParameter( $postID ) {
105 -
106 - $data = [
107 - 'enable' => 0,
108 - 'sender' => get_bloginfo( 'admin_email' ),
109 - 'sender_name' => '',
110 - 'subject' => '',
111 - 'body' => '',
112 - 'recipient' => '',
113 - 'page' => - 1,
114 - 'conditions' => 'disabled',
115 - 'template' => '',
116 - 'category' => 0,
117 - ];
118 -
119 - /**
120 - * DOI Parameter
121 - *
122 - * Filters the parameter for the DOI. This hook allows developers
123 - * to alter DOI fields.
124 - *
125 - * @param array $data Additional Fields
126 - *
127 - * @since 1.0.0
128 - */
129 - $data = apply_filters( 'f12_cf7_doubleoptin_get_parameter', $data );
130 -
131 - if ( ! $postID ) {
132 - return $data;
133 - }
134 -
135 - $options = get_post_meta( $postID, 'f12-cf7-doubleoptin', true );
136 -
137 - if ( ! $options ) {
138 - return $data;
139 - }
140 -
141 - return array_merge( $data, $options );
142 - }
143 -
144 - /**
145 - * Private constructor to prevent direct instantiation.
146 - */
147 - private function __construct() {
148 - load_plugin_textdomain( 'double-opt-in', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
149 - /**
150 - * DOI INIT
151 - *
152 - * Run custom actions after the DOI has been initialized
153 - *
154 - * @param CF7DoubleOptIn $this
155 - *
156 - * @since 3.0.0
157 - */
158 - do_action( 'f12_cf7_doubleoptin_init', $this );
159 -
160 -
161 - $this->TemplateHandler = TemplateHandler::getInstance();
162 -
163 - $UI = new UI( $this->TemplateHandler, FORGE12_OPTIN_SLUG, 'Forge12 Double Opt-In', 'manage_options' );
164 -
165 - add_action( 'after_setup_theme', array( $this, 'init' ) );
166 -
167 - $Compatibility = new Compatibility( $this );
168 - $CleanUp = new CleanUp();
169 -
170 - // Pagination
171 - Pagination::getInstance();
172 -
173 - // initialize filter
174 - CategoryOptions::getInstance();
175 - OptInLimitFilter::getInstance();
176 - OptInSearchFilter::getInstance();
177 -
178 - // Support
179 - Support::getInstance();
180 - }
181 -
182 - /**
183 - * Retrieve the template handler instance.
184 - *
185 - * @return TemplateHandler The template handler instance.
186 - */
187 - public function get_template_handler() {
188 - return $this->TemplateHandler;
189 - }
190 -
191 - /**
192 - * @private WordPress Hook
193 - */
194 - public function init() {
195 - do_action( 'f12_cf7_doubleoptin_register_implementations' );
196 - }
197 -
198 - /**
199 - * Return the settings for the optin.
200 - *
201 - * @param string $single The Key of the setting to return only the required setting
202 - *
203 - * @formatter:off
204 - * @return {
205 - * // Returns the Settings for the DOI
206 - *
207 - * @type string $optout_subject The Subject for the OptOut Mail
208 - * @type string $optout_body The Content for the OptOut Mail
209 - * @type int $optout_page The Post ID for the OptOut Page
210 - * @type int $support Defines if the Support link will be added to the footer
211 - * @type int $delete An integer from 1 to 30
212 - * @type int $delete_unconfirmed An integer from 1 to 30
213 - * @type string $delete_period The time period, either months, days, years
214 - * @type string $delete_unconfirmed_period The time period, either months, days, years
215 - * }
216 - * @formatter:on
217 - */
218 - public function getSettings( $single = '', $container = null ) {
219 - $default = array();
220 -
221 - $default = apply_filters( 'f12_cf7_doubleoptin_settings', $default );
222 -
223 - $settings = get_option( 'f12-doi-settings' );
224 -
225 - if ( ! is_array( $settings ) ) {
226 - $settings = array();
227 - }
228 -
229 - foreach ( $default as $key => $data ) {
230 - if ( isset( $settings[ $key ] ) ) {
231 - if ( is_array( $default[ $key ] ) ) {
232 - $default[ $key ] = array_merge( $default[ $key ], $settings[ $key ] );
233 - } else {
234 - $default[ $key ] = $settings[ $key ];
235 - }
236 - }
237 - }
238 -
239 - $settings = $default;
240 -
241 - if ( ! empty( $single ) ) {
242 - if ( $container != null ) {
243 - if ( isset( $settings[ $container ] ) && isset( $settings[ $container ][ $single ] ) ) {
244 - $settings = $settings[ $container ][ $single ];
245 - }
246 - }
247 - } else {
248 - if ( isset( $settings[ $single ] ) ) {
249 - $settings = $settings[ $single ];
250 - }
251 - }
252 -
253 - return $settings;
254 - }
255 - }
256 -
257 -
258 - add_action('plugins_loaded', function(){
259 - CF7DoubleOptIn::getInstance();
260 - });
261 -
262 -}
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 +}