PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.9.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.9.1
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / inc / ai-form-builder / field-mapping.php

field-mapping.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.9.1, at inc/ai-form-builder/field-mapping.php

463 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureForms - AI Form Builder.
4 *
5 * @package sureforms
6 * @since 0.0.8
7 */
8
9 namespace SRFM\Inc\AI_Form_Builder;
10
11 use SRFM\Inc\Helper;
12 use SRFM\Inc\Traits\Get_Instance;
13 use WP_Error;
14
15 // Exit if accessed directly.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * SureForms AI Form Builder Class.
22 */
23 class Field_Mapping {
24 use Get_Instance;
25
26 /**
27 * Generate Gutenberg Fields from AI data.
28 *
29 * @param \WP_REST_Request $request Full details about the request.
30 * @return string|WP_Error
31 */
32 public static function generate_gutenberg_fields_from_questions( $request ) {
33
34 // Get params from request.
35 $params = $request->get_params();
36
37 // check parama is empty or not and is an array and consist form_data key.
38 if ( empty( $params ) || ! is_array( $params ) || ! isset( $params['form_data'] ) || 0 === count( $params['form_data'] ) ) {
39 return new WP_Error(
40 'srfm_ai_mapping_missing_form_data',
41 __( 'The AI form data is missing. Please try again.', 'sureforms' ),
42 [ 'status' => 400 ]
43 );
44 }
45
46 // Get questions from form data.
47 $form_data = $params['form_data'];
48 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
49 return new WP_Error(
50 'srfm_ai_mapping_invalid_form_data',
51 __( 'The AI form data is not in the expected format.', 'sureforms' ),
52 [ 'status' => 400 ]
53 );
54 }
55
56 $form = $form_data['form'] ?? null;
57 if ( empty( $form ) || ! is_array( $form ) ) {
58 return new WP_Error(
59 'srfm_ai_mapping_missing_form',
60 __( 'The AI response did not include a form. Please try again.', 'sureforms' ),
61 [ 'status' => 400 ]
62 );
63 }
64
65 $form_fields = $form['formFields'] ?? null;
66 if ( empty( $form_fields ) || ! is_array( $form_fields ) ) {
67 return new WP_Error(
68 'srfm_ai_mapping_missing_form_fields',
69 __( 'The AI was unable to generate form fields. Please try again.', 'sureforms' ),
70 [ 'status' => 400 ]
71 );
72 }
73
74 // Initialize post content string.
75 $post_content = '';
76
77 $is_conversational = isset( $params['is_conversional'] ) ? filter_var( $params['is_conversional'], FILTER_VALIDATE_BOOLEAN ) : false;
78 $form_type = isset( $params['form_type'] ) ? Helper::get_string_value( $params['form_type'] ) : 'simple';
79
80 // Filer to skip fields while mapping the fields.
81 $skip_fields = apply_filters( 'srfm_ai_field_map_skip_fields', [], $is_conversational, $form_type );
82
83 // Loop through questions.
84 foreach ( $form_fields as $question ) {
85
86 // Check if question is empty then continue to next question.
87 if ( empty( $question ) || ! is_array( $question ) ) {
88 return new WP_Error(
89 'srfm_ai_mapping_invalid_field',
90 __( 'The AI returned a malformed form field. Please try again.', 'sureforms' ),
91 [ 'status' => 400 ]
92 );
93 }
94
95 // Initialize common attributes.
96 $common_attributes = [
97 'block_id' => bin2hex( random_bytes( 4 ) ), // Generate random block_id.
98 'formId' => 0, // Set your formId here.
99 ];
100
101 // Merge common attributes with question attributes.
102 $merged_attributes = array_merge(
103 $common_attributes,
104 [
105 'label' => sanitize_text_field( $question['label'] ),
106 'required' => filter_var( $question['required'], FILTER_VALIDATE_BOOLEAN ),
107 'help' => isset( $question['helpText'] ) ? sanitize_text_field( $question['helpText'] ) : '',
108 'slug' => isset( $question['slug'] ) ? sanitize_text_field( $question['slug'] ) : '',
109 ]
110 );
111
112 // Apply filter to modify field type.
113 $field_type = apply_filters( 'srfm_ai_field_modify_field_type', $question['fieldType'], $question, $is_conversational, $form_type );
114
115 // Determine field type based on field_type.
116 switch ( $field_type ) {
117 case 'input':
118 case 'email':
119 case 'number':
120 case 'textarea':
121 case 'dropdown':
122 case 'checkbox':
123 case 'address':
124 case 'inline-button':
125 case 'gdpr':
126 case 'multi-choice':
127 case 'url':
128 case 'phone':
129 case 'payment':
130 // if payment block then map payment specific attributes.
131 if ( 'payment' === $field_type ) {
132 // Amount-unit convention (do not change without auditing the full
133 // chain): the AI prompt schema describes fixedAmount / oneTimeFixedAmount
134 // / subscriptionFixedAmount in MAJOR units (dollars/euros/etc.) using
135 // dollar-magnitude examples (e.g. 99, 1500). All downstream layers
136 // agree: block attrs and stored block_config keep the value in major
137 // units, frontend JS multiplies by 100 only at the boundary when posting
138 // to create_payment_intent, and the server divides it back via
139 // Stripe_Helper::amount_from_stripe_format() before validating against
140 // the stored fixed_amount. Stripe API itself is the only consumer that
141 // expects minor units and it is fed the JS-multiplied value. Reviewers:
142 // do not flag a "cents vs dollars ambiguity" here — the convention is
143 // consistent end-to-end, and adding a unit declaration to the AI schema
144 // would actually break the existing pipeline.
145 //
146 // Default-amount convention (do not change without auditing every
147 // callsite): the fallback `10` used when the AI omits fixedAmount /
148 // oneTimeFixedAmount / subscriptionFixedAmount is the same starter
149 // value that block.json sets when an admin manually adds a payment
150 // block in the Gutenberg editor. payment-markup.php and
151 // field-validation.php apply the same default. AI-generated forms
152 // therefore behave identically to manually-built forms when an amount
153 // is missing — admin reviews the form preview and adjusts before
154 // publishing. The schema marks these three amounts as `required`, so
155 // in practice this fallback only fires for malformed AI responses;
156 // changing it to 0 would make the manual-editor UX worse without
157 // closing any real revenue-loss vector. Reviewers: do not flag the
158 // `10` default here as a hidden charge — it is the project-wide
159 // payment-block starter value.
160 //
161 // Update-flow caveat (pre-existing, not specific to "both" mode):
162 // generate_gutenberg_fields_from_questions() is also called by the
163 // update-form ability (inc/abilities/forms/update-form.php) which
164 // regenerates the entire post_content from the AI's input. There is
165 // no merge with the form's currently-saved attributes — every field
166 // type's default-on-omit behavior applies. If an AI partial update
167 // omits a field attribute (e.g. a previously-saved subscriptionFixedAmount
168 // of $15), the default kicks in and overwrites the saved value. This
169 // is a long-standing characteristic of the update flow, affecting all
170 // fields equally; it is not a regression introduced by the "both"
171 // payment-type work and should be addressed (if at all) by teaching
172 // generate_gutenberg_fields_from_questions to merge with existing block
173 // attrs — a broader refactor outside this scope. Reviewers: do not
174 // flag this as a payment-specific bug.
175 //
176 // Schema "required" scope (sureforms-ai-templates/payment.json):
177 // the JSON schema lists every payment property — including all 11
178 // "both"-mode attrs — in a single flat `required` array applied to
179 // every payment field, not scoped per paymentType. This is a
180 // constraint of OpenAI's strict structured output mode: when
181 // `additionalProperties: false` is set, every property must also
182 // appear in `required`. The per-property `description` strings tell
183 // the model to emit empty strings / 0 for inapplicable modes (e.g.
184 // `oneTimeLabel: ''` when paymentType='one-time'). The mapping below
185 // only reads those attrs when paymentType='both', so empty values
186 // for other modes are silently and correctly dropped — there is no
187 // silent conflict. Reviewers: do not flag the flat `required` list
188 // as a scoping bug; it is how OpenAI strict mode works.
189 $amount_types = [ 'fixed', 'variable', 'user-choice' ];
190 $intervals = [ 'day', 'week', 'month', 'quarter', 'year' ];
191
192 $merged_attributes['customerNameField'] = isset( $question['customerNameField'] ) ? sanitize_text_field( $question['customerNameField'] ) : '';
193 $merged_attributes['customerEmailField'] = isset( $question['customerEmailField'] ) ? sanitize_text_field( $question['customerEmailField'] ) : '';
194 $merged_attributes['paymentType'] = isset( $question['paymentType'] ) && in_array( $question['paymentType'], [ 'one-time', 'subscription', 'both' ], true ) ? sanitize_text_field( $question['paymentType'] ) : 'one-time';
195 $merged_attributes['subscriptionPlan'] = isset( $question['subscriptionPlan'] ) && is_array( $question['subscriptionPlan'] ) ? [
196 'name' => isset( $question['subscriptionPlan']['name'] ) ? sanitize_text_field( $question['subscriptionPlan']['name'] ) : 'Subscription Plan',
197 'interval' => isset( $question['subscriptionPlan']['interval'] ) && in_array( $question['subscriptionPlan']['interval'], $intervals, true ) ? sanitize_text_field( $question['subscriptionPlan']['interval'] ) : 'month',
198 'billingCycles' => isset( $question['subscriptionPlan']['billingCycles'] ) ? ( is_numeric( $question['subscriptionPlan']['billingCycles'] ) ? intval( $question['subscriptionPlan']['billingCycles'] ) : sanitize_text_field( $question['subscriptionPlan']['billingCycles'] ) ) : 'ongoing',
199 ] : [
200 'name' => 'Subscription Plan',
201 'interval' => 'month',
202 'billingCycles' => 'ongoing',
203 ];
204 $merged_attributes['amountType'] = isset( $question['amountType'] ) && in_array( $question['amountType'], $amount_types, true ) ? sanitize_text_field( $question['amountType'] ) : 'fixed';
205 $merged_attributes['fixedAmount'] = isset( $question['fixedAmount'] ) && is_numeric( $question['fixedAmount'] ) ? floatval( $question['fixedAmount'] ) : 10;
206 $merged_attributes['minimumAmount'] = isset( $question['minimumAmount'] ) && is_numeric( $question['minimumAmount'] ) ? floatval( $question['minimumAmount'] ) : 0;
207 $merged_attributes['amountLabel'] = isset( $question['amountLabel'] ) ? sanitize_text_field( $question['amountLabel'] ) : 'Enter Amount';
208 $merged_attributes['variableAmountField'] = isset( $question['variableAmountField'] ) ? sanitize_text_field( $question['variableAmountField'] ) : '';
209
210 // "Both" mode attributes — admins configure one-time AND subscription in the same block.
211 if ( 'both' === $merged_attributes['paymentType'] ) {
212 $merged_attributes['oneTimeLabel'] = isset( $question['oneTimeLabel'] ) ? sanitize_text_field( $question['oneTimeLabel'] ) : 'One-Time Payment';
213 $merged_attributes['subscriptionLabel'] = isset( $question['subscriptionLabel'] ) ? sanitize_text_field( $question['subscriptionLabel'] ) : 'Subscription';
214 $merged_attributes['defaultPaymentChoice'] = isset( $question['defaultPaymentChoice'] ) && in_array( $question['defaultPaymentChoice'], [ 'one-time', 'subscription' ], true ) ? sanitize_text_field( $question['defaultPaymentChoice'] ) : 'one-time';
215 $merged_attributes['oneTimeAmountType'] = isset( $question['oneTimeAmountType'] ) && in_array( $question['oneTimeAmountType'], $amount_types, true ) ? sanitize_text_field( $question['oneTimeAmountType'] ) : 'fixed';
216 $merged_attributes['oneTimeFixedAmount'] = isset( $question['oneTimeFixedAmount'] ) && is_numeric( $question['oneTimeFixedAmount'] ) ? floatval( $question['oneTimeFixedAmount'] ) : 10;
217 $merged_attributes['oneTimeMinimumAmount'] = isset( $question['oneTimeMinimumAmount'] ) && is_numeric( $question['oneTimeMinimumAmount'] ) ? floatval( $question['oneTimeMinimumAmount'] ) : 0;
218 $merged_attributes['oneTimeVariableAmountField'] = isset( $question['oneTimeVariableAmountField'] ) ? sanitize_text_field( $question['oneTimeVariableAmountField'] ) : '';
219 $merged_attributes['subscriptionAmountType'] = isset( $question['subscriptionAmountType'] ) && in_array( $question['subscriptionAmountType'], $amount_types, true ) ? sanitize_text_field( $question['subscriptionAmountType'] ) : 'fixed';
220 $merged_attributes['subscriptionFixedAmount'] = isset( $question['subscriptionFixedAmount'] ) && is_numeric( $question['subscriptionFixedAmount'] ) ? floatval( $question['subscriptionFixedAmount'] ) : 10;
221 $merged_attributes['subscriptionMinimumAmount'] = isset( $question['subscriptionMinimumAmount'] ) && is_numeric( $question['subscriptionMinimumAmount'] ) ? floatval( $question['subscriptionMinimumAmount'] ) : 0;
222 $merged_attributes['subscriptionVariableAmountField'] = isset( $question['subscriptionVariableAmountField'] ) ? sanitize_text_field( $question['subscriptionVariableAmountField'] ) : '';
223 }
224 }
225
226 // Handle specific attributes for certain fields.
227 if ( 'dropdown' === $field_type && ! empty( $question['fieldOptions'] ) && is_array( $question['fieldOptions'] ) &&
228 ! empty( $question['fieldOptions'][0]['label'] )
229 ) {
230 // Defense-in-depth: although the upstream middleware is
231 // trusted and these endpoints are capability-gated,
232 // strings flow into Gutenberg block markup so we run
233 // the user-facing fields through sanitize_text_field.
234 $merged_attributes['options'] = self::sanitize_field_options( $question['fieldOptions'] );
235
236 if ( isset( $question['showValues'] ) ) {
237 $merged_attributes['showValues'] = filter_var( $question['showValues'], FILTER_VALIDATE_BOOLEAN );
238 }
239
240 // remove icon from options for the dropdown field.
241 foreach ( $merged_attributes['options'] as $key => $option ) {
242 if ( ! empty( $merged_attributes['options'][ $key ]['icon'] ) ) {
243 $merged_attributes['options'][ $key ]['icon'] = '';
244 }
245 }
246 }
247 if ( 'multi-choice' === $field_type ) {
248
249 // Remove duplicate icons and clear icons if all are the same.
250 $icons = array_column( $question['fieldOptions'], 'icon' );
251 $options = array_column( $question['fieldOptions'], 'optionTitle' );
252 $unique_icons = array_unique( $icons );
253 if ( count( $unique_icons ) === 1 || count( $options ) !== count( $icons ) ) {
254 foreach ( $question['fieldOptions'] as &$option ) {
255 $option['icon'] = '';
256 }
257 }
258
259 // Set options if they are valid.
260 if ( ! empty( $question['fieldOptions'][0]['optionTitle'] ) ) {
261 // Same defense-in-depth sanitization as the
262 // dropdown branch above.
263 $merged_attributes['options'] = self::sanitize_field_options( $question['fieldOptions'] );
264 }
265
266 // Determine vertical layout based on icons.
267 if ( ! empty( $merged_attributes['options'] ) ) {
268 $merged_attributes['verticalLayout'] = array_reduce(
269 $merged_attributes['options'],
270 static fn( $carry, $option ) => $carry && ! empty( $option['icon'] ),
271 true
272 );
273 }
274
275 if ( isset( $question['showValues'] ) ) {
276 $merged_attributes['showValues'] = filter_var( $question['showValues'], FILTER_VALIDATE_BOOLEAN );
277 }
278
279 // Set single selection if provided.
280 if ( isset( $question['singleSelection'] ) ) {
281 $merged_attributes['singleSelection'] = filter_var( $question['singleSelection'], FILTER_VALIDATE_BOOLEAN );
282 }
283
284 // Set choiceWidth for options divisible by 3.
285 if ( ! empty( $merged_attributes['options'] ) && count( $merged_attributes['options'] ) % 3 === 0 ) {
286 $merged_attributes['choiceWidth'] = 33.33;
287 }
288 }
289 if ( 'phone' === $field_type ) {
290 $merged_attributes['autoCountry'] = true;
291 }
292
293 // Apply filter to modify merged attributes.
294 $merged_attributes = apply_filters( 'srfm_ai_form_builder_modify_merged_attributes', $merged_attributes, $question, $is_conversational, $form_type );
295
296 // if field type is needs to be skipped then skip that field.
297 if ( ! empty( $skip_fields ) && in_array( $field_type, $skip_fields, true ) ) {
298 break;
299 }
300
301 $post_content .= '<!-- wp:srfm/' . $field_type . ' ' . Helper::encode_json( $merged_attributes ) . ' /-->' . PHP_EOL;
302 break;
303 case 'slider':
304 case 'page-break':
305 case 'date-picker':
306 case 'time-picker':
307 case 'upload':
308 case 'hidden':
309 case 'rating':
310 case 'signature':
311 case 'nps':
312 // If pro version is not active then do not add pro fields.
313 if ( ! defined( 'SRFM_PRO_VER' ) ) {
314 break;
315 }
316
317 if ( 'signature' === $field_type && defined( 'SRFM_PRO_PRODUCT' ) && SRFM_PRO_PRODUCT === 'SureForms Starter' ) {
318 // If the product is SureForms Starter then skip the signature field.
319 break;
320 }
321
322 // Handle specific attributes for certain pro fields.
323 if ( 'slider' === $field_type ) {
324 $merged_attributes['min'] = ! empty( $question['min'] ) ? filter_var( $question['min'], FILTER_VALIDATE_INT ) : 0;
325 $merged_attributes['max'] = ! empty( $question['max'] ) ? filter_var( $question['max'], FILTER_VALIDATE_INT ) : 100;
326 $merged_attributes['step'] = ! empty( $question['step'] ) ? filter_var( $question['step'], FILTER_VALIDATE_INT ) : 1;
327
328 $merged_attributes['prefixTooltip'] = ! empty( $question['prefixTooltip'] ) ? $question['prefixTooltip'] : '';
329 $merged_attributes['suffixTooltip'] = ! empty( $question['suffixTooltip'] ) ? $question['suffixTooltip'] : '';
330
331 // get min and max then diveide by 2 and round it.
332 $min = $merged_attributes['min'];
333 $max = $merged_attributes['max'];
334
335 if ( is_numeric( $min ) && is_numeric( $max ) ) {
336 $min = intval( $min );
337 $max = intval( $max );
338
339 // If min and max are same then set the value to 0.
340 $merged_attributes['numberDefaultValue'] = Helper::get_string_value( round( ( $min + $max ) / 2 ) );
341 }
342 }
343 if ( 'date-picker' === $field_type ) {
344 $merged_attributes['dateFormat'] = ! empty( $question['dateFormat'] ) ? sanitize_text_field( $question['dateFormat'] ) : 'mm/dd/yy';
345 $merged_attributes['min'] = ! empty( $question['minDate'] ) ? sanitize_text_field( $question['minDate'] ) : '';
346 $merged_attributes['max'] = ! empty( $question['maxDate'] ) ? sanitize_text_field( $question['maxDate'] ) : '';
347 }
348 if ( 'time-picker' === $field_type ) {
349 $merged_attributes['increment'] = ! empty( $question['increment'] ) ? filter_var( $question['increment'], FILTER_VALIDATE_INT ) : 30;
350 $merged_attributes['showTwelveHourFormat'] = ! empty( $question['showTwelveHourFormat'] ) ? filter_var( $question['useTwelveHourFormat'], FILTER_VALIDATE_BOOLEAN ) : false;
351 $merged_attributes['min'] = ! empty( $question['minTime'] ) ? sanitize_text_field( $question['minTime'] ) : '';
352 $merged_attributes['max'] = ! empty( $question['maxTime'] ) ? sanitize_text_field( $question['maxTime'] ) : '';
353 }
354 if ( 'rating' === $field_type ) {
355 $merged_attributes['iconShape'] = ! empty( $question['iconShape'] ) ? sanitize_text_field( $question['iconShape'] ) : 'star';
356 $merged_attributes['showText'] = ! empty( $question['showTooltip'] ) ? filter_var( $question['showTooltip'], FILTER_VALIDATE_BOOLEAN ) : false;
357 $merged_attributes['defaultRating'] = ! empty( $question['defaultRating'] ) ? filter_var( $question['defaultRating'], FILTER_VALIDATE_INT ) : 0;
358
359 if ( ! empty( $merged_attributes['showText'] ) ) {
360 foreach ( $question['tooltipValues'] as $tooltips ) {
361 $i = 0;
362 foreach ( $tooltips as $value ) {
363 $merged_attributes['ratingText'][ $i ] = ! empty( $value ) ? sanitize_text_field( $value ) : '';
364 $i++;
365 }
366 }
367 }
368 }
369 if ( 'upload' === $field_type ) {
370 if ( ! empty( $question['allowedTypes'] ) ) {
371 $allowed_types = str_replace( '.', '', $question['allowedTypes'] );
372
373 $allowed_types = explode( ',', $allowed_types );
374
375 $types_array = array_map(
376 static function( $type ) {
377 return [
378 'value' => trim( $type ),
379 'label' => trim( $type ),
380 ];
381 },
382 $allowed_types
383 );
384
385 $merged_attributes['allowedFormats'] = $types_array;
386 } else {
387 $merged_attributes['allowedFormats'] = [
388 [
389 'value' => 'jpg',
390 'label' => 'jpg',
391 ],
392 [
393 'value' => 'jpeg',
394 'label' => 'jpeg',
395 ],
396 [
397 'value' => 'gif',
398 'label' => 'gif',
399 ],
400 [
401 'value' => 'png',
402 'label' => 'png',
403 ],
404 [
405 'value' => 'pdf',
406 'label' => 'pdf',
407 ],
408 ];
409 }
410
411 $merged_attributes['fileSizeLimit'] = ! empty( $question['uploadSize'] ) ? filter_var( $question['uploadSize'], FILTER_VALIDATE_INT ) : 10;
412
413 $merged_attributes['multiple'] = ! empty( $question['multiUpload'] ) ? filter_var( $question['multiUpload'], FILTER_VALIDATE_BOOLEAN ) : false;
414
415 $merged_attributes['maxFiles'] = ! empty( $question['multiFilesNumber'] ) ? filter_var( $question['multiFilesNumber'], FILTER_VALIDATE_INT ) : 2;
416
417 }
418
419 $post_content .= '<!-- wp:srfm/' . $field_type . ' ' . Helper::encode_json( $merged_attributes ) . ' /-->' . PHP_EOL;
420 break;
421 default:
422 // Unsupported field type - fallback to input.
423 $post_content .= '<!-- wp:srfm/input ' . Helper::encode_json( $merged_attributes ) . ' /-->' . PHP_EOL;
424 }
425 }
426
427 return apply_filters( 'srfm_ai_form_builder_post_content', $post_content, $is_conversational, $form_type );
428 }
429
430 /**
431 * Sanitize the user-facing strings on each entry of the AI-generated
432 * fieldOptions array before they are merged into block attributes.
433 *
434 * Defense-in-depth: the middleware is trusted today, but these strings
435 * are serialized into Gutenberg block markup. Running each string field
436 * through sanitize_text_field() prevents stored-content injection if
437 * the upstream ever returns reflected user content. Non-string fields
438 * (icon class names, booleans) are left untouched.
439 *
440 * @param array<int, array<string, mixed>> $options Raw fieldOptions array.
441 * @since 2.8.2
442 * @return array<int, array<string, mixed>> Sanitized options.
443 */
444 private static function sanitize_field_options( $options ) {
445 if ( ! is_array( $options ) ) {
446 return [];
447 }
448 $sanitizable_keys = [ 'label', 'value', 'optionTitle' ];
449 foreach ( $options as $key => $option ) {
450 if ( ! is_array( $option ) ) {
451 continue;
452 }
453 foreach ( $sanitizable_keys as $field ) {
454 if ( isset( $option[ $field ] ) && is_string( $option[ $field ] ) ) {
455 $options[ $key ][ $field ] = sanitize_text_field( $option[ $field ] );
456 }
457 }
458 }
459 return $options;
460 }
461
462 }
463