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.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
← All changes | CF7DoubleOptIn.class.php +727 -382 2.125.5.0 View file →
@@ -1,382 +1,727 @@
1 -<?php
2 -
3 -namespace forge12\contactform7\CF7DoubleOptIn {
4 - if (!defined('ABSPATH')) {
5 - exit;
6 - }
7 - /**
8 - * Dependencies
9 - */
10 - require_once('core/Messages.class.php');
11 - require_once('core/Ajax.class.php');
12 - require_once('core/Compatibility.class.php');
13 - require_once('core/CleanUp.class.php');
14 - require_once('core/HTMLSelect.class.php');
15 - require_once('core/UI.class.php');
16 - require_once('core/UIPage.class.php');
17 - require_once('core/UIPageForm.class.php');
18 - require_once('core/OptIn.class.php');
19 - require_once('core/OptInLimitFilter.class.php');
20 - require_once('core/OptInSearchFilter.class.php');
21 - require_once('core/Category.class.php');
22 - require_once('core/CategoryOptions.class.php');
23 - require_once('core/Pagination.class.php');
24 - require_once('core/Support.class.php');
25 -
26 - /**
27 - * Plugin Name: Double Opt-In (Contact Form 7, Avada) - GDPR Ready
28 - * Plugin URI: https://www.forge12.com/blog/so-verwendest-du-das-double-opt-in-fuer-contact-form-7/
29 - * Description: This plugin allows you to add a double OptIn System to your Contact Form 7 & Avada Forms.
30 - * Text Domain: double-opt-in
31 - * Domain Path: /languages
32 - * Version: 2.12
33 - * Author: Forge12 Interactive GmbH
34 - * Author URI: https://www.forge12.com
35 - */
36 - define('FORGE12_OPTIN_VERSION', '2.12');
37 - define('FORGE12_OPTIN_SLUG', 'f12-cf7-doubleoptin');
38 - define('FORGE12_OPTIN_BASENAME', plugin_basename(__FILE__));
39 -
40 - /**
41 - * Class CF7DoubleOptIn
42 - * Controller for the Custom Links.
43 - *
44 - * @package forge12\contactform7
45 - */
46 - class CF7DoubleOptIn
47 - {
48 - /**
49 - * @var CF7DoubleOptIn|Null
50 - */
51 - private static $_instance = null;
52 -
53 - /**
54 - * Get the instance of the custom links controller
55 - *
56 - * @return CF7DoubleOptIn
57 - */
58 - public static function getInstance()
59 - {
60 - if (self::$_instance == null) {
61 - self::$_instance = new CF7DoubleOptIn();
62 - }
63 -
64 - return self::$_instance;
65 - }
66 -
67 - /**
68 - * @param string $template_name The name of the template file
69 - * @param array $atts The parameter send to the template.
70 - *
71 - * @return void|null
72 - */
73 - public function renderTemplate($template_name, $atts = array())
74 - {
75 - $template = $this->getTemplate($template_name, $atts);
76 -
77 - if (empty($template)) {
78 - return null;
79 - }
80 -
81 - echo $template;
82 - }
83 -
84 - /**
85 - * Return the Content of the Template
86 - *
87 - * @param string $template_name The name of the template file
88 - * @param array $atts The parameter send to the template.
89 - *
90 - * @return string
91 - */
92 - public function getTemplate($template_name, $atts = array())
93 - {
94 - $template_path = plugin_dir_path(__FILE__) . '/templates/' . $template_name . '.php';
95 -
96 - if (!file_exists($template_path)) {
97 - return '';
98 - }
99 -
100 - extract($atts);
101 -
102 - ob_start();
103 - require($template_path);
104 - $content = ob_get_contents();
105 - ob_end_clean();
106 -
107 - return $content;
108 - }
109 -
110 - /**
111 - * Return a list containing the array with all data stored within the contact form 7 form
112 - *
113 - * @param int $postID
114 - *
115 - * @return array
116 - */
117 - public function getParameter($postID)
118 - {
119 - $data = array();
120 -
121 - $data = apply_filters('f12_cf7_doubleoptin_get_parameter', $data);
122 -
123 - if (!$postID) {
124 - return $data;
125 - }
126 -
127 - $options = get_post_meta($postID, 'f12-cf7-doubleoptin', true);
128 -
129 - if (!$options) {
130 - return $data;
131 - }
132 -
133 - return array_merge($data, $options);
134 - }
135 -
136 - /**
137 - * CustomLinks constructor.
138 - */
139 - private function __construct()
140 - {
141 - $UI = new UI(FORGE12_OPTIN_SLUG, 'Forge12 Double Opt-In', 'manage_options');
142 -
143 - add_action('after_setup_theme', [$this, 'init']);
144 - add_action( 'init', [$this, 'load_text_domain'] );
145 -
146 - $Compatibility = new Compatibility(FORGE12_OPTIN_SLUG);
147 -
148 - $CleanUp = new CleanUp();
149 -
150 - // Pagination
151 - Pagination::getInstance();
152 -
153 - // initialize filter
154 - CategoryOptions::getInstance();
155 - OptInLimitFilter::getInstance();
156 - OptInSearchFilter::getInstance();
157 -
158 - // Support
159 - Support::getInstance();
160 - }
161 -
162 - public function load_text_domain(){
163 - load_plugin_textdomain( 'double-opt-in', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
164 - }
165 -
166 - /**
167 - * @private WordPress Hook
168 - */
169 - public function init()
170 - {
171 - do_action('f12_cf7_doubleoptin_register_implementations');
172 - }
173 -
174 - /**
175 - * @return string
176 - */
177 - public function getIPAdress()
178 - {
179 - //whether ip is from share internet
180 - if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
181 - $ip_address = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
182 - } //whether ip is from proxy
183 - elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
184 - $ip_address = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
185 - } //whether ip is from remote address
186 - else {
187 - $ip_address = sanitize_text_field($_SERVER['REMOTE_ADDR']);
188 - }
189 -
190 - return $ip_address;
191 - }
192 -
193 - public function sanitize_array($data){
194 - if(!is_array($data)){
195 - return [];
196 - }
197 -
198 - $sanitized_data = [];
199 - foreach($data as $key => $value){
200 - if(is_array($value)){
201 - $sanitized_data[sanitize_text_field($key)] = $this->sanitize_array($value);
202 - }else {
203 - $sanitized_data[sanitize_text_field($key)] = wp_kses_post($value);
204 - }
205 - }
206 - return $sanitized_data;
207 - }
208 -
209 - /**
210 - * Return all saved settings
211 - *
212 - * @param string $single The Key of the setting to return only the required setting
213 - *
214 - * @return array<mixed>
215 - */
216 - public function getSettings($single = '', $container = null)
217 - {
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 - * Create the table to store the opt ins.
258 - *
259 - * @return void
260 - */
261 - function createTableOptIn()
262 - {
263 - global $wpdb;
264 -
265 - $tableName = 'f12_cf7_doubleoptin';
266 - $wpTableName = $wpdb->prefix . $tableName;
267 -
268 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
269 -
270 - $sql = "CREATE TABLE " . $wpTableName . " (
271 - id int(11) NOT NULL auto_increment,
272 - cf_form_id int(11) NOT NULL,
273 - doubleoptin int(2),
274 - content text,
275 - files text,
276 - hash VARCHAR(255),
277 - ipaddr_register varchar(255) NOT NULL DEFAULT '',
278 - ipaddr_confirmation varchar(255) NOT NULL DEFAULT '',
279 - createtime varchar(255) DEFAULT '',
280 - updatetime varchar(255) DEFAULT '',
281 - category int(11),
282 - email varchar(255),
283 - form text,
284 - mail_optin text,
285 - PRIMARY KEY (id)
286 - )";
287 - dbDelta($sql);
288 - }
289 -
290 - /**
291 - * Create the table for the categories
292 - *
293 - * @return void
294 - */
295 - function createTableOptinCategories()
296 - {
297 - global $wpdb;
298 -
299 - $tableName = 'f12_cf7_doubleoptin_categories';
300 - $wpTableName = $wpdb->prefix . $tableName;
301 -
302 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
303 -
304 - $sql = "CREATE TABLE " . $wpTableName . " (
305 - id int(11) NOT NULL auto_increment,
306 - name varchar(255),
307 - createtime varchar(255) DEFAULT '',
308 - updatetime varchar(255) DEFAULT '',
309 - PRIMARY KEY (id)
310 - )";
311 - dbDelta($sql);
312 - }
313 -
314 - /**
315 - * On Activation create the custom table to store the required information
316 - * for the double opt in system.
317 - */
318 - function onActivation()
319 - {
320 - createTableOptin();
321 - createTableOptinCategories();
322 -
323 - // Add cron
324 - if (!wp_next_scheduled('dailyOptinClear')) {
325 - wp_schedule_event(time(), 'daily', 'dailyOptinClear');
326 - }
327 - }
328 -
329 - register_activation_hook(__FILE__, 'forge12\contactform7\CF7DoubleOptIn\onActivation');
330 -
331 - /**
332 - * On deactivation delete all tables
333 - */
334 - function onDeactivation()
335 - {
336 - if (!defined('WP_UINSTALL_PLUGIN')) {
337 - return;
338 - }
339 -
340 - global $wpdb;
341 -
342 - // Backup Table
343 - $tableName = 'f12_cf7_doubleoptin';
344 - $wpTableName = $wpdb->prefix . $tableName;
345 -
346 - $wpdb->query("DROP TABLE IF EXISTS " . $wpTableName);
347 -
348 - # clear cron
349 - wp_clear_scheduled_hook('dailyOptinClear');
350 - }
351 -
352 - function onUpdate()
353 - {
354 - // Only run if the version installed not exist or the version is < 1.7
355 - // this will add categories to already existing plugins
356 - if (version_compare(get_site_option(FORGE12_OPTIN_SLUG . '_version'), '1.7') < 0) {
357 - createTableOptinCategories();
358 - createTableOptin();
359 -
360 - update_option(FORGE12_OPTIN_SLUG . '_version', '1.7');
361 - }
362 -
363 - // Only run if the version installed not exist or the version is < 2.0
364 - // this will add categories to already existing plugins
365 - if (version_compare(get_site_option(FORGE12_OPTIN_SLUG . '_version'), '2.0') < 0) {
366 - createTableOptin();
367 -
368 - update_option(FORGE12_OPTIN_SLUG . '_version', '2.0');
369 - }
370 - }
371 -
372 - onUpdate();
373 -
374 - register_deactivation_hook(__FILE__, 'forge12\contactform7\CF7DoubleOptIn\onDeactivation');
375 -
376 - CF7DoubleOptIn::getInstance();
377 -
378 - add_filter( 'safe_style_css', function( $styles ) {
379 - $styles[] = 'display';
380 - return $styles;
381 - } );
382 -}
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 +}