PluginProbe
The Innovative Form Builder – IvyForms / 1.4
The Innovative Form Builder – IvyForms v1.4
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Submission / SubmissionPayloadParser.php

SubmissionPayloadParser.php in The Innovative Form Builder – IvyForms 1.4, at backend/src/Services/Submission/SubmissionPayloadParser.php

195 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 declare(strict_types=1);
9
10 namespace IvyForms\Services\Submission;
11
12 use IvyForms\Common\Exceptions\InvalidArgumentException;
13 use IvyForms\Services\Translations\BackendStrings;
14 use WP_REST_Request;
15
16 /**
17 * Normalizes JSON or multipart (form POST) submission payloads.
18 */
19 final class SubmissionPayloadParser
20 {
21 /**
22 * @param WP_REST_Request<array<string, mixed>> $request
23 *
24 * @return array<string, mixed>
25 */
26 public static function parseRequestPayload(WP_REST_Request $request): array
27 {
28 $json = $request->get_json_params();
29 if (is_array($json)) {
30 $resolved = self::resolveStructuredPayload($json, false);
31 if ($resolved !== null) {
32 return $resolved;
33 }
34 }
35
36 $params = $request->get_params();
37 if (is_array($params)) {
38 $resolved = self::resolveStructuredPayload($params, true);
39 if ($resolved !== null) {
40 return $resolved;
41 }
42 }
43
44 return self::parseMultipartSubmissionPayload($request);
45 }
46
47 /**
48 * Wrap flat field keys (public API / OpenAPI shape) into embed submission shape.
49 *
50 * @param array<string, mixed> $body
51 *
52 * @return array<string, mixed>
53 */
54 public static function normalizeFlatPayload(array $body): array
55 {
56 $params = self::extractSubmissionScalars($body);
57
58 if (isset($body['values']) && is_array($body['values'])) {
59 $params['values'] = $body['values'];
60
61 return $params;
62 }
63
64 $values = [];
65 $reservedKeys = ['formId', 'nonce', 'postId', 'referer', 'ivyforms_fm', 'values'];
66 foreach ($body as $key => $value) {
67 if (!in_array($key, $reservedKeys, true)) {
68 $values[$key] = $value;
69 }
70 }
71 $params['values'] = $values;
72
73 if (isset($body['ivyforms_fm'])) {
74 $params['ivyforms_fm'] = is_string($body['ivyforms_fm']) ? wp_unslash($body['ivyforms_fm']) : '';
75 }
76
77 return $params;
78 }
79
80 /**
81 * @param array<string, mixed> $payload
82 *
83 * @return array<string, mixed>|null
84 */
85 private static function resolveStructuredPayload(array $payload, bool $requireValuesArray): ?array
86 {
87 if (array_key_exists('values', $payload)) {
88 if (!$requireValuesArray || is_array($payload['values'])) {
89 return $payload;
90 }
91 }
92
93 if ($payload === []) {
94 return null;
95 }
96
97 $normalized = self::normalizeFlatPayload($payload);
98
99 return $normalized['values'] !== [] ? $normalized : null;
100 }
101
102 /**
103 * @param WP_REST_Request<array<string, mixed>> $request
104 *
105 * @return array<string, mixed>
106 */
107 private static function parseMultipartSubmissionPayload(WP_REST_Request $request): array
108 {
109 $body = $request->get_body_params();
110 if (!is_array($body)) {
111 $body = [];
112 }
113
114 $params = self::extractSubmissionScalars($body);
115 $params['values'] = self::decodeSubmissionValues($body);
116
117 if (isset($body['ivyforms_fm'])) {
118 $params['ivyforms_fm'] = is_string($body['ivyforms_fm']) ? wp_unslash($body['ivyforms_fm']) : '';
119 }
120
121 return $params;
122 }
123
124 /**
125 * @param array<string, mixed> $body
126 *
127 * @return array<string, mixed>
128 */
129 private static function extractSubmissionScalars(array $body): array
130 {
131 $params = [];
132 foreach (['formId', 'nonce', 'postId', 'referer'] as $key) {
133 if (!array_key_exists($key, $body)) {
134 continue;
135 }
136
137 $value = $body[$key];
138
139 if ($key === 'nonce' || $key === 'referer') {
140 if (!is_scalar($value)) {
141 throw new InvalidArgumentException(
142 BackendStrings::getExceptionStrings()['invalid_request_data']
143 );
144 }
145
146 $params[$key] = wp_unslash((string) $value);
147 continue;
148 }
149
150 if (!is_scalar($value)) {
151 $params[$key] = 0;
152 continue;
153 }
154
155 $unslashed = wp_unslash($value);
156 $params[$key] = is_numeric($unslashed) ? (int) $unslashed : 0;
157 }
158
159 return $params;
160 }
161
162 /**
163 * @param array<string, mixed> $body
164 *
165 * @return array<string, mixed>
166 */
167 private static function decodeSubmissionValues(array $body): array
168 {
169 if (!isset($body['values'])) {
170 return [];
171 }
172
173 if (is_string($body['values'])) {
174 // WP_REST_Server already runs wp_unslash() on $_POST before populating body params,
175 // so the multipart `values` JSON arrives unslashed. Decode it as-is: unslashing again
176 // strips the backslashes that escape quotes inside rich-text HTML, which corrupts the
177 // JSON and makes json_decode() return null (dropping every field except uploaded files).
178 // The wp_unslash() fallback only covers environments that hand back a slashed body.
179 $raw = $body['values'];
180 $decoded = json_decode($raw, true);
181 if (!is_array($decoded)) {
182 $decoded = json_decode(wp_unslash($raw), true);
183 }
184
185 return is_array($decoded) ? $decoded : [];
186 }
187
188 if (is_array($body['values'])) {
189 return $body['values'];
190 }
191
192 return [];
193 }
194 }
195