PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.2.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.2.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / privacy / privacy-frontend.php

privacy-frontend.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.2.0, at inc/privacy/privacy-frontend.php

235 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Privacy Frontend
4 *
5 * Renders the donation-form privacy fields (contact-consent checkbox, privacy-policy
6 * blurb, terms blurb) that the Privacy settings enable, expanding the [privacy_policy]
7 * / [terms] tokens within those blurbs (scoped — not registered as site-wide
8 * shortcodes). Also exposes the server-side consent validation used at the shared
9 * payment validation choke point.
10 *
11 * @package SureDonation
12 * @since 1.2.0
13 */
14
15 namespace SureDonation\Inc\Privacy;
16
17 use SureDonation\Inc\Helper;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit; // Exit if accessed directly.
21 }
22
23 /**
24 * Privacy_Frontend class.
25 *
26 * @since 1.2.0
27 */
28 class Privacy_Frontend {
29 /**
30 * POST field name for the contact-consent checkbox.
31 *
32 * @since 1.2.0
33 */
34 public const CONSENT_FIELD = 'suredonation_privacy_consent';
35
36 /**
37 * Render the donation-form privacy fields enabled in the Privacy settings.
38 *
39 * Output order matches Charitable: contact-consent checkbox, then the privacy
40 * policy statement, then the terms statement. Returns '' when none are enabled.
41 *
42 * @since 1.2.0
43 * @return string Escaped markup safe to echo before the donate button.
44 */
45 public static function render_form_fields() {
46 $settings = Privacy_Settings::get_settings();
47 $html = '';
48
49 if ( ! empty( $settings['contact_consent_field'] ) ) {
50 $required = ! empty( $settings['contact_consent_required'] );
51 $label = isset( $settings['contact_consent_label'] ) ? Helper::get_string_value( $settings['contact_consent_label'] ) : '';
52 $required_mark = $required ? ' <span class="sd-required" aria-hidden="true">*</span>' : '';
53
54 // Show the required error the same way the form-builder checkbox does: an
55 // inline .sd-error message (not the browser's native required popup). It is
56 // pre-rendered here (hidden) so client validation (validation.js validates
57 // .sd-privacy-consent on submit) can display it immediately, and the server
58 // also enforces consent (Payment_Helper::validate_submission keys it into
59 // field_errors by this input's data-slug) so showServerFieldErrors targets
60 // the same .sd-error. Note: no .sd-input-common on the checkbox (that class
61 // is text-input styling and would stretch the box); the form-builder checkbox
62 // omits it too. The .sd-error-wrap uses height:auto (not the design system's
63 // fixed error-line height) so it collapses when there is no error, keeping
64 // the gap to the privacy blurb equal to the other fields.
65 $error_slot = $required ? sprintf(
66 '<div class="sd-error-wrap" style="height:auto"><p class="sd-error" role="alert" style="display:none">%s</p></div>',
67 esc_html( self::consent_required_message() )
68 ) : '';
69
70 $html .= sprintf(
71 '<div class="sd-block-single sd-block sd-checkbox-block sd-consent-field">
72 <div class="sd-block-wrap sd-checkbox-wrap">
73 <label class="sd-checkbox-label">
74 <input type="checkbox" class="sd-input-checkbox sd-privacy-consent" name="%1$s" value="yes" data-slug="%1$s" data-required="%2$s" aria-required="%2$s" />
75 <span class="sd-checkbox-text">%3$s%4$s</span>
76 </label>
77 </div>
78 %5$s
79 </div>',
80 esc_attr( self::CONSENT_FIELD ),
81 esc_attr( $required ? 'true' : 'false' ),
82 esc_html( $label ),
83 $required_mark,
84 $error_slot
85 );
86 }
87
88 // Rendered as full-width paragraphs (sd-block-width-100 → flex-basis 100% so each
89 // sits on its own line) rather than sd-block-single, which is a flex column and would
90 // stack the inline [privacy_policy]/[terms] link onto its own line.
91 // margin:0 so the blurbs rely solely on the form's row-gap for spacing (the
92 // browser's default <p> margin would otherwise stack on top of it and make the
93 // gaps around these blurbs larger than between the other fields).
94 if ( ! empty( $settings['privacy_policy_field'] ) && ! empty( $settings['privacy_policy_text'] ) ) {
95 $html .= sprintf(
96 '<p class="sd-block-width-100 sd-privacy-policy" style="margin:0">%s</p>',
97 self::expand_tokens( wp_kses_post( Helper::get_string_value( $settings['privacy_policy_text'] ) ) )
98 );
99 }
100
101 if ( ! empty( $settings['terms_conditions_field'] ) && ! empty( $settings['terms_conditions_text'] ) ) {
102 $html .= sprintf(
103 '<p class="sd-block-width-100 sd-terms-conditions" style="margin:0">%s</p>',
104 self::expand_tokens( wp_kses_post( Helper::get_string_value( $settings['terms_conditions_text'] ) ) )
105 );
106 }
107
108 return $html;
109 }
110
111 /**
112 * The message shown when consent is required but not given.
113 *
114 * Shared by the pre-rendered inline .sd-error (client validation) and the
115 * server-side validate_consent() so both surface identical text.
116 *
117 * @since 1.2.0
118 * @return string
119 */
120 public static function consent_required_message() {
121 return __( 'Please acknowledge consent before submitting your donation.', 'suredonation' );
122 }
123
124 /**
125 * Validate the contact-consent requirement against the current request.
126 *
127 * Reads the consent checkbox from the POST body; the caller is responsible for
128 * nonce/token verification (mirrors Payment_Helper::get_submitted_donor_email).
129 * Returns '' when consent is not required or has been given.
130 *
131 * @since 1.2.0
132 * @return string Error message when consent is required but missing, else ''.
133 */
134 public static function validate_consent() {
135 $settings = Privacy_Settings::get_settings();
136
137 if ( empty( $settings['contact_consent_field'] ) || empty( $settings['contact_consent_required'] ) ) {
138 return '';
139 }
140
141 if ( 'yes' !== self::submitted_consent_value() ) {
142 return self::consent_required_message();
143 }
144
145 return '';
146 }
147
148 /**
149 * Resolve the [privacy_policy] link — the Privacy Policy page configured in the
150 * Privacy settings, falling back to WordPress core's assigned privacy policy page.
151 *
152 * @since 1.2.0
153 * @return string
154 */
155 private static function privacy_policy_link() {
156 $settings = Privacy_Settings::get_settings();
157 $page_id = isset( $settings['privacy_page_id'] ) ? absint( Helper::get_string_value( $settings['privacy_page_id'] ) ) : 0;
158 if ( 0 === $page_id ) {
159 $page_id = absint( Helper::get_string_value( get_option( 'wp_page_for_privacy_policy' ) ) );
160 }
161 return self::page_link( $page_id, __( 'Privacy Policy', 'suredonation' ) );
162 }
163
164 /**
165 * Resolve the [terms] link — the configured Terms & Conditions page.
166 *
167 * @since 1.2.0
168 * @return string
169 */
170 private static function terms_link() {
171 $settings = Privacy_Settings::get_settings();
172 $page_id = isset( $settings['terms_page_id'] ) ? absint( Helper::get_string_value( $settings['terms_page_id'] ) ) : 0;
173 return self::page_link( $page_id, __( 'Terms & Conditions', 'suredonation' ) );
174 }
175
176 /**
177 * Expand the scoped [privacy_policy] / [terms] tokens within a blurb string.
178 *
179 * Handled by a local str_replace rather than global shortcodes so the short,
180 * friendly tokens work inside the privacy/terms blurbs without registering
181 * site-wide shortcodes that would collide with other post/theme content
182 * (especially the very common [terms]).
183 *
184 * @since 1.2.0
185 * @param string $text Blurb text (already wp_kses_post'd).
186 * @return string
187 */
188 private static function expand_tokens( $text ) {
189 return str_replace(
190 [ '[privacy_policy]', '[terms]' ],
191 [ self::privacy_policy_link(), self::terms_link() ],
192 $text
193 );
194 }
195
196 /**
197 * Build a link to a page, or plain fallback text when the page is unset/invalid.
198 *
199 * @since 1.2.0
200 * @param int $page_id Page ID.
201 * @param string $fallback Text used when no page is set.
202 * @return string
203 */
204 private static function page_link( $page_id, $fallback ) {
205 if ( $page_id > 0 && 'publish' === get_post_status( $page_id ) ) {
206 return sprintf(
207 '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
208 esc_url( (string) get_permalink( $page_id ) ),
209 esc_html( get_the_title( $page_id ) )
210 );
211 }
212 return esc_html( $fallback );
213 }
214
215 /**
216 * Read the submitted consent value from the request.
217 *
218 * The AJAX gateways serialize every rendered field as fields[slug][value] (see
219 * gateway-base.js appendFieldValues), so the consent checkbox arrives nested under
220 * its data-slug; the no-JS native form POST sends it as a top-level field. Accept
221 * either location. The caller verifies the nonce/HMAC.
222 *
223 * @since 1.2.0
224 * @return string 'yes' when consent was given, else ''.
225 */
226 private static function submitted_consent_value() {
227 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce/HMAC verified by the calling handler.
228 $nested = isset( $_POST['fields'][ self::CONSENT_FIELD ]['value'] ) ? sanitize_text_field( wp_unslash( $_POST['fields'][ self::CONSENT_FIELD ]['value'] ) ) : '';
229 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce/HMAC verified by the calling handler.
230 $top = isset( $_POST[ self::CONSENT_FIELD ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::CONSENT_FIELD ] ) ) : '';
231
232 return 'yes' === $nested || 'yes' === $top ? 'yes' : '';
233 }
234 }
235