PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.1
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.1
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 3.0.60 3.0.61 3.0.62 All 38 releases
double-opt-in / src / Integration / CF7Integration.php

CF7Integration.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.6.1, at src/Integration/CF7Integration.php

841 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Contact Form 7 Integration
4 *
5 * @package Forge12\DoubleOptIn\Integration
6 * @since 4.0.0
7 */
8
9 namespace Forge12\DoubleOptIn\Integration;
10
11 use Forge12\DoubleOptIn\Container\Container;
12 use Forge12\DoubleOptIn\EmailTemplates\PlaceholderMapper;
13 use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt;
14 use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator;
15 use Forge12\DoubleOptIn\FollowUp\FollowUpResult;
16 use forge12\contactform7\CF7DoubleOptIn\Category;
17 use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn;
18 use forge12\contactform7\CF7DoubleOptIn\HTMLSelect;
19 use forge12\contactform7\CF7DoubleOptIn\OptIn;
20 use forge12\contactform7\CF7DoubleOptIn\SanitizeHelper;
21 use Forge12\Shared\LoggerInterface;
22
23 if ( ! defined( 'ABSPATH' ) ) {
24 exit;
25 }
26
27 /**
28 * Class CF7Integration
29 *
30 * Integration for Contact Form 7.
31 * Handles opt-in creation, confirmation mail sending, and admin panel.
32 */
33 class CF7Integration extends AbstractFormIntegration implements AdminPanelInterface {
34
35 /**
36 * Current OptIn for mail attachment handling.
37 *
38 * @var OptIn|null
39 */
40 private ?OptIn $currentOptIn = null;
41
42 /**
43 * {@inheritdoc}
44 */
45 public function getIdentifier(): string {
46 return 'cf7';
47 }
48
49 /**
50 * {@inheritdoc}
51 */
52 public function getName(): string {
53 return __( 'Contact Form 7', 'double-opt-in' );
54 }
55
56 /**
57 * {@inheritdoc}
58 */
59 public function isAvailable(): bool {
60 return function_exists( 'wpcf7' ) || class_exists( '\WPCF7_ContactForm' );
61 }
62
63 /**
64 * {@inheritdoc}
65 */
66 protected function getPostType(): string {
67 return 'wpcf7_contact_form';
68 }
69
70 /**
71 * {@inheritdoc}
72 */
73 public function getFormEditUrl( $formId ): string {
74 return admin_url( 'admin.php?page=wpcf7&post=' . (int) $formId . '&action=edit' );
75 }
76
77 /**
78 * {@inheritdoc}
79 */
80 public function registerHooks(): void {
81 // Frontend hooks
82 add_action( 'wpcf7_before_send_mail', array( $this, 'onSubmit' ), $this->getHookPriority(), 3 );
83 add_action( 'init', array( $this, 'handleOptInConfirmation' ) );
84
85 // Register recipient filter
86 add_filter( 'f12_cf7_doubleoptin_get_recipient_cf7', array( $this, 'getRecipientFilter' ), 10, 3 );
87
88 // Confirmation mail hooks
89 add_action( 'f12_cf7_doubleoptin_before_send_default_mail', array( $this, 'beforeSendDefaultMail' ) );
90 add_action( 'f12_cf7_doubleoptin_after_send_default_mail', array( $this, 'afterSendDefaultMail' ) );
91 add_action( 'f12_cf7_doubleoptin_trigger_default_mail', array( $this, 'onTriggerDefaultMail' ) );
92
93 // File hand-off + pending-cleanup. CF7 attaches files to the
94 // confirmation mail in attachExtraAttachments (hooked on
95 // wpcf7_before_send_mail during sendConfirmationMail). Cleanup
96 // MUST run AFTER the mail is sent — otherwise we'd be deleting
97 // files before they're attached. Hence `after_send_default_mail`,
98 // not `after_confirm` like the other integrations. Priority 20
99 // (default 10) so any third-party listener that introspects the
100 // attachments still sees them.
101 // File-lifecycle plan, Schritt 2c (2026-05-08).
102 add_action( 'f12_cf7_doubleoptin_after_send_default_mail', array( $this, 'cleanupPendingAfterMail' ), 20, 1 );
103
104 // Admin hooks
105 $this->registerAdminHooks();
106
107 $this->getLogger()->debug(
108 'CF7 integration hooks registered',
109 array(
110 'plugin' => 'double-opt-in',
111 )
112 );
113 }
114
115 /**
116 * {@inheritdoc}
117 */
118 public function registerAdminHooks(): void {
119 add_action( 'admin_init', array( $this, 'setupAdminPanel' ) );
120 add_action( 'admin_enqueue_scripts', array( $this, 'enqueueAdminAssets' ) );
121 }
122
123 /**
124 * Setup admin panel hooks.
125 *
126 * @return void
127 */
128 public function setupAdminPanel(): void {
129 add_filter( 'wpcf7_editor_panels', array( $this, 'addEditorPanel' ), 10, 1 );
130 add_action( 'wpcf7_save_contact_form', array( $this, 'saveFormSettings' ), 10, 3 );
131 }
132
133 /**
134 * {@inheritdoc}
135 */
136 public function enqueueAdminAssets( string $hook ): void {
137 wp_enqueue_script(
138 'f12-cf7-doubleoptin-admin',
139 plugins_url( 'compatibility/cf7/assets/f12-cf7-popup.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
140 array( 'jquery' )
141 );
142
143 wp_localize_script(
144 'f12-cf7-doubleoptin-admin',
145 'doi',
146 array(
147 'ajax_url' => admin_url( 'admin-ajax.php' ),
148 'nonce' => wp_create_nonce( 'f12_doi_details' ),
149 )
150 );
151
152 wp_enqueue_script(
153 'f12-cf7-doubleoptin-templateloader',
154 plugins_url( 'compatibility/cf7/assets/f12-cf7-templateloader.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
155 array( 'jquery' )
156 );
157
158 wp_localize_script(
159 'f12-cf7-doubleoptin-templateloader',
160 'templateloader',
161 array(
162 'ajax_url' => admin_url( 'admin-ajax.php' ),
163 'nonce' => wp_create_nonce( 'f12_doi_templateloader' ),
164 'label_placeholder' => __( 'Please wait while we load the template...', 'double-opt-in' ),
165 )
166 );
167 }
168
169 /**
170 * {@inheritdoc}
171 */
172 public function getHookPriority(): int {
173 return 5;
174 }
175
176 /**
177 * {@inheritdoc}
178 */
179 public function processSubmission( $context ): ?FormDataInterface {
180 if ( ! is_array( $context ) || ! isset( $context['form'] ) || ! isset( $context['submission'] ) ) {
181 return null;
182 }
183
184 $form = $context['form'];
185 $submission = $context['submission'];
186
187 return FormData::fromCF7( $form, $submission );
188 }
189
190 /**
191 * {@inheritdoc}
192 */
193 public function resolveRecipient( FormDataInterface $formData, array $formParameter ): string {
194 if ( ! isset( $formParameter['recipient'] ) ) {
195 return '';
196 }
197
198 $recipientField = str_replace( array( '[', ']' ), '', $formParameter['recipient'] );
199 $fields = $formData->getFields();
200
201 if ( isset( $fields[ $recipientField ] ) ) {
202 return sanitize_email( $fields[ $recipientField ] );
203 }
204
205 return '';
206 }
207
208 /**
209 * Recipient filter callback for legacy compatibility.
210 *
211 * @param string $recipient Current recipient.
212 * @param array $formParameter Form parameters.
213 * @param array $postParameter Post data.
214 *
215 * @return string The resolved recipient.
216 */
217 public function getRecipientFilter( string $recipient, array $formParameter, array $postParameter ): string {
218 if ( ! isset( $formParameter['recipient'] ) ) {
219 return $recipient;
220 }
221
222 $recipientField = str_replace( array( '[', ']' ), '', $formParameter['recipient'] );
223
224 if ( isset( $postParameter[ $recipientField ] ) ) {
225 return sanitize_email( $postParameter[ $recipientField ] );
226 }
227
228 return $recipient;
229 }
230
231 /**
232 * Handle form submission.
233 *
234 * @param \WPCF7_ContactForm $form The contact form.
235 * @param bool $abort Whether to abort submission.
236 * @param \WPCF7_Submission $submission The submission.
237 *
238 * @return void
239 */
240 public function onSubmit( $form, &$abort, $submission ): void {
241 $formId = $form->id();
242
243 $this->getLogger()->debug(
244 'CF7 form submission received',
245 array(
246 'plugin' => 'double-opt-in',
247 'form_id' => $formId,
248 )
249 );
250
251 if ( ! $this->isOptInEnabled( $formId ) ) {
252 // Our own post-confirmation replay: attach the stored files.
253 if ( self::isReplaying() ) {
254 $this->attachStoredFiles( $submission );
255 }
256 return;
257 }
258
259 // Remove CF7 DB integration
260 remove_action( 'wpcf7_before_send_mail', 'cfdb7_before_send_mail' );
261
262 // Create form data
263 $formData = FormData::fromCF7( $form, $submission );
264 $formParameter = $this->getFormParameter( $formId );
265
266 // Check skip filter
267 if ( apply_filters( 'f12_cf7_doubleoptin_skip_option', false, $formId, $formData->getFields(), 'cf7' ) ) {
268 $this->getLogger()->info(
269 'OptIn skipped by filter',
270 array(
271 'plugin' => 'double-opt-in',
272 'form_id' => $formId,
273 )
274 );
275 return;
276 }
277
278 // Set recipient
279 $recipient = $this->resolveRecipient( $formData, $formParameter );
280 $formData = $formData->withRecipientEmail( $recipient );
281
282 // Create OptIn
283 $optIn = $this->createOptIn( $formData, $formParameter );
284
285 if ( ! $optIn ) {
286 // Always prevent the original CF7 mail from being sent when opt-in creation fails
287 add_filter( 'wpcf7_skip_mail', '__return_true' );
288
289 $error = self::getLastError();
290 if ( $error && apply_filters( 'f12_cf7_doubleoptin_show_validation_error', false ) ) {
291 $message = apply_filters( 'f12_cf7_doubleoptin_error_message', $error->getMessage(), $error, $formId );
292 if ( method_exists( $submission, 'set_response' ) ) {
293 $submission->set_response( $message );
294 }
295 $abort = true;
296 }
297 return;
298 }
299
300 // Send opt-in mail
301 $this->sendOptInMail( $optIn, $formData, $formParameter );
302
303 // Skip original mail
304 add_filter( 'wpcf7_skip_mail', '__return_true' );
305 do_action( 'f12_cf7_doubleoptin_sent', $form, $formId );
306 }
307
308 /**
309 * {@inheritdoc}
310 */
311 public function sendOptInMail( OptIn $optIn, FormDataInterface $formData, array $formParameter ): bool {
312 $formParameter['formUrl'] = $formData->getMetaValue( 'source_url', '' );
313
314 // Get template body
315 $body = apply_filters(
316 'f12_cf7_doubleoptin_template_body',
317 $formParameter['body'],
318 $formParameter['template'] ?? 'blank',
319 $formParameter,
320 $optIn
321 );
322
323 // Process placeholders
324 $body = $this->prepareMailBody( $body, $optIn, $formParameter );
325 $body = apply_filters( 'f12_cf7_doubleoptin_body', $body );
326
327 // Store mail content in OptIn
328 $optIn->set_mail_optin( $body );
329 $optIn->save();
330
331 // Prepare mail arguments
332 $args = apply_filters(
333 'f12-cf7-doubleoptin-cf7-args',
334 array(
335 'subject' => $formParameter['subject'] ?? '',
336 'body' => $body,
337 'sender' => $formParameter['sender'] ?? '',
338 'sender_name' => $formParameter['sender_name'] ?? '',
339 'recipient' => $optIn->get_email(),
340 'use_html' => true,
341 'additional_headers' => '',
342 )
343 );
344
345 if ( ! empty( $args['sender_name'] ) ) {
346 $args['additional_headers'] .= 'From: ' . $args['sender_name'] . ' <' . $args['sender'] . '>';
347 }
348
349 // Send via CF7 mail system
350 \WPCF7_Mail::send( $args, 'mail' );
351
352 $this->getLogger()->info(
353 'OptIn mail sent via CF7',
354 array(
355 'plugin' => 'double-opt-in',
356 'form_id' => $formData->getFormId(),
357 'recipient' => $args['recipient'],
358 )
359 );
360
361 return true;
362 }
363
364 /**
365 * {@inheritdoc}
366 */
367 public function sendConfirmationMail( OptIn $optIn ): void {
368 self::runAsReplay(
369 function () use ( $optIn ) {
370 $this->replaySubmission( $optIn );
371 }
372 );
373 }
374
375 /**
376 * Listener on the global `f12_cf7_doubleoptin_trigger_default_mail`.
377 * That action fires for every integration's opt-in; this used to run
378 * the CF7 submission for Elementor opt-ins too, overwriting $_POST.
379 * Managed opt-ins go through the follow-up coordinator, so a second
380 * trigger never sends twice.
381 *
382 * @param OptIn $optIn The confirmed opt-in.
383 *
384 * @since 5.6.0
385 */
386 public function onTriggerDefaultMail( OptIn $optIn ): void {
387 if ( ! $optIn->isType( $this->getIdentifier() ) ) {
388 return;
389 }
390
391 $coordinator = FollowUpCoordinator::instance();
392 if ( $coordinator !== null && $coordinator->plan( $optIn, true ) ) {
393 $coordinator->run( $optIn, FollowUpAttempt::TRIGGER_LEGACY );
394 return;
395 }
396
397 $this->sendConfirmationMail( $optIn );
398 }
399
400 /**
401 * Re-run the stored submission through CF7 and report what CF7 says.
402 *
403 * Must run inside {@see runAsReplay()} so our own
404 * `wpcf7_before_send_mail` listener processes it as the confirmed
405 * submission (attachments) instead of creating a new opt-in.
406 *
407 * @since 5.6.0
408 */
409 public function replaySubmission( OptIn $optIn ): FollowUpResult {
410 if ( ! $this->isAvailable() || ! class_exists( '\\WPCF7_ContactForm' ) || ! class_exists( '\\WPCF7_Submission' ) ) {
411 $this->getLogger()->warning(
412 'CF7 not available for confirmation mail',
413 array(
414 'plugin' => 'double-opt-in',
415 )
416 );
417 return FollowUpResult::failedRetryable( 'integration_unavailable' );
418 }
419
420 $contactForm = \WPCF7_ContactForm::get_instance( $optIn->get_cf_form_id() );
421 if ( ! $contactForm ) {
422 $this->getLogger()->warning(
423 'CF7 form not found for confirmation mail',
424 array(
425 'plugin' => 'double-opt-in',
426 'form_id' => $optIn->get_cf_form_id(),
427 )
428 );
429 return FollowUpResult::failedPermanent( 'form_missing' );
430 }
431
432 $data = maybe_unserialize( $optIn->get_content() );
433 if ( ! is_array( $data ) ) {
434 return FollowUpResult::failedPermanent( 'payload_missing' );
435 }
436
437 $previousPost = $_POST;
438 $previousOptIn = $this->currentOptIn;
439 $this->currentOptIn = $optIn;
440 $status = '';
441
442 try {
443 $_POST = SanitizeHelper::sanitize_array( $data );
444
445 // Disable validation and spam checks before creating submission
446 $this->beforeSendConfirmationMail();
447
448 // Create submission and send mail. Attachments are added by
449 // onSubmit() → attachStoredFiles() while isReplaying().
450 $submission = \WPCF7_Submission::get_instance( $contactForm );
451
452 if ( is_object( $submission ) && method_exists( $submission, 'get_status' ) ) {
453 $status = (string) $submission->get_status();
454 }
455 } finally {
456 // Re-enable validation and spam checks, restore request state.
457 $this->afterSendConfirmationMail();
458 $_POST = $previousPost;
459 $this->currentOptIn = $previousOptIn;
460 }
461
462 $this->getLogger()->info(
463 'Confirmation mail triggered via CF7',
464 array(
465 'plugin' => 'double-opt-in',
466 'form_id' => $optIn->get_cf_form_id(),
467 'optin_id' => $optIn->get_id(),
468 'cf7_status' => $status,
469 )
470 );
471
472 return CF7FollowUpAdapter::mapStatus( $status );
473 }
474
475 /**
476 * Handle opt-in confirmation from URL.
477 *
478 * @return void
479 */
480 public function handleOptInConfirmation(): void {
481 if ( ! isset( $_GET['optin'] ) ) {
482 return;
483 }
484
485 $hash = sanitize_text_field( $_GET['optin'] );
486 $this->validateOptIn( $hash );
487 }
488
489 /**
490 * Before sending default mail callback.
491 *
492 * @return void
493 */
494 public function beforeSendDefaultMail(): void {
495 $this->beforeSendConfirmationMail();
496 }
497
498 /**
499 * After sending default mail callback.
500 *
501 * @return void
502 */
503 public function afterSendDefaultMail(): void {
504 $this->afterSendConfirmationMail();
505 }
506
507 /**
508 * Attach extra attachments to the mail.
509 *
510 * @param \WPCF7_ContactForm $form The contact form.
511 * @param bool $abort Whether to abort.
512 * @param \WPCF7_Submission $submission The submission.
513 *
514 * @return void
515 */
516 public function attachExtraAttachments( $form, $abort, $submission ): void {
517 if ( $this->currentOptIn && $this->currentOptIn->get_files() ) {
518 $files = maybe_unserialize( $this->currentOptIn->get_files() );
519 if ( is_array( $files ) ) {
520 foreach ( $files as $file ) {
521 $submission->add_extra_attachments( $file );
522 }
523 }
524 }
525 }
526
527 /**
528 * Attach stored files to submission during confirmation.
529 *
530 * @param \WPCF7_Submission $submission The submission.
531 *
532 * @return void
533 */
534 private function attachStoredFiles( $submission ): void {
535 // The opt-in being replayed — not the one named in the URL, which
536 // a cron or admin retry does not have (and a visitor controls).
537 $optIn = $this->currentOptIn;
538
539 if ( ! $optIn ) {
540 return;
541 }
542
543 $files = maybe_unserialize( $optIn->get_files() );
544 if ( empty( $files ) ) {
545 return;
546 }
547
548 foreach ( $files as $file ) {
549 if ( empty( $file ) ) {
550 continue;
551 }
552
553 if ( apply_filters( 'f12_cf7_doubleoptin_files_mail_1', true, $optIn ) ) {
554 $submission->add_extra_attachments( $file );
555 }
556
557 if ( apply_filters( 'f12_cf7_doubleoptin_files_mail_2', true, $optIn ) ) {
558 $submission->add_extra_attachments( $file, 'mail_2' );
559 }
560 }
561 }
562
563 /**
564 * File hand-off — for CF7, the "form system" is the confirmation
565 * mail itself. The hand-off completes when `attachExtraAttachments`
566 * has added the pending files as mail attachments and CF7 has sent
567 * the mail. By the time `cleanupPendingAfterMail` invokes the
568 * template-method (registered on `after_send_default_mail`), this
569 * has already happened and returning true triggers the pending/
570 * cleanup — single source of truth = the recipient's mailbox.
571 *
572 * Differs from WPForms/GF (where the integration's own DB owns the
573 * file URL) in WHEN the hand-off happens, not WHAT it returns. The
574 * hook timing in registerHooks() is the actual difference.
575 *
576 * {@inheritdoc}
577 */
578 public function handOffFilesToFormSystem( OptIn $optIn ): bool {
579 return true;
580 }
581
582 /**
583 * CF7-specific cleanup wrapper. Hooks the file-lifecycle template-
584 * method onto `f12_cf7_doubleoptin_after_send_default_mail` rather
585 * than `f12_cf7_doubleoptin_after_confirm` (the timing the other
586 * integrations use), so pending files survive long enough for
587 * `attachExtraAttachments` to attach them to the confirmation mail.
588 *
589 * Edge case: if `f12_cf7_doubleoptin_send_default_mail` filter is
590 * false (admin opted out of the CF7 confirmation mail), this hook
591 * never fires and pending/ stays populated until the OptIn-deletion
592 * cron sweeps it via cascade-delete. Acceptable per the plan's
593 * fault-tolerance pattern — no silent data loss, just delayed
594 * cleanup.
595 *
596 * @param OptIn $optIn The just-confirmed opt-in (action arg).
597 *
598 * @since 4.3.0
599 */
600 public function cleanupPendingAfterMail( OptIn $optIn ): void {
601 // Managed opt-ins: CF7FollowUpAdapter::onSettled() cleans up only
602 // once the mail was actually handed over. This hook fires after
603 // the attempt regardless of its outcome.
604 $coordinator = FollowUpCoordinator::instance();
605 if ( $coordinator !== null && $coordinator->adapterFor( $optIn ) !== null ) {
606 return;
607 }
608
609 // Template-method's $hash arg is unused inside processFilesOnConfirm;
610 // passing an empty string keeps the contract tight.
611 $this->processFilesOnConfirm( '', $optIn );
612 }
613
614 /**
615 * {@inheritdoc}
616 */
617 public function getFormFields( $formId ): array {
618 $formId = (int) $formId;
619 $post = get_post( $formId );
620 if ( ! $post || $post->post_type !== 'wpcf7_contact_form' ) {
621 return array();
622 }
623
624 $contactForm = \WPCF7_ContactForm::get_instance( $formId );
625 if ( ! $contactForm ) {
626 return array();
627 }
628
629 $fields = array();
630 $tags = $contactForm->scan_form_tags();
631
632 foreach ( $tags as $tag ) {
633 if ( ! empty( $tag->name ) ) {
634 $fields[ $tag->name ] = $tag->name;
635 }
636 }
637
638 return $fields;
639 }
640
641 /**
642 * Add editor panel to CF7.
643 *
644 * @param array $panels The panels array.
645 *
646 * @return array Modified panels.
647 */
648 public function addEditorPanel( array $panels ): array {
649 $panels['optin'] = array(
650 'title' => $this->getPanelTitle(),
651 'callback' => array( $this, 'renderPanel' ),
652 );
653 return $panels;
654 }
655
656 /**
657 * {@inheritdoc}
658 */
659 public function render( $form, array $metadata ): void {
660 $this->renderPanel( $form );
661 }
662
663 /**
664 * Render the CF7 editor panel.
665 *
666 * Displays a notice with link to central form management.
667 * Full settings are now managed centrally in the Forms admin page.
668 *
669 * @param \WPCF7_ContactForm $post The contact form.
670 *
671 * @return void
672 */
673 public function renderPanel( $post ): void {
674 if ( ! $post || ! $post->id() ) {
675 ?>
676 <div class="doi-cf7-notice" style="padding: 20px;">
677 <div style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px;">
678 <h2 style="margin-top: 0;"><?php _e( 'Double Opt-In Settings', 'double-opt-in' ); ?></h2>
679 <p style="color: #666;">
680 <?php _e( 'Please save the contact form first before configuring Double Opt-In.', 'double-opt-in' ); ?>
681 </p>
682 </div>
683 </div>
684 <?php
685 return;
686 }
687
688 $metadata = $this->getFormParameter( $post->id() );
689 $centralUrl = admin_url( 'admin.php?page=f12-doi-admin#/forms' );
690 $isEnabled = $this->isOptInEnabled( $post->id() );
691
692 $this->getLogger()->debug(
693 'Rendering CF7 panel notice',
694 array(
695 'plugin' => 'double-opt-in',
696 'form_id' => $post->id(),
697 'enabled' => $isEnabled,
698 )
699 );
700 ?>
701 <div class="doi-cf7-notice" style="padding: 20px;">
702 <div style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px;">
703 <h2 style="margin-top: 0;"><?php _e( 'Double Opt-In Settings', 'double-opt-in' ); ?></h2>
704
705 <div style="display: flex; align-items: center; gap: 15px; margin-bottom: 20px;">
706 <span style="font-weight: 600;"><?php _e( 'Status:', 'double-opt-in' ); ?></span>
707 <?php if ( $isEnabled ) : ?>
708 <span style="display: inline-block; padding: 4px 12px; background: #d4edda; color: #155724; border-radius: 3px; font-weight: 500;">
709 <?php _e( 'Enabled', 'double-opt-in' ); ?>
710 </span>
711 <?php else : ?>
712 <span style="display: inline-block; padding: 4px 12px; background: #f8d7da; color: #721c24; border-radius: 3px; font-weight: 500;">
713 <?php _e( 'Disabled', 'double-opt-in' ); ?>
714 </span>
715 <?php endif; ?>
716 </div>
717
718 <p style="color: #666; margin-bottom: 20px;">
719 <?php _e( 'Double Opt-In settings are now managed centrally. Use the button below to configure this form.', 'double-opt-in' ); ?>
720 </p>
721
722 <a href="<?php echo esc_url( $centralUrl ); ?>" class="button button-primary" target="_blank">
723 <?php _e( 'Configure Double Opt-In', 'double-opt-in' ); ?>
724 </a>
725 </div>
726 </div>
727 <?php
728 }
729
730 /**
731 * {@inheritdoc}
732 */
733 public function save( int $formId, array $data ): bool {
734 if ( ! isset( $data['doubleoptin'] ) ) {
735 update_post_meta( $formId, 'f12-cf7-doubleoptin', array() );
736 return true;
737 }
738
739 $parameter = SanitizeHelper::sanitize_array( $data['doubleoptin'] );
740 $metadata = $this->getFormParameter( $formId );
741
742 foreach ( $metadata as $key => $value ) {
743 if ( isset( $parameter[ $key ] ) ) {
744 $metadata[ $key ] = $key === 'enable' ? (int) $parameter[ $key ] : $parameter[ $key ];
745 } elseif ( $key === 'enable' ) {
746 $metadata[ $key ] = 0;
747 }
748 }
749
750 $metadata = apply_filters( 'f12_cf7_doubleoptin_metadata_cf7', $metadata );
751 $metadata = apply_filters( 'f12_cf7_doubleoptin_save_form', $metadata );
752
753 update_post_meta( $formId, 'f12-cf7-doubleoptin', $metadata );
754
755 // Save placeholder mapping
756 if ( isset( $data['doubleoptin']['placeholder_mapping'] ) ) {
757 $mapping = array_map( 'sanitize_text_field', $data['doubleoptin']['placeholder_mapping'] );
758 PlaceholderMapper::saveCustomMapping( $formId, $mapping, 'cf7' );
759 }
760
761 return true;
762 }
763
764 /**
765 * Save form settings callback.
766 *
767 * @param \WPCF7_ContactForm $contactForm The contact form.
768 * @param array $args The arguments.
769 * @param string $context The context.
770 *
771 * @return void
772 */
773 public function saveFormSettings( $contactForm, $args, $context ): void {
774 $formId = $contactForm->id();
775
776 // Verify nonce
777 if ( ! isset( $_POST['f12_cf7_doubleoptin_save_form_nonce'] ) ||
778 ! wp_verify_nonce( wp_unslash( $_POST['f12_cf7_doubleoptin_save_form_nonce'] ), 'f12_cf7_doubleoptin_save_form_action' ) ) {
779 return;
780 }
781
782 $this->save( $formId, $_POST );
783 }
784
785 /**
786 * {@inheritdoc}
787 */
788 public function getPanelTitle(): string {
789 return __( 'Double-Opt-in', 'double-opt-in' );
790 }
791
792 /**
793 * {@inheritdoc}
794 */
795 public function getAvailableTemplates(): array {
796 $templates = array(
797 'blank' => 'blank',
798 'newsletter_en' => 'newsletter_en',
799 'newsletter_en_2' => 'newsletter_en_2',
800 'newsletter_en_3' => 'newsletter_en_3',
801 );
802
803 // Add custom templates
804 try {
805 $container = Container::getInstance();
806 $integration = $container->get( \Forge12\DoubleOptIn\EmailTemplates\EmailTemplateIntegration::class );
807 $custom = $integration->getCustomTemplates();
808
809 foreach ( $custom as $template ) {
810 $templates[ 'custom_' . $template['id'] ] = $template['title'] . ' (' . __( 'Custom', 'double-opt-in' ) . ')';
811 }
812 } catch ( \Exception $e ) {
813 // Ignore if custom templates not available
814 }
815
816 return $templates;
817 }
818
819 /**
820 * {@inheritdoc}
821 */
822 public function getAvailableCategories(): array {
823 $categories = array( 0 => __( 'Please select', 'double-opt-in' ) );
824
825 $list = Category::get_list(
826 array(
827 'perPage' => -1,
828 'orderBy' => 'name',
829 'order' => 'ASC',
830 ),
831 $numberOfPages
832 );
833
834 foreach ( $list as $category ) {
835 $categories[ $category->get_id() ] = $category->get_name();
836 }
837
838 return $categories;
839 }
840 }
841