PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.1.6
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.1.6
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 / FormSettings / FormSettingsValidator.php

FormSettingsValidator.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.1.6, at src/FormSettings/FormSettingsValidator.php

285 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Form Settings Validator
4 *
5 * @package Forge12\DoubleOptIn\FormSettings
6 * @since 4.1.0
7 */
8
9 namespace Forge12\DoubleOptIn\FormSettings;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class FormSettingsValidator
17 *
18 * Validates form settings before saving.
19 */
20 class FormSettingsValidator {
21
22 /**
23 * Validate form settings.
24 *
25 * @param FormSettingsDTO $settings The settings to validate.
26 *
27 * @return array Array of validation errors. Empty if valid.
28 */
29 public function validate( FormSettingsDTO $settings ): array {
30 $errors = array();
31
32 // Only validate if enabled
33 if ( ! $settings->enabled ) {
34 return $errors;
35 }
36
37 // Hart-required fields — delegate to the DTO so save-time and
38 // completeness-gate share one source of truth (plan/doi-completeness-gate.md §2.1).
39 // The DTO returns stable string IDs; we map them to translatable messages here.
40 foreach ( $settings->getMissingRequiredFields() as $field ) {
41 $errors[ $field ] = $this->messageForMissingField( $field );
42 }
43
44 // Format-only checks live in the Validator (the DTO is shape-only,
45 // no I/O — page existence and category lookups need wpdb).
46
47 // Validate sender email format (soft-required: empty is OK,
48 // falls back to WP admin email at runtime — plan §5.3)
49 if ( ! empty( $settings->sender ) && ! $this->isValidEmailOrPlaceholder( $settings->sender ) ) {
50 $errors['sender'] = __( 'Invalid sender email format.', 'double-opt-in' );
51 }
52
53 // Validate confirmation page
54 if ( $settings->confirmationPage > 0 ) {
55 $page = get_post( $settings->confirmationPage );
56 if ( ! $page || $page->post_type !== 'page' ) {
57 $errors['confirmationPage'] = __( 'Invalid confirmation page selected.', 'double-opt-in' );
58 }
59 }
60
61 // Validate error redirect page
62 if ( $settings->errorRedirectPage > 0 ) {
63 $page = get_post( $settings->errorRedirectPage );
64 if ( ! $page || $page->post_type !== 'page' ) {
65 $errors['errorRedirectPage'] = __( 'Invalid error redirect page selected.', 'double-opt-in' );
66 }
67 }
68
69 // Validate category
70 if ( $settings->category > 0 ) {
71 $category = \forge12\contactform7\CF7DoubleOptIn\Category::get_by_id( $settings->category );
72 if ( ! $category ) {
73 $errors['category'] = __( 'Invalid category selected.', 'double-opt-in' );
74 }
75 }
76
77 return $errors;
78 }
79
80 /**
81 * Sanitize form settings.
82 *
83 * @param array $data Raw input data.
84 *
85 * @return FormSettingsDTO Sanitized settings DTO.
86 */
87 public function sanitize( array $data ): FormSettingsDTO {
88 $dto = new FormSettingsDTO();
89
90 $dto->enabled = ! empty( $data['enabled'] ) || ! empty( $data['enable'] );
91
92 $dto->sender = isset( $data['sender'] )
93 ? $this->sanitizeEmailOrPlaceholder( $data['sender'] )
94 : '';
95
96 $dto->senderName = isset( $data['senderName'] ) || isset( $data['sender_name'] )
97 ? sanitize_text_field( $data['senderName'] ?? $data['sender_name'] )
98 : '';
99
100 $dto->subject = isset( $data['subject'] )
101 ? sanitize_text_field( $data['subject'] )
102 : '';
103
104 $dto->body = isset( $data['body'] )
105 ? $this->sanitizeBody( $data['body'] )
106 : '';
107
108 $dto->recipient = isset( $data['recipient'] )
109 ? sanitize_text_field( $data['recipient'] )
110 : '';
111
112 $dto->confirmationPage = isset( $data['confirmationPage'] ) || isset( $data['page'] )
113 ? (int) ( $data['confirmationPage'] ?? $data['page'] )
114 : -1;
115
116 $dto->errorRedirectPage = isset( $data['errorRedirectPage'] ) || isset( $data['error_page'] )
117 ? (int) ( $data['errorRedirectPage'] ?? $data['error_page'] )
118 : -1;
119
120 $dto->conditions = isset( $data['conditions'] )
121 ? sanitize_text_field( $data['conditions'] )
122 : 'disabled';
123
124 $dto->template = isset( $data['template'] )
125 ? sanitize_text_field( $data['template'] )
126 : '';
127
128 $dto->category = isset( $data['category'] )
129 ? absint( $data['category'] )
130 : 0;
131
132 $dto->consentText = isset( $data['consentText'] ) || isset( $data['consent_text'] )
133 ? sanitize_textarea_field( $data['consentText'] ?? $data['consent_text'] )
134 : '';
135
136 // Consent acknowledgment field — name of the form field that
137 // captures the user's explicit consent (e.g. CF7 [acceptance]).
138 // Stored as a sanitize_key string since it must match a real
139 // form-field name at submit time.
140 $dto->consentField = isset( $data['consentField'] ) || isset( $data['consent_field'] )
141 ? sanitize_key( (string) ( $data['consentField'] ?? $data['consent_field'] ) )
142 : '';
143
144 // Field-mapping (placeholder-tag → form-field-name) for the
145 // Mapping tab. Sanitize each key + value to text-safe strings
146 // since both end up in the email body / database meta.
147 $rawMapping = $data['fieldMapping'] ?? $data['field_mapping'] ?? array();
148 if ( is_array( $rawMapping ) ) {
149 $mapping = array();
150 foreach ( $rawMapping as $key => $value ) {
151 $mapping[ sanitize_key( (string) $key ) ] = sanitize_text_field( (string) $value );
152 }
153 $dto->fieldMapping = $mapping;
154 }
155
156 /**
157 * Filter to allow addons to sanitize and contribute their own
158 * extensionData fields. Mirrors `f12_doi_settings_dto_from_array`
159 * (load-side) so save and load are symmetric — both go through
160 * the same addon-side filter chain. An addon that registers one
161 * filter without the other silently loses data on roundtrip.
162 *
163 * @since 4.4.0
164 *
165 * @param FormSettingsDTO $dto The DTO populated with Core fields.
166 * @param array $data The raw input array.
167 */
168 $dto = apply_filters( 'f12_doi_settings_dto_sanitize', $dto, $data );
169
170 return $dto;
171 }
172
173 /**
174 * Map a missing-required-field ID (as returned by
175 * {@see FormSettingsDTO::getMissingRequiredFields()}) to a translatable
176 * user-facing error message.
177 *
178 * Addon-contributed IDs fall through to a generic message; addons
179 * that need bespoke wording should filter the messages array via
180 * `f12_doi_form_missing_field_messages` (registered at apply_filters
181 * time below).
182 */
183 private function messageForMissingField( string $field ): string {
184 $messages = apply_filters(
185 'f12_doi_form_missing_field_messages',
186 array(
187 'recipient' => __( 'Recipient field is required.', 'double-opt-in' ),
188 'subject' => __( 'Email subject is required.', 'double-opt-in' ),
189 'body_or_template' => __( 'Email body must contain the [doubleoptinlink] placeholder or a template must be selected.', 'double-opt-in' ),
190 )
191 );
192
193 return $messages[ $field ] ?? __( 'Required field is missing.', 'double-opt-in' );
194 }
195
196 /**
197 * Check if a value is a valid email or placeholder.
198 *
199 * @param string $value The value to check.
200 *
201 * @return bool True if valid.
202 */
203 private function isValidEmailOrPlaceholder( string $value ): bool {
204 // Check for placeholder format [field_name]
205 if ( preg_match( '/^\[.+\]$/', $value ) ) {
206 return true;
207 }
208
209 return is_email( $value ) !== false;
210 }
211
212 /**
213 * Sanitize an email or placeholder value.
214 *
215 * @param string $value The value to sanitize.
216 *
217 * @return string Sanitized value.
218 */
219 private function sanitizeEmailOrPlaceholder( string $value ): string {
220 $value = trim( $value );
221
222 // If it's a placeholder, sanitize as text
223 if ( preg_match( '/^\[.+\]$/', $value ) ) {
224 return sanitize_text_field( $value );
225 }
226
227 // Otherwise sanitize as email
228 $email = sanitize_email( $value );
229 return $email ?: $value;
230 }
231
232 /**
233 * Sanitize email body content.
234 *
235 * Allows HTML but removes dangerous content.
236 *
237 * @param string $body The body content.
238 *
239 * @return string Sanitized body.
240 */
241 private function sanitizeBody( string $body ): string {
242 // Allow HTML tags used in email templates
243 $allowedHtml = wp_kses_allowed_html( 'post' );
244
245 // Add additional tags commonly used in emails
246 $allowedHtml['style'] = array();
247 $allowedHtml['center'] = array();
248 $allowedHtml['table'] = array(
249 'class' => true,
250 'id' => true,
251 'style' => true,
252 'width' => true,
253 'height' => true,
254 'cellpadding' => true,
255 'cellspacing' => true,
256 'border' => true,
257 'align' => true,
258 'bgcolor' => true,
259 );
260 $allowedHtml['tr'] = array(
261 'class' => true,
262 'style' => true,
263 'align' => true,
264 'valign' => true,
265 );
266 $allowedHtml['td'] = array(
267 'class' => true,
268 'style' => true,
269 'width' => true,
270 'height' => true,
271 'align' => true,
272 'valign' => true,
273 'bgcolor' => true,
274 'colspan' => true,
275 'rowspan' => true,
276 );
277 $allowedHtml['th'] = $allowedHtml['td'];
278 $allowedHtml['thead'] = array();
279 $allowedHtml['tbody'] = array();
280 $allowedHtml['tfoot'] = array();
281
282 return wp_kses( $body, $allowedHtml );
283 }
284 }
285