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
double-opt-in / templates / optout-list.php

optout-list.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.6.2, at templates/optout-list.php

329 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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>
329