PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.3
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.3
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.3, at src/Integration/CF7Integration.php

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