PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.2
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.2
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
← All changes | templates/optout-list.php +328 -87 3.0.61 → 5.6.2 View file →
@@ -1,87 +1,328 @@
1 -<?php
2 -/**
3 - * @var OptIn[] $list
4 - * @var string $hash
5 - */
6 -
7 -use forge12\contactform7\CF7DoubleOptIn\OptIn;
8 -
9 -$settings = \forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn::getInstance()->getSettings();
10 -$manual_delete_value = isset($settings['delete_unconfirmed']) ? $settings['delete_unconfirmed'] : 1;
11 -$manual_delete_period = isset($settings['delete_unconfirmed_period']) ? $settings['delete_unconfirmed_period'] : 'months';
12 -$admin_email = get_option('admin_email');
13 -
14 -?>
15 -<div class="f12-cf7-doubleoptin-optout f12-cf7-doubleoptin-optout-list">
16 - <div class="f12-cf7-doubleoptin-optout-list--inner">
17 - <table>
18 - <tr>
19 - <th>
20 - <?php _e('Hash', 'double-opt-in'); ?>
21 - </th>
22 - <th>
23 - <?php _e('Date', 'double-opt-in'); ?>
24 - </th>
25 - <th>
26 - <?php _e('Status', 'double-opt-in'); ?>
27 - </th>
28 - <th>
29 - <?php _e('Valid until', 'double-opt-in'); ?>*
30 - </th>
31 - <th>
32 -
33 - </th>
34 - </tr>
35 - <?php foreach ($list as $OptIn): ?>
36 - <tr>
37 - <td>
38 - <?php echo esc_html($OptIn->get_hash()); ?>
39 - </td>
40 - <td>
41 - <?php echo esc_html($OptIn->get_createtime('view')); ?>
42 - </td>
43 - <td>
44 - <?php if ($OptIn->is_confirmed()): ?>
45 - <div class="dashicons dashicons-yes"></div>
46 - <?php else: ?>
47 - <div class="dashicons dashicons-no"></div>
48 - <?php endif; ?>
49 - </td>
50 - <td>
51 - <?php if ($settings['delete'] == 0 || $settings['delete_unconfirmed'] == 0): ?>
52 - <center><?php _e('&infin;'); ?></center>
53 - <?php else: ?>
54 - <?php echo esc_html($OptIn->get_valid_until()); ?>
55 - <?php endif; ?>
56 -
57 - <?php if ($OptIn->is_confirmed() && $settings['delete'] != 0): ?>
58 - <small>(<?php echo esc_html($settings['delete']); ?> <?php echo esc_html($settings['delete_period']); ?>
59 - )</small>
60 - <?php endif; ?>
61 - <?php if (!$OptIn->is_confirmed() && $settings['delete_unconfirmed'] != 0): ?>
62 - <small>(<?php echo esc_html($settings['delete_unconfirmed']); ?> <?php echo esc_html($settings['delete_unconfirmed_period']); ?>
63 - )</small>
64 - <?php endif; ?>
65 - </td>
66 - <td>
67 - <?php if ($OptIn->is_confirmed()): ?>
68 - <a href="<?php echo $OptIn->get_link_optout(); ?>&hash=<?php echo $hash; ?>" target="_blank"
69 - class="button"><?php _e('Opt-Out now!', 'double-opt-in'); ?></a>
70 - <?php else: /*
71 - <a href="<?php echo $OptIn->get_link_optin(); ?>&hash=<?php echo $hash; ?>" target="_blank"
72 - class="button"><?php _e('Opt-In now!', 'double-opt-in'); ?></a>
73 - */ endif; ?>
74 - </td>
75 - </tr>
76 - <?php endforeach; ?>
77 - </table>
78 - </div>
79 - <span class="hint">* <?php _e('The date indicates when your personal data will be deleted.', 'double-opt-in'); ?></span>
80 - <span class="hint">*
81 - <?php if ($manual_delete_value == 0): ?>
82 - <?php printf(__('Please note that even with manual opt-out, your data will be stored. Please contact the administrator to delete your personal data: %s.', 'double-opt-in'), $admin_email); ?>
83 - <?php else: ?>
84 - <?php printf(__('Please note that even with manual opt-out, your data will only be deleted in %d %s.', 'double-opt-in'), $manual_delete_value, $manual_delete_period); ?>
85 - <?php endif; ?>
86 - </span>
87 -</div>
1 +<?php
2 +if ( ! defined( 'ABSPATH' ) ) {
3 + exit;
4 +}
5 +/**
6 + * Opt-out subscription list — rendered after the visitor clicks the
7 + * hashed link in the opt-out invitation email.
8 + *
9 + * Variables provided by OptOutListShortcode::getContent():
10 + *
11 + * @var OptIn[] $list Subscriptions tied to the hash's email.
12 + * @var string $hash OptOut hash (used in cancel-action links).
13 + * @var int|null $done_form_id cf_form_id of just-cancelled record (single).
14 + * @var bool $done_bulk True after the bulk-cancel action.
15 + * @var int $done_count Number of rows affected by bulk-cancel.
16 + * @var int|null $reoptin_form_id cf_form_id of just-reactivated record.
17 + *
18 + * 2026-05-15 redesign — labels, bulk cancel, re-opt-in, and JS confirm
19 + * dialogs are all driven by OptOutConfigResolver so the admin can
20 + * customize every string in /opt-out → "List page labels" / "List
21 + * page notices" cards. Strings flow through WPML String Translation
22 + * for per-language overrides.
23 + */
24 +
25 +use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn;
26 +use forge12\contactform7\CF7DoubleOptIn\OptIn;
27 +use Forge12\DoubleOptIn\OptOut\OptOutConfigResolver;
28 +
29 +$settings = CF7DoubleOptIn::getInstance()->getSettings();
30 +$manual_delete_value = isset( $settings['delete_unconfirmed'] ) ? $settings['delete_unconfirmed'] : 1;
31 +$manual_delete_period = isset( $settings['delete_unconfirmed_period'] ) ? $settings['delete_unconfirmed_period'] : 'months';
32 +$admin_email = get_option( 'admin_email' );
33 +
34 +$done_form_id = isset( $done_form_id ) ? (int) $done_form_id : 0;
35 +$done_bulk = ! empty( $done_bulk );
36 +$done_count = isset( $done_count ) ? (int) $done_count : 0;
37 +$reoptin_form_id = isset( $reoptin_form_id ) ? (int) $reoptin_form_id : 0;
38 +$affected_hash = isset( $affected_hash ) ? (string) $affected_hash : '';
39 +
40 +$resolver = new OptOutConfigResolver();
41 +
42 +// Helper closure for label lookups so the template stays compact.
43 +$L = static function ( string $key ) use ( $resolver ): string {
44 + return $resolver->labelText( $key );
45 +};
46 +
47 +// Count active rows so the bulk button can be hidden when there's
48 +// nothing to cancel.
49 +$activeCount = 0;
50 +foreach ( $list as $OptIn ) {
51 + if ( $OptIn->is_confirmed() ) {
52 + $activeCount++;
53 + }
54 +}
55 +?>
56 +<div class="f12-cf7-doubleoptin-optout f12-cf7-doubleoptin-optout-list">
57 +
58 + <?php if ( $done_bulk ) : ?>
59 + <div class="f12-doi-optout-notice f12-doi-optout-notice--success" role="status">
60 + <strong><?php echo esc_html( $resolver->listNoticeText( 'cancelled_all', 'title' ) ); ?></strong>
61 + <p><?php echo esc_html( sprintf( $resolver->listNoticeText( 'cancelled_all', 'body' ), $done_count ) ); ?></p>
62 + </div>
63 + <?php elseif ( $done_form_id > 0 ) :
64 + $doneTitle = get_the_title( $done_form_id );
65 + if ( $doneTitle === '' ) {
66 + $doneTitle = sprintf( __( 'Form #%d', 'double-opt-in' ), $done_form_id );
67 + }
68 + ?>
69 + <div class="f12-doi-optout-notice f12-doi-optout-notice--success" role="status">
70 + <strong><?php echo esc_html( $resolver->listNoticeText( 'cancelled', 'title' ) ); ?></strong>
71 + <p><?php echo esc_html( sprintf( $resolver->listNoticeText( 'cancelled', 'body' ), $doneTitle ) ); ?></p>
72 + </div>
73 + <?php elseif ( $reoptin_form_id > 0 ) :
74 + $reoptinTitle = get_the_title( $reoptin_form_id );
75 + if ( $reoptinTitle === '' ) {
76 + $reoptinTitle = sprintf( __( 'Form #%d', 'double-opt-in' ), $reoptin_form_id );
77 + }
78 + ?>
79 + <div class="f12-doi-optout-notice f12-doi-optout-notice--success" role="status">
80 + <strong><?php echo esc_html( $resolver->listNoticeText( 'reactivated', 'title' ) ); ?></strong>
81 + <p><?php echo esc_html( sprintf( $resolver->listNoticeText( 'reactivated', 'body' ), $reoptinTitle ) ); ?></p>
82 + </div>
83 + <?php endif; ?>
84 +
85 + <div class="f12-cf7-doubleoptin-optout-list--inner">
86 + <?php if ( empty( $list ) ) : ?>
87 + <div class="f12-doi-optout-empty">
88 + <?php echo esc_html( $L( 'empty_state' ) ); ?>
89 + </div>
90 + <?php else : ?>
91 + <?php if ( $activeCount > 1 ) :
92 + // Pre-encode the hash so add_query_arg's =-eating
93 + // regex doesn't strip the base64 padding (see
94 + // OptOutHandler::doRedirect for the full story).
95 + $encodedHash = rawurlencode( $hash );
96 + $bulkUrl = add_query_arg(
97 + array( 'optout_all' => $encodedHash, 'hash' => $encodedHash ),
98 + get_permalink()
99 + );
100 + ?>
101 + <div class="f12-doi-optout-bulk-bar">
102 + <a href="<?php echo esc_url( $bulkUrl ); ?>"
103 + class="f12-doi-optout-bulk"
104 + data-f12-confirm="<?php echo esc_attr( $L( 'bulk_confirm' ) ); ?>">
105 + <?php echo esc_html( $L( 'bulk_button' ) ); ?>
106 + </a>
107 + </div>
108 + <?php endif; ?>
109 +
110 + <table class="doi-table">
111 + <thead>
112 + <tr>
113 + <th><?php echo esc_html( $L( 'column_subscription' ) ); ?></th>
114 + <th><?php echo esc_html( $L( 'column_signed_up' ) ); ?></th>
115 + <th><?php echo esc_html( $L( 'column_status' ) ); ?></th>
116 + <th><?php echo esc_html( $L( 'column_valid_until' ) ); ?>*</th>
117 + <th></th>
118 + </tr>
119 + </thead>
120 + <tbody>
121 + <?php foreach ( $list as $OptIn ) :
122 + $formId = $OptIn->get_cf_form_id();
123 + $formTitle = $formId > 0 ? get_the_title( $formId ) : '';
124 + if ( $formTitle === '' ) {
125 + $formTitle = sprintf( __( 'Form #%d', 'double-opt-in' ), $formId );
126 + }
127 + $isConfirmed = $OptIn->is_confirmed();
128 + // Highlight only the SPECIFIC row that was just
129 + // acted on. Matching on $affected_hash means a
130 + // click on one of N identical-form duplicates
131 + // highlights just that one (was form_id before;
132 + // caught every duplicate, user-reported 2026-05-15).
133 + $justDone = ( $affected_hash !== '' && $OptIn->get_hash() === $affected_hash );
134 + $rowClass = $justDone ? 'f12-doi-optout-row--justdone' : '';
135 + ?>
136 + <tr class="<?php echo esc_attr( $rowClass ); ?>">
137 + <td class="f12-doi-optout-name">
138 + <?php echo esc_html( $formTitle ); ?>
139 + </td>
140 + <td>
141 + <?php echo esc_html( $OptIn->get_createtime( 'view' ) ); ?>
142 + </td>
143 + <td>
144 + <?php if ( $isConfirmed ) : ?>
145 + <span class="f12-doi-optout-badge f12-doi-optout-badge--active">
146 + <?php echo esc_html( $L( 'badge_active' ) ); ?>
147 + </span>
148 + <?php else : ?>
149 + <span class="f12-doi-optout-badge f12-doi-optout-badge--cancelled">
150 + <?php echo esc_html( $L( 'badge_cancelled' ) ); ?>
151 + </span>
152 + <?php endif; ?>
153 + </td>
154 + <td>
155 + <?php if ( $settings['delete'] == 0 || $settings['delete_unconfirmed'] == 0 ) : ?>
156 + <?php echo esc_html( '∞' ); ?>
157 + <?php else : ?>
158 + <?php echo esc_html( $OptIn->get_valid_until() ); ?>
159 + <?php endif; ?>
160 +
161 + <?php if ( $isConfirmed && $settings['delete'] != 0 ) : ?>
162 + <small>(<?php echo esc_html( $settings['delete'] ); ?> <?php echo esc_html( $settings['delete_period'] ); ?>)</small>
163 + <?php endif; ?>
164 + <?php if ( ! $isConfirmed && $settings['delete_unconfirmed'] != 0 ) : ?>
165 + <small>(<?php echo esc_html( $settings['delete_unconfirmed'] ); ?> <?php echo esc_html( $settings['delete_unconfirmed_period'] ); ?>)</small>
166 + <?php endif; ?>
167 + </td>
168 + <td>
169 + <?php if ( $isConfirmed ) :
170 + // Use get_permalink() instead of
171 + // OptIn::get_link_optout() — the
172 + // latter still reads from the legacy
173 + // `f12-doi-settings` blob and falls
174 + // back to home_url() when the page
175 + // isn't mirrored there from the new
176 + // option (user-reported 2026-05-15:
177 + // "Cancel takes me to homepage").
178 + // We're rendering inside the list
179 + // shortcode, so the current permalink
180 + // IS the opt-out page — no lookup
181 + // needed.
182 + $cancelUrl = add_query_arg(
183 + array(
184 + 'optout' => rawurlencode( $OptIn->get_hash() ),
185 + 'hash' => rawurlencode( $hash ),
186 + ),
187 + get_permalink()
188 + );
189 + ?>
190 + <a href="<?php echo esc_url( $cancelUrl ); ?>"
191 + class="button f12-doi-optout-cancel"
192 + data-f12-confirm="<?php echo esc_attr( $L( 'cancel_confirm' ) ); ?>">
193 + <?php echo esc_html( $L( 'cancel_button' ) ); ?>
194 + </a>
195 + <?php else : ?>
196 + <?php
197 + // Re-opt-in lives on the same page —
198 + // `?reoptin=<optin-hash>&hash=<owner>`
199 + // keeps the list-rendering hash on the
200 + // redirect target so the table comes
201 + // back with the row highlighted.
202 + // Pre-encoded to dodge add_query_arg's
203 + // =-stripping regex on base64 padding.
204 + $reoptinUrl = add_query_arg(
205 + array(
206 + 'reoptin' => rawurlencode( $OptIn->get_hash() ),
207 + 'hash' => rawurlencode( $hash ),
208 + ),
209 + get_permalink()
210 + );
211 + ?>
212 + <a href="<?php echo esc_url( $reoptinUrl ); ?>"
213 + class="button f12-doi-optout-reactivate"
214 + data-f12-confirm="<?php echo esc_attr( $L( 'reactivate_confirm' ) ); ?>">
215 + <?php echo esc_html( $L( 'reactivate_button' ) ); ?>
216 + </a>
217 + <?php endif; ?>
218 + </td>
219 + </tr>
220 + <?php endforeach; ?>
221 + </tbody>
222 + </table>
223 + <?php endif; ?>
224 + </div>
225 +
226 + <span class="hint">* <?php esc_html_e( 'The date indicates when your personal data will be deleted.', 'double-opt-in' ); ?></span>
227 + <span class="hint">*
228 + <?php if ( $manual_delete_value == 0 ) : ?>
229 + <?php printf(
230 + /* translators: %s = admin email */
231 + esc_html__( 'Please note that even with manual opt-out, your data will be stored. Please contact the administrator to delete your personal data: %s.', 'double-opt-in' ),
232 + esc_html( $admin_email )
233 + ); ?>
234 + <?php else : ?>
235 + <?php printf(
236 + /* translators: 1: number, 2: period (e.g. months) */
237 + esc_html__( 'Please note that even with manual opt-out, your data will only be deleted in %1$d %2$s.', 'double-opt-in' ),
238 + (int) $manual_delete_value,
239 + esc_html( $manual_delete_period )
240 + ); ?>
241 + <?php endif; ?>
242 + </span>
243 +</div>
244 +
245 +<!--
246 + Styled confirm modal for cancel / bulk / reactivate actions.
247 + Uses the native <dialog> element — accessible (focus trap +
248 + Escape-to-close are built in), no JS framework needed. Hidden
249 + until JS upgrades a click; if <dialog> support is missing on
250 + very old browsers, the script falls back to window.confirm().
251 +-->
252 +<dialog class="f12-doi-optout-dialog" aria-labelledby="f12-doi-optout-dialog__prompt">
253 + <p class="f12-doi-optout-dialog__prompt" id="f12-doi-optout-dialog__prompt"></p>
254 + <div class="f12-doi-optout-dialog__actions">
255 + <button type="button" class="f12-doi-optout-dialog__cancel"><?php echo esc_html( $L( 'confirm_no' ) ); ?></button>
256 + <button type="button" class="f12-doi-optout-dialog__confirm"><?php echo esc_html( $L( 'confirm_yes' ) ); ?></button>
257 + </div>
258 +</dialog>
259 +
260 +<script>
261 +/*
262 + * Click-time confirm modal for the cancel / bulk / reactivate links.
263 + * Reads the prompt from data-f12-confirm on each anchor so the admin
264 + * can configure the text per action under /opt-out → Labels. Button
265 + * labels live in the modal itself (rendered server-side).
266 + *
267 + * Falls back to window.confirm() if the browser doesn't support the
268 + * <dialog> element (pre-2022 Safari etc.) so the action never
269 + * silently fires without a confirmation.
270 + *
271 + * Lives inline because the script is tiny + only needed when the
272 + * list shortcode renders. An enqueued asset would cost an extra HTTP
273 + * request for ~30 lines of JS.
274 + */
275 +(function () {
276 + var dialog = document.querySelector('.f12-doi-optout-dialog');
277 + var supportsDialog = dialog && typeof dialog.showModal === 'function';
278 +
279 + if (!supportsDialog && dialog) {
280 + // Old-browser fallback: rip the <dialog> out of the DOM so
281 + // its content doesn't render as a visible block of text.
282 + dialog.parentNode.removeChild(dialog);
283 + dialog = null;
284 + }
285 +
286 + var pendingHref = null;
287 + if (dialog) {
288 + var promptEl = dialog.querySelector('.f12-doi-optout-dialog__prompt');
289 + var yesBtn = dialog.querySelector('.f12-doi-optout-dialog__confirm');
290 + var noBtn = dialog.querySelector('.f12-doi-optout-dialog__cancel');
291 +
292 + yesBtn.addEventListener('click', function () {
293 + dialog.close();
294 + if (pendingHref) {
295 + window.location.href = pendingHref;
296 + pendingHref = null;
297 + }
298 + });
299 + noBtn.addEventListener('click', function () {
300 + pendingHref = null;
301 + dialog.close();
302 + });
303 + // Cancel = Escape key + clicking outside → also reset.
304 + dialog.addEventListener('cancel', function () { pendingHref = null; });
305 + dialog.addEventListener('close', function () { pendingHref = null; });
306 + }
307 +
308 + document.querySelectorAll('[data-f12-confirm]').forEach(function (el) {
309 + el.addEventListener('click', function (ev) {
310 + var msg = el.getAttribute('data-f12-confirm') || '';
311 + if (!msg) return;
312 +
313 + ev.preventDefault();
314 +
315 + if (!supportsDialog) {
316 + if (window.confirm(msg)) {
317 + window.location.href = el.href;
318 + }
319 + return;
320 + }
321 +
322 + promptEl.textContent = msg;
323 + pendingHref = el.href;
324 + dialog.showModal();
325 + });
326 + });
327 +})();
328 +</script>