| 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 |
|