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