PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
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.5, at backend/src/Services/Submission/SubmissionPayloadParser.php

230 lines 6.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 private const SCALAR_TYPE_STRING = 'string';
22
23 private const SCALAR_TYPE_INT = 'int';
24
25 /**
26 * Top-level submission keys (everything outside `values`) and how each is cast.
27 *
28 * @var array<string, string>
29 */
30 private const SCALAR_KEY_TYPES = [
31 'formId' => self::SCALAR_TYPE_INT,
32 'nonce' => self::SCALAR_TYPE_STRING,
33 'postId' => self::SCALAR_TYPE_INT,
34 'referer' => self::SCALAR_TYPE_STRING,
35 'sourceId' => self::SCALAR_TYPE_STRING,
36 'paymentMethodId' => self::SCALAR_TYPE_STRING,
37 'paymentIntentId' => self::SCALAR_TYPE_STRING,
38 'paypalOrderId' => self::SCALAR_TYPE_STRING,
39 'idempotencyKey' => self::SCALAR_TYPE_STRING,
40 ];
41
42 /**
43 * @param WP_REST_Request<array<string, mixed>> $request
44 *
45 * @return array<string, mixed>
46 */
47 public static function parseRequestPayload(WP_REST_Request $request): array
48 {
49 $json = $request->get_json_params();
50 if (is_array($json)) {
51 $resolved = self::resolveStructuredPayload($json, false);
52 if ($resolved !== null) {
53 return $resolved;
54 }
55 }
56
57 $params = $request->get_params();
58 if (is_array($params)) {
59 $resolved = self::resolveStructuredPayload($params, true);
60 if ($resolved !== null) {
61 return $resolved;
62 }
63 }
64
65 return self::parseMultipartSubmissionPayload($request);
66 }
67
68 /**
69 * Wrap flat field keys (public API / OpenAPI shape) into embed submission shape.
70 *
71 * @param array<string, mixed> $body
72 *
73 * @return array<string, mixed>
74 */
75 public static function normalizeFlatPayload(array $body): array
76 {
77 $params = self::extractSubmissionScalars($body);
78
79 if (isset($body['values']) && is_array($body['values'])) {
80 $params['values'] = $body['values'];
81
82 return $params;
83 }
84
85 $values = [];
86 $reservedKeys = array_merge(
87 array_keys(self::SCALAR_KEY_TYPES),
88 ['ivyforms_fm', 'values']
89 );
90 foreach ($body as $key => $value) {
91 if (!in_array($key, $reservedKeys, true)) {
92 $values[$key] = $value;
93 }
94 }
95 $params['values'] = $values;
96
97 if (isset($body['ivyforms_fm'])) {
98 $params['ivyforms_fm'] = is_string($body['ivyforms_fm']) ? wp_unslash($body['ivyforms_fm']) : '';
99 }
100
101 return $params;
102 }
103
104 /**
105 * @param array<string, mixed> $payload
106 *
107 * @return array<string, mixed>|null
108 */
109 private static function resolveStructuredPayload(array $payload, bool $requireValuesArray): ?array
110 {
111 if (array_key_exists('values', $payload)) {
112 if (!$requireValuesArray || is_array($payload['values'])) {
113 return $payload;
114 }
115 }
116
117 if ($payload === []) {
118 return null;
119 }
120
121 $normalized = self::normalizeFlatPayload($payload);
122
123 return $normalized['values'] !== [] ? $normalized : null;
124 }
125
126 /**
127 * @param WP_REST_Request<array<string, mixed>> $request
128 *
129 * @return array<string, mixed>
130 */
131 private static function parseMultipartSubmissionPayload(WP_REST_Request $request): array
132 {
133 $body = $request->get_body_params();
134 if (!is_array($body)) {
135 $body = [];
136 }
137
138 $params = self::extractSubmissionScalars($body);
139 $params['values'] = self::decodeSubmissionValues($body);
140
141 if (isset($body['ivyforms_fm'])) {
142 $params['ivyforms_fm'] = is_string($body['ivyforms_fm']) ? wp_unslash($body['ivyforms_fm']) : '';
143 }
144
145 return $params;
146 }
147
148 /**
149 * @param array<string, mixed> $body
150 *
151 * @return array<string, mixed>
152 */
153 private static function extractSubmissionScalars(array $body): array
154 {
155 $params = [];
156 foreach (self::SCALAR_KEY_TYPES as $key => $type) {
157 if (!array_key_exists($key, $body)) {
158 continue;
159 }
160
161 $params[$key] = $type === self::SCALAR_TYPE_STRING
162 ? self::extractStringScalar($body[$key])
163 : self::extractIntScalar($body[$key]);
164 }
165
166 return $params;
167 }
168
169 /**
170 * @param mixed $value
171 */
172 private static function extractStringScalar($value): string
173 {
174 if (!is_scalar($value)) {
175 throw new InvalidArgumentException(
176 BackendStrings::getExceptionStrings()['invalid_request_data']
177 );
178 }
179
180 return wp_unslash((string) $value);
181 }
182
183 /**
184 * @param mixed $value
185 */
186 private static function extractIntScalar($value): int
187 {
188 if (!is_scalar($value)) {
189 return 0;
190 }
191
192 $unslashed = wp_unslash($value);
193
194 return is_numeric($unslashed) ? (int) $unslashed : 0;
195 }
196
197 /**
198 * @param array<string, mixed> $body
199 *
200 * @return array<string, mixed>
201 */
202 private static function decodeSubmissionValues(array $body): array
203 {
204 if (!isset($body['values'])) {
205 return [];
206 }
207
208 if (is_string($body['values'])) {
209 // WP_REST_Server already runs wp_unslash() on $_POST before populating body params,
210 // so the multipart `values` JSON arrives unslashed. Decode it as-is: unslashing again
211 // strips the backslashes that escape quotes inside rich-text HTML, which corrupts the
212 // JSON and makes json_decode() return null (dropping every field except uploaded files).
213 // The wp_unslash() fallback only covers environments that hand back a slashed body.
214 $raw = $body['values'];
215 $decoded = json_decode($raw, true);
216 if (!is_array($decoded)) {
217 $decoded = json_decode(wp_unslash($raw), true);
218 }
219
220 return is_array($decoded) ? $decoded : [];
221 }
222
223 if (is_array($body['values'])) {
224 return $body['values'];
225 }
226
227 return [];
228 }
229 }
230