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 +244 -59 1.2.4 → 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,12 +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;
13 +use Dudlewebs\WPMCS\s3\Aws\EndpointV2\EndpointV2SerializerTrait;
14 +use Dudlewebs\WPMCS\s3\Aws\EndpointV2\Ruleset\RulesetEndpoint;
15 +use DateTimeInterface;
12 16 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7;
17 +use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Request;
13 18 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Uri;
14 19 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\UriResolver;
15 20 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
21 +use Dudlewebs\WPMCS\s3\Psr\Http\Message\UriInterface;
16 22 /**
17 23 * Serializes HTTP locations like header, uri, payload, etc...
18 24 * @internal
19 25 */
@@ -18,15 +24,20 @@
18 24 * @internal
19 25 */
20 26 abstract class RestSerializer
21 27 {
28 + private const TEMPLATE_STRING_REGEX = '/\\{([^\\}]+)\\}/';
29 + private static array $excludeContentType = ['s3' => \true, 'glacier' => \true];
22 30 /** @var Service */
23 - private $api;
24 - /** @var Psr7\Uri */
31 + private Service $api;
32 + /** @var Uri */
25 33 private $endpoint;
34 + /** @var bool */
35 + private $isUseEndpointV2;
36 + use EndpointV2SerializerTrait;
26 37 /**
27 - * @param Service $api Service API description
28 - * @param string $endpoint Endpoint to connect to
38 + * @param Service $api Service API description
39 + * @param string $endpoint Endpoint to connect to
29 40 */
30 41 public function __construct(Service $api, $endpoint)
31 42 {
32 43 $this->api = $api;
@@ -32,26 +43,31 @@
32 43 $this->api = $api;
33 44 $this->endpoint = Psr7\Utils::uriFor($endpoint);
34 45 }
35 46 /**
36 - * @param CommandInterface $command Command to serialized
37 - *
47 + * @param CommandInterface $command Command to serialize into a request.
48 + * @param mixed|null $endpoint
38 49 * @return RequestInterface
39 50 */
40 - public function __invoke(CommandInterface $command)
51 + public function __invoke(CommandInterface $command, mixed $endpoint = null)
41 52 {
42 53 $operation = $this->api->getOperation($command->getName());
43 - $args = $command->toArray();
44 - $opts = $this->serialize($operation, $args);
45 - $uri = $this->buildEndpoint($operation, $args, $opts);
46 - return new Psr7\Request($operation['http']['method'], $uri, isset($opts['headers']) ? $opts['headers'] : [], isset($opts['body']) ? $opts['body'] : null);
54 + $commandArgs = $command->toArray();
55 + $opts = $this->serialize($operation, $commandArgs);
56 + $headers = $opts['headers'] ?? [];
57 + if ($endpoint instanceof RulesetEndpoint) {
58 + $this->isUseEndpointV2 = \true;
59 + $this->setEndpointV2RequestOptions($endpoint, $headers);
60 + }
61 + $uri = $this->buildEndpoint($operation, $commandArgs, $opts);
62 + return new Request($operation['http']['method'], $uri, $headers, $opts['body'] ?? null);
47 63 }
48 64 /**
49 65 * Modifies a hash of request options for a payload body.
50 66 *
51 - * @param StructureShape $member Member to serialize
52 - * @param array $value Value to serialize
53 - * @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.
54 70 */
55 71 protected abstract function payload(StructureShape $member, array $value, array &$opts);
56 72 private function serialize(Operation $operation, array $args)
57 73 {
@@ -66,22 +82,22 @@
66 82 $member = $input->getMember($name);
67 83 $location = $member['location'];
68 84 if (!$payload && !$location) {
69 85 $bodyMembers[$name] = $value;
70 - } elseif ($location == 'header') {
86 + } elseif ($location === 'header') {
71 87 $this->applyHeader($name, $member, $value, $opts);
72 - } elseif ($location == 'querystring') {
88 + } elseif ($location === 'querystring') {
73 89 $this->applyQuery($name, $member, $value, $opts);
74 - } elseif ($location == 'headers') {
90 + } elseif ($location === 'headers') {
75 91 $this->applyHeaderMap($name, $member, $value, $opts);
76 92 }
77 93 }
78 94 }
79 95 if (isset($bodyMembers)) {
80 - $this->payload($operation->getInput(), $bodyMembers, $opts);
96 + $this->payload($input, $bodyMembers, $opts);
81 97 } else {
82 98 if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
83 - $this->payload($operation->getInput(), [], $opts);
99 + $this->payload($input, [], $opts);
84 100 }
85 101 }
86 102 return $opts;
87 103 }
@@ -90,12 +106,30 @@
90 106 if (!isset($args[$name])) {
91 107 return;
92 108 }
93 109 $m = $input->getMember($name);
94 - 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 + }
95 129 // Streaming bodies or payloads that are strings are
96 130 // always just a stream of data.
97 - $opts['body'] = Psr7\Utils::streamFor($args[$name]);
131 + $opts['body'] = Psr7\Utils::streamFor($body);
98 132 return;
99 133 }
100 134 $this->payload($m, $args[$name], $opts);
101 135 }
@@ -100,13 +134,29 @@
100 134 $this->payload($m, $args[$name], $opts);
101 135 }
102 136 private function applyHeader($name, Shape $member, $value, array &$opts)
103 137 {
104 - if ($member->getType() === 'timestamp') {
105 - $timestampFormat = !empty($member['timestampFormat']) ? $member['timestampFormat'] : 'rfc822';
106 - $value = TimestampShape::format($value, $timestampFormat);
107 - } elseif ($member->getType() === 'boolean') {
108 - $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 + }
109 159 }
110 160 if ($member['jsonvalue']) {
111 161 $value = \json_encode($value);
112 162 if (empty($value) && \JSON_ERROR_NONE !== \json_last_error()) {
@@ -129,65 +179,142 @@
129 179 private function applyQuery($name, Shape $member, $value, array &$opts)
130 180 {
131 181 if ($member instanceof MapShape) {
132 182 $opts['query'] = isset($opts['query']) && \is_array($opts['query']) ? $opts['query'] + $value : $value;
133 - } elseif ($value !== null) {
134 - $type = $member->getType();
135 - if ($type === 'boolean') {
136 - $value = $value ? 'true' : 'false';
137 - } elseif ($type === 'timestamp') {
138 - $timestampFormat = !empty($member['timestampFormat']) ? $member['timestampFormat'] : 'iso8601';
139 - $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'];
140 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 + }
141 202 $opts['query'][$member['locationName'] ?: $name] = $value;
142 203 }
143 204 }
144 - private function buildEndpoint(Operation $operation, array $args, array $opts)
205 + private function buildEndpoint(Operation $operation, array $args, array $opts) : UriInterface
145 206 {
146 - $varspecs = [];
147 - // Create an associative array of varspecs used in expansions
148 - foreach ($operation->getInput()->getMembers() as $name => $member) {
149 - if ($member['location'] == 'uri') {
150 - $varspecs[$member['locationName'] ?: $name] = isset($args[$name]) ? $args[$name] : null;
151 - }
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);
152 212 }
153 - $relative = \preg_replace_callback('/\\{([^\\}]+)\\}/', function (array $matches) use($varspecs) {
154 - $isGreedy = \substr($matches[1], -1, 1) == '+';
155 - $k = $isGreedy ? \substr($matches[1], 0, -1) : $matches[1];
156 - if (!isset($varspecs[$k])) {
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 + {
229 + $varDefinitions = $this->getVarDefinitions($operation, $args);
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])) {
157 234 return '';
158 235 }
236 + $value = $varDefinitions[$varName];
159 237 if ($isGreedy) {
160 - return \str_replace('%2F', '/', \rawurlencode($varspecs[$k]));
238 + return \str_replace('%2F', '/', \rawurlencode($value));
161 239 }
162 - return \rawurlencode($varspecs[$k]);
240 + return \rawurlencode($value);
163 241 }, $operation['http']['requestUri']);
164 - // Add the query string variables or appending to one if needed.
165 - if (!empty($opts['query'])) {
166 - $append = Psr7\Query::build($opts['query']);
167 - $relative .= \strpos($relative, '?') ? "&{$append}" : "?{$append}";
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;
258 + }
259 + }
168 260 }
169 - // If endpoint has path, remove leading '/' to preserve URI resolution.
170 - $path = $this->endpoint->getPath();
171 - if ($path && $relative[0] === '/') {
172 - $relative = \substr($relative, 1);
261 + // Keys starting with slash
262 + if (\str_starts_with($key, '/')) {
263 + return \true;
173 264 }
174 - // Expand path place holders using Amazon's slightly different URI
175 - // template syntax.
176 - return UriResolver::resolve($this->endpoint, new Uri($relative));
265 + return \false;
177 266 }
178 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 + }
298 + }
299 + return UriResolver::resolve($this->endpoint, new Uri($relativeUri));
300 + }
301 + /**
179 302 * @param StructureShape $input
303 + * @param $payload
304 + *
305 + * @return bool
180 306 */
181 307 private function hasPayloadParam(StructureShape $input, $payload)
182 308 {
183 309 if ($payload) {
184 310 $potentiallyEmptyTypes = ['blob', 'string'];
185 - if ($this->api->getMetadata('protocol') == 'rest-xml') {
311 + if ($this->api->getProtocol() === 'rest-xml') {
186 312 $potentiallyEmptyTypes[] = 'structure';
187 313 }
188 314 $payloadMember = $input->getMember($payload);
189 - if (\in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
315 + //unions may also be empty/unset
316 + if (!empty($payloadMember['union']) || \in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
190 317 return \false;
191 318 }
192 319 }
193 320 foreach ($input->getMembers() as $member) {
@@ -195,6 +322,64 @@
195 322 return \true;
196 323 }
197 324 }
198 325 return \false;
326 + }
327 + /**
328 + * @param $query
329 + * @param $relativeUri
330 + *
331 + * @return string
332 + */
333 + private function appendQuery($query, $relativeUri) : string
334 + {
335 + $append = Psr7\Query::build($query);
336 + return $relativeUri . (\str_contains($relativeUri, '?') ? "&{$append}" : "?{$append}");
337 + }
338 + /**
339 + * @param CommandInterface $command
340 + * @param array $args
341 + *
342 + * @return array
343 + */
344 + private function getVarDefinitions(Operation $operation, array $args) : array
345 + {
346 + $varDefinitions = [];
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;
362 + }
363 + }
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';
199 384 }
200 385 }