PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
← All changes | includes/sdk/s3/Aws/Api/Serializer/RestSerializer.php +221 -75 1.2.13 → 1.4.2 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace Dudlewebs\WPMCS\s3\Aws\Api\Serializer;
4 4
5 +use Dudlewebs\WPMCS\s3\Aws\Api\ListShape;
5 6 use Dudlewebs\WPMCS\s3\Aws\Api\MapShape;
6 7 use Dudlewebs\WPMCS\s3\Aws\Api\Service;
7 8 use Dudlewebs\WPMCS\s3\Aws\Api\Operation;
8 9 use Dudlewebs\WPMCS\s3\Aws\Api\Shape;
@@ -8,16 +9,17 @@
8 9 use Dudlewebs\WPMCS\s3\Aws\Api\Shape;
9 10 use Dudlewebs\WPMCS\s3\Aws\Api\StructureShape;
10 11 use Dudlewebs\WPMCS\s3\Aws\Api\TimestampShape;
11 12 use Dudlewebs\WPMCS\s3\Aws\CommandInterface;
12 -use Dudlewebs\WPMCS\s3\Aws\EndpointV2\EndpointProviderV2;
13 13 use Dudlewebs\WPMCS\s3\Aws\EndpointV2\EndpointV2SerializerTrait;
14 14 use Dudlewebs\WPMCS\s3\Aws\EndpointV2\Ruleset\RulesetEndpoint;
15 +use DateTimeInterface;
15 16 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7;
16 17 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Request;
17 18 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Uri;
18 19 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\UriResolver;
19 20 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
21 +use Dudlewebs\WPMCS\s3\Psr\Http\Message\UriInterface;
20 22 /**
21 23 * Serializes HTTP locations like header, uri, payload, etc...
22 24 * @internal
23 25 */
@@ -22,16 +24,20 @@
22 24 * @internal
23 25 */
24 26 abstract class RestSerializer
25 27 {
26 - use EndpointV2SerializerTrait;
28 + private const TEMPLATE_STRING_REGEX = '/\\{([^\\}]+)\\}/';
29 + private static array $excludeContentType = ['s3' => \true, 'glacier' => \true];
27 30 /** @var Service */
28 - private $api;
31 + private Service $api;
29 32 /** @var Uri */
30 33 private $endpoint;
34 + /** @var bool */
35 + private $isUseEndpointV2;
36 + use EndpointV2SerializerTrait;
31 37 /**
32 - * @param Service $api Service API description
33 - * @param string $endpoint Endpoint to connect to
38 + * @param Service $api Service API description
39 + * @param string $endpoint Endpoint to connect to
34 40 */
35 41 public function __construct(Service $api, $endpoint)
36 42 {
37 43 $this->api = $api;
@@ -38,31 +44,30 @@
38 44 $this->endpoint = Psr7\Utils::uriFor($endpoint);
39 45 }
40 46 /**
41 47 * @param CommandInterface $command Command to serialize into a request.
42 - * @param $endpointProvider Provider used for dynamic endpoint resolution.
43 - * @param $clientArgs Client arguments used for dynamic endpoint resolution.
44 - *
48 + * @param mixed|null $endpoint
45 49 * @return RequestInterface
46 50 */
47 - public function __invoke(CommandInterface $command, $endpoint = null)
51 + public function __invoke(CommandInterface $command, mixed $endpoint = null)
48 52 {
49 53 $operation = $this->api->getOperation($command->getName());
50 54 $commandArgs = $command->toArray();
51 55 $opts = $this->serialize($operation, $commandArgs);
52 - $headers = isset($opts['headers']) ? $opts['headers'] : [];
56 + $headers = $opts['headers'] ?? [];
53 57 if ($endpoint instanceof RulesetEndpoint) {
58 + $this->isUseEndpointV2 = \true;
54 59 $this->setEndpointV2RequestOptions($endpoint, $headers);
55 60 }
56 61 $uri = $this->buildEndpoint($operation, $commandArgs, $opts);
57 - return new Request($operation['http']['method'], $uri, $headers, isset($opts['body']) ? $opts['body'] : null);
62 + return new Request($operation['http']['method'], $uri, $headers, $opts['body'] ?? null);
58 63 }
59 64 /**
60 65 * Modifies a hash of request options for a payload body.
61 66 *
62 - * @param StructureShape $member Member to serialize
63 - * @param array $value Value to serialize
64 - * @param array $opts Request options to modify.
67 + * @param StructureShape $member Member to serialize
68 + * @param array $value Value to serialize
69 + * @param array $opts Request options to modify.
65 70 */
66 71 protected abstract function payload(StructureShape $member, array $value, array &$opts);
67 72 private function serialize(Operation $operation, array $args)
68 73 {
@@ -77,22 +82,22 @@
77 82 $member = $input->getMember($name);
78 83 $location = $member['location'];
79 84 if (!$payload && !$location) {
80 85 $bodyMembers[$name] = $value;
81 - } elseif ($location == 'header') {
86 + } elseif ($location === 'header') {
82 87 $this->applyHeader($name, $member, $value, $opts);
83 - } elseif ($location == 'querystring') {
88 + } elseif ($location === 'querystring') {
84 89 $this->applyQuery($name, $member, $value, $opts);
85 - } elseif ($location == 'headers') {
90 + } elseif ($location === 'headers') {
86 91 $this->applyHeaderMap($name, $member, $value, $opts);
87 92 }
88 93 }
89 94 }
90 95 if (isset($bodyMembers)) {
91 - $this->payload($operation->getInput(), $bodyMembers, $opts);
96 + $this->payload($input, $bodyMembers, $opts);
92 97 } else {
93 98 if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
94 - $this->payload($operation->getInput(), [], $opts);
99 + $this->payload($input, [], $opts);
95 100 }
96 101 }
97 102 return $opts;
98 103 }
@@ -101,12 +106,30 @@
101 106 if (!isset($args[$name])) {
102 107 return;
103 108 }
104 109 $m = $input->getMember($name);
105 - if ($m['streaming'] || ($m['type'] == 'string' || $m['type'] == 'blob')) {
110 + $type = $m->getType();
111 + if ($m['streaming'] || ($type === 'string' || $type === 'blob')) {
112 + // This path skips setting the content-type header usually done in
113 + // RestJsonSerializer and RestXmlSerializer.certain S3 and glacier
114 + // operations determine content type in Middleware::ContentType()
115 + if (!isset(self::$excludeContentType[$this->api->getServiceName()])) {
116 + switch ($type) {
117 + case 'string':
118 + $opts['headers']['Content-Type'] = 'text/plain';
119 + break;
120 + case 'blob':
121 + $opts['headers']['Content-Type'] = 'application/octet-stream';
122 + break;
123 + }
124 + }
125 + $body = $args[$name];
126 + if (!$m['streaming'] && \is_string($body)) {
127 + $opts['headers']['Content-Length'] = \strlen($body);
128 + }
106 129 // Streaming bodies or payloads that are strings are
107 130 // always just a stream of data.
108 - $opts['body'] = Psr7\Utils::streamFor($args[$name]);
131 + $opts['body'] = Psr7\Utils::streamFor($body);
109 132 return;
110 133 }
111 134 $this->payload($m, $args[$name], $opts);
112 135 }
@@ -111,13 +134,29 @@
111 134 $this->payload($m, $args[$name], $opts);
112 135 }
113 136 private function applyHeader($name, Shape $member, $value, array &$opts)
114 137 {
115 - if ($member->getType() === 'timestamp') {
116 - $timestampFormat = !empty($member['timestampFormat']) ? $member['timestampFormat'] : 'rfc822';
117 - $value = TimestampShape::format($value, $timestampFormat);
118 - } elseif ($member->getType() === 'boolean') {
119 - $value = $value ? 'true' : 'false';
138 + // Handle lists by recursively applying header logic to each element
139 + if ($member instanceof ListShape) {
140 + $listMember = $member->getMember();
141 + $headerValues = [];
142 + foreach ($value as $listValue) {
143 + $tempOpts = ['headers' => []];
144 + $this->applyHeader('temp', $listMember, $listValue, $tempOpts);
145 + $convertedValue = $tempOpts['headers']['temp'];
146 + $headerValues[] = $convertedValue;
147 + }
148 + $value = $headerValues;
149 + } elseif (!\is_null($value)) {
150 + switch ($member->getType()) {
151 + case 'timestamp':
152 + $timestampFormat = $member['timestampFormat'] ?? 'rfc822';
153 + $value = $this->formatTimestamp($value, $timestampFormat);
154 + break;
155 + case 'boolean':
156 + $value = $this->formatBoolean($value);
157 + break;
158 + }
120 159 }
121 160 if ($member['jsonvalue']) {
122 161 $value = \json_encode($value);
123 162 if (empty($value) && \JSON_ERROR_NONE !== \json_last_error()) {
@@ -140,78 +179,142 @@
140 179 private function applyQuery($name, Shape $member, $value, array &$opts)
141 180 {
142 181 if ($member instanceof MapShape) {
143 182 $opts['query'] = isset($opts['query']) && \is_array($opts['query']) ? $opts['query'] + $value : $value;
144 - } elseif ($value !== null) {
145 - $type = $member->getType();
146 - if ($type === 'boolean') {
147 - $value = $value ? 'true' : 'false';
148 - } elseif ($type === 'timestamp') {
149 - $timestampFormat = !empty($member['timestampFormat']) ? $member['timestampFormat'] : 'iso8601';
150 - $value = TimestampShape::format($value, $timestampFormat);
183 + } elseif ($member instanceof ListShape) {
184 + $listMember = $member->getMember();
185 + $paramName = $member['locationName'] ?: $name;
186 + foreach ($value as $listValue) {
187 + // Recursively call applyQuery for each list element
188 + $tempOpts = ['query' => []];
189 + $this->applyQuery('temp', $listMember, $listValue, $tempOpts);
190 + $opts['query'][$paramName][] = $tempOpts['query']['temp'];
151 191 }
192 + } elseif (!\is_null($value)) {
193 + switch ($member->getType()) {
194 + case 'timestamp':
195 + $timestampFormat = $member['timestampFormat'] ?? 'iso8601';
196 + $value = $this->formatTimestamp($value, $timestampFormat);
197 + break;
198 + case 'boolean':
199 + $value = $this->formatBoolean($value);
200 + break;
201 + }
152 202 $opts['query'][$member['locationName'] ?: $name] = $value;
153 203 }
154 204 }
155 - private function buildEndpoint(Operation $operation, array $args, array $opts)
205 + private function buildEndpoint(Operation $operation, array $args, array $opts) : UriInterface
156 206 {
157 - // Create an associative array of variable definitions used in expansions
207 + // Expand `requestUri` field members
208 + $relativeUri = $this->expandUriTemplate($operation, $args);
209 + // Add query members to relativeUri
210 + if (!empty($opts['query'])) {
211 + $relativeUri = $this->appendQuery($opts['query'], $relativeUri);
212 + }
213 + // Special case - S3 keys that need path preservation
214 + if ($this->api->getServiceName() === 's3' && isset($args['Key']) && $this->shouldPreservePath($args['Key'])) {
215 + return new Uri($this->endpoint . $relativeUri);
216 + }
217 + return $this->resolveUri($relativeUri, $opts);
218 + }
219 + /**
220 + * Expands `requestUri` members
221 + *
222 + * @param Operation $operation
223 + * @param array $args
224 + *
225 + * @return string
226 + */
227 + private function expandUriTemplate(Operation $operation, array $args) : string
228 + {
158 229 $varDefinitions = $this->getVarDefinitions($operation, $args);
159 - $relative = \preg_replace_callback('/\\{([^\\}]+)\\}/', function (array $matches) use($varDefinitions) {
160 - $isGreedy = \substr($matches[1], -1, 1) == '+';
161 - $k = $isGreedy ? \substr($matches[1], 0, -1) : $matches[1];
162 - if (!isset($varDefinitions[$k])) {
230 + return \preg_replace_callback(self::TEMPLATE_STRING_REGEX, static function (array $matches) use($varDefinitions) {
231 + $isGreedy = \str_ends_with($matches[1], '+');
232 + $varName = $isGreedy ? \substr($matches[1], 0, -1) : $matches[1];
233 + if (!isset($varDefinitions[$varName])) {
163 234 return '';
164 235 }
236 + $value = $varDefinitions[$varName];
165 237 if ($isGreedy) {
166 - return \str_replace('%2F', '/', \rawurlencode($varDefinitions[$k]));
238 + return \str_replace('%2F', '/', \rawurlencode($value));
167 239 }
168 - return \rawurlencode($varDefinitions[$k]);
240 + return \rawurlencode($value);
169 241 }, $operation['http']['requestUri']);
170 - // Add the query string variables or appending to one if needed.
171 - if (!empty($opts['query'])) {
172 - $relative = $this->appendQuery($opts['query'], $relative);
173 - }
174 - $path = $this->endpoint->getPath();
175 - //Accounts for trailing '/' in path when custom endpoint
176 - //is provided to endpointProviderV2
177 - if ($this->api->isModifiedModel() && $this->api->getServiceName() === 's3') {
178 - if (\substr($path, -1) === '/' && $relative[0] === '/') {
179 - $path = \rtrim($path, '/');
180 - }
181 - $relative = $path . $relative;
182 - if (\strpos($relative, '../') !== \false || \substr($relative, -2) === '..') {
183 - if ($relative[0] !== '/') {
184 - $relative = '/' . $relative;
242 + }
243 + /**
244 + * Checks for path-like key names. If detected, traditional
245 + * URI resolution is bypassed.
246 + *
247 + * @param string $key
248 + * @return bool
249 + */
250 + private function shouldPreservePath(string $key) : bool
251 + {
252 + // Keys with dot segments
253 + if (\str_contains($key, '.')) {
254 + $segments = \explode('/', $key);
255 + foreach ($segments as $segment) {
256 + if ($segment === '.' || $segment === '..') {
257 + return \true;
185 258 }
186 - return new Uri($this->endpoint->withPath('') . $relative);
187 259 }
188 260 }
189 - // If endpoint has path, remove leading '/' to preserve URI resolution.
190 - if ($path && $relative[0] === '/') {
191 - $relative = \substr($relative, 1);
261 + // Keys starting with slash
262 + if (\str_starts_with($key, '/')) {
263 + return \true;
192 264 }
193 - //Append path to endpoint when leading '//...'
194 - // present as uri cannot be properly resolved
195 - if ($this->api->isModifiedModel() && \strpos($relative, '//') === 0) {
196 - return new Uri($this->endpoint . $relative);
265 + return \false;
266 + }
267 + /**
268 + * @param string $relativeUri
269 + * @param array $opts
270 + *
271 + * @return UriInterface
272 + */
273 + private function resolveUri(string $relativeUri, array $opts) : UriInterface
274 + {
275 + $basePath = $this->endpoint->getPath();
276 + // Only process if we have a non-empty base path
277 + if (!empty($basePath) && $basePath !== '/') {
278 + // if relative is just '/', we want just the base path without trailing slash
279 + if ($relativeUri === '/' || empty($relativeUri)) {
280 + // Remove trailing slash if present
281 + return $this->endpoint->withPath(\rtrim($basePath, '/'));
282 + }
283 + // if relative is '/?query', we want base path without trailing slash + query
284 + // for now, this is only seen with S3 GetBucketLocation after processing the model
285 + if (empty($opts['query']) && \str_starts_with($relativeUri, '/?')) {
286 + $query = \substr($relativeUri, 2);
287 + // Remove '/?'
288 + return $this->endpoint->withQuery($query);
289 + }
290 + // Ensure base path has trailing slash
291 + if (!\str_ends_with($basePath, '/')) {
292 + $this->endpoint = $this->endpoint->withPath($basePath . '/');
293 + }
294 + // Remove leading slash from relative path to make it relative
295 + if (\str_starts_with($relativeUri, '/')) {
296 + $relativeUri = \substr($relativeUri, 1);
297 + }
197 298 }
198 - // Expand path place holders using Amazon's slightly different URI
199 - // template syntax.
200 - return UriResolver::resolve($this->endpoint, new Uri($relative));
299 + return UriResolver::resolve($this->endpoint, new Uri($relativeUri));
201 300 }
202 301 /**
203 302 * @param StructureShape $input
303 + * @param $payload
304 + *
305 + * @return bool
204 306 */
205 307 private function hasPayloadParam(StructureShape $input, $payload)
206 308 {
207 309 if ($payload) {
208 310 $potentiallyEmptyTypes = ['blob', 'string'];
209 - if ($this->api->getMetadata('protocol') == 'rest-xml') {
311 + if ($this->api->getProtocol() === 'rest-xml') {
210 312 $potentiallyEmptyTypes[] = 'structure';
211 313 }
212 314 $payloadMember = $input->getMember($payload);
213 - if (\in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
315 + //unions may also be empty/unset
316 + if (!empty($payloadMember['union']) || \in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
214 317 return \false;
215 318 }
216 319 }
217 320 foreach ($input->getMembers() as $member) {
@@ -220,20 +323,63 @@
220 323 }
221 324 }
222 325 return \false;
223 326 }
224 - private function appendQuery($query, $endpoint)
327 + /**
328 + * @param $query
329 + * @param $relativeUri
330 + *
331 + * @return string
332 + */
333 + private function appendQuery($query, $relativeUri) : string
225 334 {
226 335 $append = Psr7\Query::build($query);
227 - return $endpoint .= \strpos($endpoint, '?') !== \false ? "&{$append}" : "?{$append}";
336 + return $relativeUri . (\str_contains($relativeUri, '?') ? "&{$append}" : "?{$append}");
228 337 }
229 - private function getVarDefinitions($command, $args)
338 + /**
339 + * @param CommandInterface $command
340 + * @param array $args
341 + *
342 + * @return array
343 + */
344 + private function getVarDefinitions(Operation $operation, array $args) : array
230 345 {
231 346 $varDefinitions = [];
232 - foreach ($command->getInput()->getMembers() as $name => $member) {
233 - if ($member['location'] == 'uri') {
234 - $varDefinitions[$member['locationName'] ?: $name] = isset($args[$name]) ? $args[$name] : null;
347 + foreach ($operation->getInput()->getMembers() as $name => $member) {
348 + if ($member['location'] === 'uri') {
349 + $value = $args[$name] ?? null;
350 + if (!\is_null($value)) {
351 + switch ($member->getType()) {
352 + case 'timestamp':
353 + $timestampFormat = $member['timestampFormat'] ?? 'iso8601';
354 + $value = $this->formatTimestamp($value, $timestampFormat);
355 + break;
356 + case 'boolean':
357 + $value = $this->formatBoolean($value);
358 + break;
359 + }
360 + }
361 + $varDefinitions[$member['locationName'] ?: $name] = $value;
235 362 }
236 363 }
237 364 return $varDefinitions;
365 + }
366 + /**
367 + * @param DateTimeInterface|string|int $value
368 + * @param string $timestampFormat
369 + *
370 + * @return string
371 + */
372 + private function formatTimestamp(DateTimeInterface|string|int $value, string $timestampFormat) : string
373 + {
374 + return TimestampShape::format($value, $timestampFormat);
375 + }
376 + /**
377 + * @param $value
378 + *
379 + * @return string
380 + */
381 + private function formatBoolean($value) : string
382 + {
383 + return $value ? 'true' : 'false';
238 384 }
239 385 }