| @@ -3,8 +3,9 @@ | ||
| 3 | 3 | namespace FluentForm\App\Services\Transfer; |
| 4 | 4 | |
| 5 | 5 | defined('ABSPATH') or die; |
| 6 | 6 | |
| 7 | +use FluentForm\App\Services\FormBuilder\AutocompleteTokens; | |
| 7 | 8 | use Exception; |
| 8 | 9 | use FluentForm\App\Helpers\Helper; |
| 9 | 10 | use FluentForm\App\Models\Form; |
| 10 | 11 | use FluentForm\App\Models\FormMeta; |
| @@ -50,8 +51,9 @@ | ||
| 50 | 51 | if (is_array($node)) { |
| 51 | 52 | foreach ($node as $key => &$value) { |
| 52 | 53 | if ('attributes' === $key) { |
| 53 | 54 | $value = self::dropEventHandlerAttributeKeys($value); |
| 55 | + $value = self::sanitizeFieldAttributes($value); | |
| 54 | 56 | } |
| 55 | 57 | $value = self::sanitizeAttributeControlSetting($key, $value); |
| 56 | 58 | self::sanitizeJsonNode($value); |
| 57 | 59 | } |
| @@ -59,8 +61,9 @@ | ||
| 59 | 61 | } elseif (is_object($node)) { |
| 60 | 62 | foreach (get_object_vars($node) as $key => $value) { |
| 61 | 63 | if ('attributes' === $key) { |
| 62 | 64 | $value = self::dropEventHandlerAttributeKeys($value); |
| 65 | + $value = self::sanitizeFieldAttributes($value); | |
| 63 | 66 | } |
| 64 | 67 | $value = self::sanitizeAttributeControlSetting($key, $value); |
| 65 | 68 | self::sanitizeJsonNode($value); |
| 66 | 69 | $node->{$key} = $value; |
| @@ -69,8 +72,22 @@ | ||
| 69 | 72 | $node = wp_kses_post($node); |
| 70 | 73 | } |
| 71 | 74 | } |
| 72 | 75 | |
| 76 | + // Scoped to a field's own attributes: sanitizeJsonNode() walks the whole meta | |
| 77 | + // tree, so matching on key name alone would rewrite any unrelated property | |
| 78 | + // that happens to be called autocomplete. | |
| 79 | + private static function sanitizeFieldAttributes($attributes) | |
| 80 | + { | |
| 81 | + if (is_object($attributes) && property_exists($attributes, 'autocomplete')) { | |
| 82 | + $attributes->autocomplete = AutocompleteTokens::sanitize($attributes->autocomplete); | |
| 83 | + } elseif (is_array($attributes) && array_key_exists('autocomplete', $attributes)) { | |
| 84 | + $attributes['autocomplete'] = AutocompleteTokens::sanitize($attributes['autocomplete']); | |
| 85 | + } | |
| 86 | + | |
| 87 | + return $attributes; | |
| 88 | + } | |
| 89 | + | |
| 73 | 90 | private static function sanitizeAttributeControlSetting($key, $value) |
| 74 | 91 | { |
| 75 | 92 | if ('max_repeat_field' === $key) { |
| 76 | 93 | return is_scalar($value) && '' !== trim((string) $value) ? absint($value) : ''; |
| @@ -360,8 +377,9 @@ | ||
| 360 | 377 | $type = sanitize_key(Arr::get($args, 'format', 'csv')); |
| 361 | 378 | if (!in_array($type, ['csv', 'ods', 'xlsx', 'json'])) { |
| 362 | 379 | exit('Invalid requested format'); |
| 363 | 380 | } |
| 381 | + self::markDownloadStarted(Arr::get($args, 'download_token')); | |
| 364 | 382 | if ('json' == $type) { |
| 365 | 383 | self::exportAsJSON($form, $args); |
| 366 | 384 | } |
| 367 | 385 | if (!defined('FLUENTFORM_DOING_CSV_EXPORT')) { |
| @@ -674,8 +692,30 @@ | ||
| 674 | 692 | $sanitizedTitle = 'export'; |
| 675 | 693 | } |
| 676 | 694 | |
| 677 | 695 | return $sanitizedTitle . '-' . date('Y-m-d'); |
| 696 | + } | |
| 697 | + | |
| 698 | + /** | |
| 699 | + * Set a short-lived cookie the admin page polls to know the download has started. | |
| 700 | + * | |
| 701 | + * @param string|null $token | |
| 702 | + * @return void | |
| 703 | + */ | |
| 704 | + private static function markDownloadStarted($token) | |
| 705 | + { | |
| 706 | + $token = substr(sanitize_key((string) $token), 0, 32); | |
| 707 | + | |
| 708 | + if (!$token || headers_sent()) { | |
| 709 | + return; | |
| 710 | + } | |
| 711 | + | |
| 712 | + setcookie('ff_export_' . $token, '1', [ | |
| 713 | + 'expires' => time() + 60, | |
| 714 | + 'path' => '/', | |
| 715 | + 'secure' => is_ssl(), | |
| 716 | + 'samesite' => 'Lax', | |
| 717 | + ]); | |
| 678 | 718 | } |
| 679 | 719 | |
| 680 | 720 | private static function sendDownloadHeaders($contentType, $fileName) |
| 681 | 721 | { |