| @@ -79,8 +79,12 @@ | ||
| 79 | 79 | 'credit_card' => array( |
| 80 | 80 | 'name' => __( 'Payment', 'formidable' ), |
| 81 | 81 | 'icon' => 'frmfont frm_credit_card2_icon', |
| 82 | 82 | ), |
| 83 | + 'address' => array( | |
| 84 | + 'name' => __( 'Address', 'formidable' ), | |
| 85 | + 'icon' => 'frmfont frm_location2_icon', | |
| 86 | + ), | |
| 83 | 87 | FrmSubmitHelper::FIELD_TYPE => array( |
| 84 | 88 | 'name' => __( 'Submit', 'formidable' ), |
| 85 | 89 | 'hide' => true, |
| 86 | 90 | ), |
| @@ -88,8 +92,23 @@ | ||
| 88 | 92 | 'name' => __( 'GDPR', 'formidable' ), |
| 89 | 93 | 'icon' => 'frmfont frm-gdpr-icon', |
| 90 | 94 | 'hide' => FrmFieldGdprHelper::hide_gdpr_field(), |
| 91 | 95 | ), |
| 96 | + 'product' => array( | |
| 97 | + 'name' => __( 'Product', 'formidable' ), | |
| 98 | + 'icon' => 'frmfont frm_product2_icon', | |
| 99 | + 'section' => 'pricing', | |
| 100 | + ), | |
| 101 | + 'quantity' => array( | |
| 102 | + 'name' => __( 'Quantity', 'formidable' ), | |
| 103 | + 'icon' => 'frmfont frm_quantity_icon', | |
| 104 | + 'section' => 'pricing', | |
| 105 | + ), | |
| 106 | + 'total' => array( | |
| 107 | + 'name' => __( 'Total', 'formidable' ), | |
| 108 | + 'icon' => 'frmfont frm_total2_icon', | |
| 109 | + 'section' => 'pricing', | |
| 110 | + ), | |
| 92 | 111 | ); |
| 93 | 112 | |
| 94 | 113 | /** |
| 95 | 114 | * @param array $fields |
| @@ -301,8 +320,16 @@ | ||
| 301 | 320 | 'link' => 'https://simplyscheduleappointments.com/meet/formidable/', |
| 302 | 321 | 'upsell_image' => $upsell_images_url . 'appointment-field-preview.webp', |
| 303 | 322 | 'learn-more' => 'simply-schedule-appointments-forms', |
| 304 | 323 | ), |
| 324 | + 'virtual' => array( | |
| 325 | + 'name' => __( 'Virtual', 'formidable' ), | |
| 326 | + 'icon' => 'frmfont frm-virtual-field-icon', | |
| 327 | + 'message' => esc_html__( 'Protect sensitive data by storing field values server-side only, preventing users from viewing or manipulating them in their browser.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong | |
| 328 | + 'upsell_image' => $upsell_images_url . 'virtual-field-preview.webp', | |
| 329 | + 'learn-more' => '/virtual', | |
| 330 | + 'is_new' => self::field_is_new( 'virtual' ), | |
| 331 | + ), | |
| 305 | 332 | 'product' => array( |
| 306 | 333 | 'name' => __( 'Product', 'formidable' ), |
| 307 | 334 | 'icon' => 'frmfont frm_product2_icon', |
| 308 | 335 | 'section' => 'pricing', |
| @@ -346,8 +373,41 @@ | ||
| 346 | 373 | return apply_filters( 'frm_pro_available_fields', $fields ); |
| 347 | 374 | } |
| 348 | 375 | |
| 349 | 376 | /** |
| 377 | + * Flag Pro field types that require a newer Pro version with frm_show_update. | |
| 378 | + * | |
| 379 | + * @since 6.29 | |
| 380 | + * | |
| 381 | + * @param array $fields Available Pro field types. | |
| 382 | + * | |
| 383 | + * @return array | |
| 384 | + */ | |
| 385 | + public static function show_update_for_pro_fields( $fields ) { | |
| 386 | + if ( FrmAppHelper::pro_is_installed() && ! class_exists( 'FrmProVirtualFieldController', false ) ) { | |
| 387 | + $fields['virtual']['icon'] .= ' frm_show_update'; | |
| 388 | + } | |
| 389 | + | |
| 390 | + return $fields; | |
| 391 | + } | |
| 392 | + | |
| 393 | + /** | |
| 394 | + * Gets field section labels in the form builder. | |
| 395 | + * | |
| 396 | + * @since 6.30 | |
| 397 | + * | |
| 398 | + * @return array | |
| 399 | + */ | |
| 400 | + public static function field_section_labels() { | |
| 401 | + return apply_filters( | |
| 402 | + 'frm_available_field_sections', | |
| 403 | + array( | |
| 404 | + 'pricing' => __( 'Pricing Fields', 'formidable' ), | |
| 405 | + ) | |
| 406 | + ); | |
| 407 | + } | |
| 408 | + | |
| 409 | + /** | |
| 350 | 410 | * Consider a field new for 90 days after the release date. |
| 351 | 411 | * |
| 352 | 412 | * @since 6.8.3 |
| 353 | 413 | * |
| @@ -356,9 +416,9 @@ | ||
| 356 | 416 | * @return bool |
| 357 | 417 | */ |
| 358 | 418 | private static function field_is_new( $type ) { |
| 359 | 419 | $release_dates = array( |
| 360 | - 'coupon' => '2026-01-13', | |
| 420 | + 'virtual' => '2026-03-10', | |
| 361 | 421 | ); |
| 362 | 422 | |
| 363 | 423 | if ( ! isset( $release_dates[ $type ] ) ) { |
| 364 | 424 | return false; |
| @@ -369,15 +429,31 @@ | ||
| 369 | 429 | return gmdate( 'Y-m-d' ) < $three_months_after_release; |
| 370 | 430 | } |
| 371 | 431 | |
| 372 | 432 | /** |
| 433 | + * Remove field types that are moved from Pro to Lite. | |
| 434 | + * | |
| 435 | + * @since 6.30 | |
| 436 | + * | |
| 437 | + * @param array $pro_fields | |
| 438 | + * | |
| 439 | + * @return void | |
| 440 | + */ | |
| 441 | + public static function remove_moved_field_types_from_pro( &$pro_fields ) { | |
| 442 | + unset( $pro_fields['credit_card'] ); | |
| 443 | + unset( $pro_fields['product'] ); | |
| 444 | + unset( $pro_fields['quantity'] ); | |
| 445 | + unset( $pro_fields['total'] ); | |
| 446 | + unset( $pro_fields['address'] ); | |
| 447 | + } | |
| 448 | + | |
| 449 | + /** | |
| 373 | 450 | * @since 4.0 |
| 374 | 451 | * |
| 375 | 452 | * @return array |
| 376 | 453 | */ |
| 377 | 454 | public static function all_field_selection() { |
| 378 | - $pro_field_selection = self::pro_field_selection(); | |
| 379 | - return array_merge( $pro_field_selection, self::field_selection() ); | |
| 455 | + return array_merge( self::pro_field_selection(), self::field_selection() ); | |
| 380 | 456 | } |
| 381 | 457 | |
| 382 | 458 | /** |
| 383 | 459 | * Create a field. |
| @@ -401,13 +477,19 @@ | ||
| 401 | 477 | $new_values[ $col ] = $values[ $col ]; |
| 402 | 478 | } |
| 403 | 479 | } |
| 404 | 480 | |
| 405 | - $new_values['options'] = self::maybe_filter_options( $values['options'] ); | |
| 481 | + // Both option columns are stored serialized, but they do not accept the same shapes. | |
| 482 | + // Choice fields pass options already serialized, as does FrmFieldsHelper::fill_field | |
| 483 | + // when a field is duplicated, and fields with no choices pass an empty string, so a | |
| 484 | + // missing key is the only thing worth defaulting. An is_array check here would throw | |
| 485 | + // every one of those defaults away. field_options is always an array, so anything | |
| 486 | + // else is invalid input, and replacing it stops consumers getting null. | |
| 487 | + $new_values['options'] = self::maybe_filter_options( $values['options'] ?? array() ); | |
| 406 | 488 | $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null; |
| 407 | 489 | $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0; |
| 408 | 490 | $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null; |
| 409 | - $new_values['field_options'] = $values['field_options']; | |
| 491 | + $new_values['field_options'] = isset( $values['field_options'] ) && is_array( $values['field_options'] ) ? $values['field_options'] : array(); | |
| 410 | 492 | $new_values['created_at'] = current_time( 'mysql', 1 ); |
| 411 | 493 | |
| 412 | 494 | if ( isset( $values['id'] ) ) { |
| 413 | 495 | $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key']; |
| @@ -450,9 +532,9 @@ | ||
| 450 | 532 | * See FrmAppHelper::allow_unfiltered_html. |
| 451 | 533 | * |
| 452 | 534 | * @since 5.0.08 |
| 453 | 535 | * |
| 454 | - * @param array $options | |
| 536 | + * @param array|string $options Already serialized for choice fields, and an empty string for fields with no choices. | |
| 455 | 537 | * |
| 456 | 538 | * @return array |
| 457 | 539 | */ |
| 458 | 540 | private static function maybe_filter_options( $options ) { |
| @@ -502,13 +584,9 @@ | ||
| 502 | 584 | |
| 503 | 585 | $safe_atts = array(); |
| 504 | 586 | |
| 505 | 587 | foreach ( $attr as $attr_key => $att ) { |
| 506 | - if ( ! is_numeric( $attr_key ) ) { | |
| 507 | - // opt=1 without parentheses for example is mapped like 'opt' => 1. | |
| 508 | - $key = $attr_key; | |
| 509 | - $value = $att; | |
| 510 | - } else { | |
| 588 | + if ( is_numeric( $attr_key ) ) { | |
| 511 | 589 | // Some data is mapped like 0 => 'placeholder="Placeholder"'. |
| 512 | 590 | $split = explode( '=', $att, 2 ); |
| 513 | 591 | |
| 514 | 592 | if ( 2 !== count( $split ) ) { |
| @@ -516,8 +594,12 @@ | ||
| 516 | 594 | } |
| 517 | 595 | |
| 518 | 596 | $key = trim( $split[0] ); |
| 519 | 597 | $value = trim( $split[1], '"' ); |
| 598 | + } else { | |
| 599 | + // opt=1 without parentheses for example is mapped like 'opt' => 1. | |
| 600 | + $key = $attr_key; | |
| 601 | + $value = $att; | |
| 520 | 602 | } |
| 521 | 603 | |
| 522 | 604 | if ( FrmAppHelper::input_key_is_safe( $key, 'update' ) ) { |
| 523 | 605 | $safe_atts[ $key ] = $value; |
| @@ -689,15 +771,17 @@ | ||
| 689 | 771 | } |
| 690 | 772 | |
| 691 | 773 | // Serialize array values |
| 692 | 774 | foreach ( array( 'field_options', 'options' ) as $opt ) { |
| 693 | - if ( isset( $values[ $opt ] ) && is_array( $values[ $opt ] ) ) { | |
| 694 | - if ( 'field_options' === $opt ) { | |
| 695 | - $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] ); | |
| 696 | - } | |
| 775 | + if ( ! isset( $values[ $opt ] ) || ! is_array( $values[ $opt ] ) ) { | |
| 776 | + continue; | |
| 777 | + } | |
| 697 | 778 | |
| 698 | - $values[ $opt ] = serialize( $values[ $opt ] ); | |
| 779 | + if ( 'field_options' === $opt ) { | |
| 780 | + $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] ); | |
| 699 | 781 | } |
| 782 | + | |
| 783 | + $values[ $opt ] = serialize( $values[ $opt ] ); | |
| 700 | 784 | } |
| 701 | 785 | |
| 702 | 786 | if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) { |
| 703 | 787 | $values['default_value'] = json_encode( $values['default_value'] ); |
| @@ -1000,17 +1084,19 @@ | ||
| 1000 | 1084 | * |
| 1001 | 1085 | * @return void |
| 1002 | 1086 | */ |
| 1003 | 1087 | private static function maybe_include_repeating_fields( $inc_repeat, &$where ) { |
| 1004 | - if ( $inc_repeat === 'include' ) { | |
| 1005 | - $form_id = $where['fi.form_id']; | |
| 1006 | - $where[] = array( | |
| 1007 | - 'or' => 1, | |
| 1008 | - 'fi.form_id' => $form_id, | |
| 1009 | - 'fr.parent_form_id' => $form_id, | |
| 1010 | - ); | |
| 1011 | - unset( $where['fi.form_id'] ); | |
| 1088 | + if ( $inc_repeat !== 'include' ) { | |
| 1089 | + return; | |
| 1012 | 1090 | } |
| 1091 | + | |
| 1092 | + $form_id = $where['fi.form_id']; | |
| 1093 | + $where[] = array( | |
| 1094 | + 'or' => 1, | |
| 1095 | + 'fi.form_id' => $form_id, | |
| 1096 | + 'fr.parent_form_id' => $form_id, | |
| 1097 | + ); | |
| 1098 | + unset( $where['fi.form_id'] ); | |
| 1013 | 1099 | } |
| 1014 | 1100 | |
| 1015 | 1101 | /** |
| 1016 | 1102 | * @param array $results |
| @@ -1351,11 +1437,9 @@ | ||
| 1351 | 1437 | * |
| 1352 | 1438 | * @return bool |
| 1353 | 1439 | */ |
| 1354 | 1440 | public static function is_multiple_select( $field ) { |
| 1355 | - $field_type = self::get_field_type( $field ); | |
| 1356 | - $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && $field_type !== 'hidden'; | |
| 1357 | - | |
| 1441 | + $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && self::get_field_type( $field ) !== 'hidden'; | |
| 1358 | 1442 | return apply_filters( 'frm_is_multiple_select', $is_multiple, $field ); |
| 1359 | 1443 | } |
| 1360 | 1444 | |
| 1361 | 1445 | /** |