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
media-cloud-sync / includes / sdk / s3 / Aws / Api / Serializer / RestSerializer.php

RestSerializer.php in Media Cloud Sync 1.4.2, at includes/sdk/s3/Aws/Api/Serializer/RestSerializer.php

386 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\Aws\Api\Serializer;
4
5 use Dudlewebs\WPMCS\s3\Aws\Api\ListShape;
6 use Dudlewebs\WPMCS\s3\Aws\Api\MapShape;
7 use Dudlewebs\WPMCS\s3\Aws\Api\Service;
8 use Dudlewebs\WPMCS\s3\Aws\Api\Operation;
9 use Dudlewebs\WPMCS\s3\Aws\Api\Shape;
10 use Dudlewebs\WPMCS\s3\Aws\Api\StructureShape;
11 use Dudlewebs\WPMCS\s3\Aws\Api\TimestampShape;
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;
16 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7;
17 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Request;
18 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Uri;
19 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\UriResolver;
20 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
21 use Dudlewebs\WPMCS\s3\Psr\Http\Message\UriInterface;
22 /**
23 * Serializes HTTP locations like header, uri, payload, etc...
24 * @internal
25 */
26 abstract class RestSerializer
27 {
28 private const TEMPLATE_STRING_REGEX = '/\\{([^\\}]+)\\}/';
29 private static array $excludeContentType = ['s3' => \true, 'glacier' => \true];
30 /** @var Service */
31 private Service $api;
32 /** @var Uri */
33 private $endpoint;
34 /** @var bool */
35 private $isUseEndpointV2;
36 use EndpointV2SerializerTrait;
37 /**
38 * @param Service $api Service API description
39 * @param string $endpoint Endpoint to connect to
40 */
41 public function __construct(Service $api, $endpoint)
42 {
43 $this->api = $api;
44 $this->endpoint = Psr7\Utils::uriFor($endpoint);
45 }
46 /**
47 * @param CommandInterface $command Command to serialize into a request.
48 * @param mixed|null $endpoint
49 * @return RequestInterface
50 */
51 public function __invoke(CommandInterface $command, mixed $endpoint = null)
52 {
53 $operation = $this->api->getOperation($command->getName());
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);
63 }
64 /**
65 * Modifies a hash of request options for a payload body.
66 *
67 * @param StructureShape $member Member to serialize
68 * @param array $value Value to serialize
69 * @param array $opts Request options to modify.
70 */
71 protected abstract function payload(StructureShape $member, array $value, array &$opts);
72 private function serialize(Operation $operation, array $args)
73 {
74 $opts = [];
75 $input = $operation->getInput();
76 // Apply the payload trait if present
77 if ($payload = $input['payload']) {
78 $this->applyPayload($input, $payload, $args, $opts);
79 }
80 foreach ($args as $name => $value) {
81 if ($input->hasMember($name)) {
82 $member = $input->getMember($name);
83 $location = $member['location'];
84 if (!$payload && !$location) {
85 $bodyMembers[$name] = $value;
86 } elseif ($location === 'header') {
87 $this->applyHeader($name, $member, $value, $opts);
88 } elseif ($location === 'querystring') {
89 $this->applyQuery($name, $member, $value, $opts);
90 } elseif ($location === 'headers') {
91 $this->applyHeaderMap($name, $member, $value, $opts);
92 }
93 }
94 }
95 if (isset($bodyMembers)) {
96 $this->payload($input, $bodyMembers, $opts);
97 } else {
98 if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
99 $this->payload($input, [], $opts);
100 }
101 }
102 return $opts;
103 }
104 private function applyPayload(StructureShape $input, $name, array $args, array &$opts)
105 {
106 if (!isset($args[$name])) {
107 return;
108 }
109 $m = $input->getMember($name);
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 }
129 // Streaming bodies or payloads that are strings are
130 // always just a stream of data.
131 $opts['body'] = Psr7\Utils::streamFor($body);
132 return;
133 }
134 $this->payload($m, $args[$name], $opts);
135 }
136 private function applyHeader($name, Shape $member, $value, array &$opts)
137 {
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 }
159 }
160 if ($member['jsonvalue']) {
161 $value = \json_encode($value);
162 if (empty($value) && \JSON_ERROR_NONE !== \json_last_error()) {
163 throw new \InvalidArgumentException('Unable to encode the provided value' . ' with \'json_encode\'. ' . \json_last_error_msg());
164 }
165 $value = \base64_encode($value);
166 }
167 $opts['headers'][$member['locationName'] ?: $name] = $value;
168 }
169 /**
170 * Note: This is currently only present in the Amazon S3 model.
171 */
172 private function applyHeaderMap($name, Shape $member, array $value, array &$opts)
173 {
174 $prefix = $member['locationName'];
175 foreach ($value as $k => $v) {
176 $opts['headers'][$prefix . $k] = $v;
177 }
178 }
179 private function applyQuery($name, Shape $member, $value, array &$opts)
180 {
181 if ($member instanceof MapShape) {
182 $opts['query'] = isset($opts['query']) && \is_array($opts['query']) ? $opts['query'] + $value : $value;
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'];
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 }
202 $opts['query'][$member['locationName'] ?: $name] = $value;
203 }
204 }
205 private function buildEndpoint(Operation $operation, array $args, array $opts) : UriInterface
206 {
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 {
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])) {
234 return '';
235 }
236 $value = $varDefinitions[$varName];
237 if ($isGreedy) {
238 return \str_replace('%2F', '/', \rawurlencode($value));
239 }
240 return \rawurlencode($value);
241 }, $operation['http']['requestUri']);
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 }
260 }
261 // Keys starting with slash
262 if (\str_starts_with($key, '/')) {
263 return \true;
264 }
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 }
298 }
299 return UriResolver::resolve($this->endpoint, new Uri($relativeUri));
300 }
301 /**
302 * @param StructureShape $input
303 * @param $payload
304 *
305 * @return bool
306 */
307 private function hasPayloadParam(StructureShape $input, $payload)
308 {
309 if ($payload) {
310 $potentiallyEmptyTypes = ['blob', 'string'];
311 if ($this->api->getProtocol() === 'rest-xml') {
312 $potentiallyEmptyTypes[] = 'structure';
313 }
314 $payloadMember = $input->getMember($payload);
315 //unions may also be empty/unset
316 if (!empty($payloadMember['union']) || \in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
317 return \false;
318 }
319 }
320 foreach ($input->getMembers() as $member) {
321 if (!isset($member['location'])) {
322 return \true;
323 }
324 }
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';
384 }
385 }
386