PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.1
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 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / migrator / block-templates.php
sureforms / inc / migrator Last commit date
importers 4 weeks ago base-migrator.php 1 month ago block-templates.php 1 month ago bootstrap.php 1 month ago
block-templates.php
493 lines
1 <?php
2 /**
3 * Block Templates — emits Gutenberg block markup for each SureForms field block.
4 *
5 * Acts as the canonical destination dictionary for the form migrator. Each
6 * `template_*()` method returns a serialized Gutenberg block string that can
7 * be concatenated into a SureForms form's `post_content`.
8 *
9 * Attribute schemas mirror `src/blocks/<block>/block.json` — only attributes
10 * different from the block's defaults are emitted, to keep imported forms
11 * indistinguishable from hand-built ones in the editor.
12 *
13 * @package sureforms
14 * @since 2.11.0
15 */
16
17 namespace SRFM\Inc\Migrator;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Block_Templates
25 *
26 * @since 2.11.0
27 */
28 class Block_Templates {
29 /**
30 * Build a srfm/input block (text / email-as-text / password / date / hidden).
31 *
32 * @since 2.11.0
33 *
34 * @param array<string,mixed> $args Field args (label, placeholder, required, fieldType, etc.).
35 * @return string Serialized Gutenberg block markup.
36 */
37 public static function input( array $args ) {
38 $attrs = self::strip_empty(
39 [
40 'label' => self::str( $args, 'label', 'Text Field' ),
41 'placeholder' => self::str( $args, 'placeholder' ),
42 'defaultValue' => self::str( $args, 'default_value' ),
43 'required' => self::bool( $args, 'required' ),
44 'help' => self::str( $args, 'help' ),
45 'errorMsg' => self::str( $args, 'error_message' ),
46 'textLength' => self::int_or_null( $args, 'max_length' ),
47 'inputMask' => self::str( $args, 'input_mask', 'none' ),
48 'slug' => self::slug_from_args( $args, 'text-field' ),
49 'block_id' => self::block_id(),
50 ]
51 );
52 return self::serialize_block( 'srfm/input', $attrs );
53 }
54
55 /**
56 * Build a srfm/email block. When `confirm_email` is set, the block's
57 * native confirm-email mode (`isConfirmEmail`) is enabled — a second
58 * "Confirm Email" input on the same field — instead of the caller having
59 * to emit a separate email block.
60 *
61 * @since 2.11.0
62 *
63 * @param array<string,mixed> $args Field args (incl. `confirm_email`,
64 * `confirm_label`).
65 * @return string
66 */
67 public static function email( array $args ) {
68 $attrs = self::strip_empty(
69 [
70 'label' => self::str( $args, 'label', 'Email' ),
71 'placeholder' => self::str( $args, 'placeholder' ),
72 'defaultValue' => self::str( $args, 'default_value' ),
73 'required' => self::bool( $args, 'required' ),
74 'help' => self::str( $args, 'help' ),
75 'errorMsg' => self::str( $args, 'error_message' ),
76 'slug' => self::slug_from_args( $args, 'email' ),
77 'block_id' => self::block_id(),
78 ]
79 );
80 if ( self::bool( $args, 'confirm_email' ) ) {
81 $attrs['isConfirmEmail'] = true;
82 $confirm_label = self::str( $args, 'confirm_label' );
83 if ( '' !== $confirm_label ) {
84 $attrs['confirmLabel'] = $confirm_label;
85 }
86 }
87 return self::serialize_block( 'srfm/email', $attrs );
88 }
89
90 /**
91 * Build a srfm/url block.
92 *
93 * @since 2.11.0
94 *
95 * @param array<string,mixed> $args Field args.
96 * @return string
97 */
98 public static function url( array $args ) {
99 $attrs = self::strip_empty(
100 [
101 'label' => self::str( $args, 'label', 'URL' ),
102 'placeholder' => self::str( $args, 'placeholder' ),
103 'defaultValue' => self::str( $args, 'default_value' ),
104 'required' => self::bool( $args, 'required' ),
105 'help' => self::str( $args, 'help' ),
106 'errorMsg' => self::str( $args, 'error_message' ),
107 'slug' => self::slug_from_args( $args, 'url' ),
108 'block_id' => self::block_id(),
109 ]
110 );
111 return self::serialize_block( 'srfm/url', $attrs );
112 }
113
114 /**
115 * Build a srfm/phone block.
116 *
117 * @since 2.11.0
118 *
119 * @param array<string,mixed> $args Field args.
120 * @return string
121 */
122 public static function phone( array $args ) {
123 $attrs = self::strip_empty(
124 [
125 'label' => self::str( $args, 'label', 'Phone Number' ),
126 'placeholder' => self::str( $args, 'placeholder' ),
127 'required' => self::bool( $args, 'required' ),
128 'help' => self::str( $args, 'help' ),
129 'errorMsg' => self::str( $args, 'error_message' ),
130 'slug' => self::slug_from_args( $args, 'phone' ),
131 'block_id' => self::block_id(),
132 ]
133 );
134 return self::serialize_block( 'srfm/phone', $attrs );
135 }
136
137 /**
138 * Build a srfm/number block. Handles range/slider via min/max args.
139 *
140 * @since 2.11.0
141 *
142 * @param array<string,mixed> $args Field args.
143 * @return string
144 */
145 public static function number( array $args ) {
146 $attrs = self::strip_empty(
147 [
148 'label' => self::str( $args, 'label', 'Number' ),
149 'placeholder' => self::str( $args, 'placeholder' ),
150 'defaultValue' => self::str( $args, 'default_value' ),
151 'required' => self::bool( $args, 'required' ),
152 'help' => self::str( $args, 'help' ),
153 'errorMsg' => self::str( $args, 'error_message' ),
154 'minValue' => self::int_or_null( $args, 'min' ),
155 'maxValue' => self::int_or_null( $args, 'max' ),
156 'prefix' => self::str( $args, 'prefix' ),
157 'suffix' => self::str( $args, 'suffix' ),
158 'slug' => self::slug_from_args( $args, 'number' ),
159 'block_id' => self::block_id(),
160 ]
161 );
162 return self::serialize_block( 'srfm/number', $attrs );
163 }
164
165 /**
166 * Build a srfm/textarea block.
167 *
168 * @since 2.11.0
169 *
170 * @param array<string,mixed> $args Field args.
171 * @return string
172 */
173 public static function textarea( array $args ) {
174 $attrs = self::strip_empty(
175 [
176 'label' => self::str( $args, 'label', 'Textarea' ),
177 'placeholder' => self::str( $args, 'placeholder' ),
178 'defaultValue' => self::str( $args, 'default_value' ),
179 'required' => self::bool( $args, 'required' ),
180 'help' => self::str( $args, 'help' ),
181 'errorMsg' => self::str( $args, 'error_message' ),
182 'minLength' => self::int_or_null( $args, 'min_length' ),
183 'maxLength' => self::int_or_null( $args, 'max_length' ),
184 'rows' => self::int_or_null( $args, 'rows', 4 ),
185 'slug' => self::slug_from_args( $args, 'textarea' ),
186 'block_id' => self::block_id(),
187 ]
188 );
189 return self::serialize_block( 'srfm/textarea', $attrs );
190 }
191
192 /**
193 * Build a srfm/dropdown (single-select or multi-select) block.
194 *
195 * @since 2.11.0
196 *
197 * @param array<string,mixed> $args Field args. Expects `options` => list of strings.
198 * @return string
199 */
200 public static function dropdown( array $args ) {
201 $options = self::format_options( $args, 'label' );
202 $attrs = self::strip_empty(
203 [
204 'label' => self::str( $args, 'label', 'Dropdown' ),
205 'placeholder' => self::str( $args, 'placeholder' ),
206 'required' => self::bool( $args, 'required' ),
207 'multiSelect' => self::bool( $args, 'multiple' ),
208 'help' => self::str( $args, 'help' ),
209 'errorMsg' => self::str( $args, 'error_message' ),
210 'options' => $options,
211 'preselectedOptions' => self::array_or_null( $args, 'preselected' ),
212 'slug' => self::slug_from_args( $args, 'dropdown' ),
213 'block_id' => self::block_id(),
214 ]
215 );
216 return self::serialize_block( 'srfm/dropdown', $attrs );
217 }
218
219 /**
220 * Build a srfm/multi-choice (radio / multiple-choice) block.
221 *
222 * @since 2.11.0
223 *
224 * @param array<string,mixed> $args Field args. Expects `options` => list of strings.
225 * @return string
226 */
227 public static function multi_choice( array $args ) {
228 $options = self::format_options( $args, 'optionTitle' );
229 $attrs = self::strip_empty(
230 [
231 'label' => self::str( $args, 'label', 'Multi Choice' ),
232 'required' => self::bool( $args, 'required' ),
233 'singleSelection' => ! self::bool( $args, 'multiple' ),
234 'help' => self::str( $args, 'help' ),
235 'errorMsg' => self::str( $args, 'error_message' ),
236 'options' => $options,
237 'preselectedOptions' => self::array_or_null( $args, 'preselected' ),
238 'slug' => self::slug_from_args( $args, 'multi-choice' ),
239 'block_id' => self::block_id(),
240 ]
241 );
242 return self::serialize_block( 'srfm/multi-choice', $attrs );
243 }
244
245 /**
246 * Build a srfm/checkbox block.
247 *
248 * @since 2.11.0
249 *
250 * @param array<string,mixed> $args Field args.
251 * @return string
252 */
253 public static function checkbox( array $args ) {
254 $attrs = self::strip_empty(
255 [
256 'label' => self::str( $args, 'label', 'Checkbox' ),
257 'required' => self::bool( $args, 'required' ),
258 'checked' => self::bool( $args, 'checked' ),
259 'help' => self::str( $args, 'help' ),
260 'errorMsg' => self::str( $args, 'error_message' ),
261 'slug' => self::slug_from_args( $args, 'checkbox' ),
262 'block_id' => self::block_id(),
263 ]
264 );
265 return self::serialize_block( 'srfm/checkbox', $attrs );
266 }
267
268 /**
269 * Build a srfm/gdpr block.
270 *
271 * @since 2.11.0
272 *
273 * @param array<string,mixed> $args Field args. Expects `label` (consent text).
274 * @return string
275 */
276 public static function gdpr( array $args ) {
277 $attrs = self::strip_empty(
278 [
279 'label' => self::str( $args, 'label', 'I consent to have this website store my submitted information so they can respond to my inquiry.' ),
280 'checked' => self::bool( $args, 'checked' ),
281 'help' => self::str( $args, 'help' ),
282 'errorMsg' => self::str( $args, 'error_message' ),
283 'slug' => self::slug_from_args( $args, 'consent' ),
284 'block_id' => self::block_id(),
285 ]
286 );
287 return self::serialize_block( 'srfm/gdpr', $attrs );
288 }
289
290 /**
291 * Generate a deterministic Gutenberg-style block id.
292 *
293 * @since 2.11.0
294 *
295 * @return string An 8-character lowercase hex slug.
296 */
297 public static function block_id() {
298 return substr( md5( wp_generate_uuid4() ), 0, 8 );
299 }
300
301 /**
302 * Serialize a self-closing Gutenberg block with the given name and attributes.
303 *
304 * @since 2.11.0
305 *
306 * @param string $name Block name (e.g. 'srfm/input').
307 * @param array<string,mixed> $attrs Block attributes.
308 * @return string Block markup.
309 */
310 private static function serialize_block( $name, array $attrs ) {
311 $json = self::encode_attrs( $attrs );
312 return "<!-- wp:{$name} {$json} /-->\n";
313 }
314
315 /**
316 * JSON-encode block attributes for Gutenberg comment delimiter.
317 *
318 * @since 2.11.0
319 *
320 * @param array<string,mixed> $attrs Attributes to encode.
321 * @return string JSON string, or '{}' if empty.
322 */
323 private static function encode_attrs( array $attrs ) {
324 if ( empty( $attrs ) ) {
325 return '{}';
326 }
327 // JSON_HEX_TAG/AMP/APOS/QUOT escape <, >, &, ', " as < etc. so attacker-controlled
328 // strings (e.g. a CF7 label containing `-->`) cannot terminate the surrounding HTML block
329 // comment delimiter and inject markup. Mirrors what core's serialize_block() does.
330 $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
331 $json = wp_json_encode( $attrs, $flags );
332 return is_string( $json ) ? $json : '{}';
333 }
334
335 /**
336 * Drop keys whose value is null, empty string, or empty array.
337 *
338 * Keeps zero-valued numbers and `false` booleans, which are meaningful.
339 *
340 * @since 2.11.0
341 *
342 * @param array<string,mixed> $arr Attribute array.
343 * @return array<string,mixed>
344 */
345 private static function strip_empty( array $arr ) {
346 return array_filter(
347 $arr,
348 static function ( $v ) {
349 if ( null === $v ) {
350 return false;
351 }
352 if ( '' === $v ) {
353 return false;
354 }
355 if ( is_array( $v ) && empty( $v ) ) {
356 return false;
357 }
358 return true;
359 }
360 );
361 }
362
363 /**
364 * Fetch a string from args with a default.
365 *
366 * @since 2.11.0
367 *
368 * @param array<string,mixed> $args Args.
369 * @param string $key Key to fetch.
370 * @param string $default Default if missing/empty.
371 * @return string
372 */
373 private static function str( array $args, $key, $default = '' ) {
374 if ( ! isset( $args[ $key ] ) ) {
375 return $default;
376 }
377 $v = $args[ $key ];
378 if ( ! is_string( $v ) && ! is_numeric( $v ) ) {
379 return $default;
380 }
381 return (string) $v;
382 }
383
384 /**
385 * Fetch a boolean from args.
386 *
387 * @since 2.11.0
388 *
389 * @param array<string,mixed> $args Args.
390 * @param string $key Key.
391 * @return bool
392 */
393 private static function bool( array $args, $key ) {
394 return ! empty( $args[ $key ] );
395 }
396
397 /**
398 * Fetch an integer or return null if missing.
399 *
400 * @since 2.11.0
401 *
402 * @param array<string,mixed> $args Args.
403 * @param string $key Key.
404 * @param int|null $default Default if missing.
405 * @return int|null
406 */
407 private static function int_or_null( array $args, $key, $default = null ) {
408 if ( ! isset( $args[ $key ] ) || '' === $args[ $key ] ) {
409 return $default;
410 }
411 if ( ! is_numeric( $args[ $key ] ) ) {
412 return $default;
413 }
414 return (int) $args[ $key ];
415 }
416
417 /**
418 * Fetch an array or return null if missing.
419 *
420 * @since 2.11.0
421 *
422 * @param array<string,mixed> $args Args.
423 * @param string $key Key.
424 * @return array<int,mixed>|null
425 */
426 private static function array_or_null( array $args, $key ) {
427 if ( ! isset( $args[ $key ] ) || ! is_array( $args[ $key ] ) || empty( $args[ $key ] ) ) {
428 return null;
429 }
430 return array_values( $args[ $key ] );
431 }
432
433 /**
434 * Convert a label string to a URL-safe slug, lowercased.
435 *
436 * @since 2.11.0
437 *
438 * @param string $label Source label.
439 * @return string Slug — falls back to 'field' if empty.
440 */
441 private static function slugify( $label ) {
442 $slug = sanitize_title( $label );
443 return $slug ? $slug : 'field';
444 }
445
446 /**
447 * Resolve the block slug from `$args`, honoring an explicit `slug` override
448 * (used by importers that need to dedupe collisions across the form).
449 *
450 * @since 2.11.0
451 *
452 * @param array<string,mixed> $args Block args.
453 * @param string $fallback Slug fallback when label is empty.
454 * @return string
455 */
456 private static function slug_from_args( array $args, $fallback ) {
457 $override = self::str( $args, 'slug' );
458 if ( '' !== $override ) {
459 return $override;
460 }
461 return self::slugify( self::str( $args, 'label', $fallback ) );
462 }
463
464 /**
465 * Format a list of source-plugin choices into SureForms dropdown/multi-choice
466 * option shape.
467 *
468 * @since 2.11.0
469 *
470 * @param array<string,mixed> $args Field args containing 'options'.
471 * @param string $label_key Either 'label' (dropdown) or 'optionTitle' (multi-choice).
472 * @return array<int,array<string,string>>
473 */
474 private static function format_options( array $args, $label_key ) {
475 if ( empty( $args['options'] ) || ! is_array( $args['options'] ) ) {
476 return [
477 [ $label_key => 'Option 1' ],
478 ];
479 }
480 $out = [];
481 foreach ( $args['options'] as $opt ) {
482 if ( is_string( $opt ) ) {
483 $out[] = [ $label_key => $opt ];
484 continue;
485 }
486 if ( is_array( $opt ) && isset( $opt['label'] ) ) {
487 $out[] = [ $label_key => (string) $opt['label'] ];
488 }
489 }
490 return empty( $out ) ? [ [ $label_key => 'Option 1' ] ] : $out;
491 }
492 }
493