Ec2ParamBuilder.php
11 months ago
JsonBody.php
11 months ago
JsonRpcSerializer.php
11 months ago
QueryParamBuilder.php
11 months ago
QuerySerializer.php
11 months ago
RestJsonSerializer.php
11 months ago
RestSerializer.php
11 months ago
RestXmlSerializer.php
11 months ago
XmlBody.php
11 months ago
RestSerializer.php
308 lines
| 1 | <?php |
| 2 | namespace Aws\Api\Serializer; |
| 3 | |
| 4 | use Aws\Api\MapShape; |
| 5 | use Aws\Api\Service; |
| 6 | use Aws\Api\Operation; |
| 7 | use Aws\Api\Shape; |
| 8 | use Aws\Api\StructureShape; |
| 9 | use Aws\Api\TimestampShape; |
| 10 | use Aws\CommandInterface; |
| 11 | use Aws\EndpointV2\EndpointProviderV2; |
| 12 | use Aws\EndpointV2\EndpointV2SerializerTrait; |
| 13 | use Aws\EndpointV2\Ruleset\RulesetEndpoint; |
| 14 | use GuzzleHttp\Psr7; |
| 15 | use GuzzleHttp\Psr7\Request; |
| 16 | use GuzzleHttp\Psr7\Uri; |
| 17 | use GuzzleHttp\Psr7\UriResolver; |
| 18 | use Psr\Http\Message\RequestInterface; |
| 19 | |
| 20 | /** |
| 21 | * Serializes HTTP locations like header, uri, payload, etc... |
| 22 | * @internal |
| 23 | */ |
| 24 | abstract class RestSerializer |
| 25 | { |
| 26 | use EndpointV2SerializerTrait; |
| 27 | |
| 28 | /** @var Service */ |
| 29 | private $api; |
| 30 | |
| 31 | /** @var Uri */ |
| 32 | private $endpoint; |
| 33 | |
| 34 | /** |
| 35 | * @param Service $api Service API description |
| 36 | * @param string $endpoint Endpoint to connect to |
| 37 | */ |
| 38 | public function __construct(Service $api, $endpoint) |
| 39 | { |
| 40 | $this->api = $api; |
| 41 | $this->endpoint = Psr7\Utils::uriFor($endpoint); |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * @param CommandInterface $command Command to serialize into a request. |
| 46 | * @param $endpointProvider Provider used for dynamic endpoint resolution. |
| 47 | * @param $clientArgs Client arguments used for dynamic endpoint resolution. |
| 48 | * |
| 49 | * @return RequestInterface |
| 50 | */ |
| 51 | public function __invoke( |
| 52 | CommandInterface $command, |
| 53 | $endpoint = null |
| 54 | ) |
| 55 | { |
| 56 | $operation = $this->api->getOperation($command->getName()); |
| 57 | $commandArgs = $command->toArray(); |
| 58 | $opts = $this->serialize($operation, $commandArgs); |
| 59 | $headers = isset($opts['headers']) ? $opts['headers'] : []; |
| 60 | |
| 61 | if ($endpoint instanceof RulesetEndpoint) { |
| 62 | $this->setEndpointV2RequestOptions($endpoint, $headers); |
| 63 | } |
| 64 | |
| 65 | $uri = $this->buildEndpoint($operation, $commandArgs, $opts); |
| 66 | |
| 67 | return new Request( |
| 68 | $operation['http']['method'], |
| 69 | $uri, |
| 70 | $headers, |
| 71 | isset($opts['body']) ? $opts['body'] : null |
| 72 | ); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Modifies a hash of request options for a payload body. |
| 77 | * |
| 78 | * @param StructureShape $member Member to serialize |
| 79 | * @param array $value Value to serialize |
| 80 | * @param array $opts Request options to modify. |
| 81 | */ |
| 82 | abstract protected function payload( |
| 83 | StructureShape $member, |
| 84 | array $value, |
| 85 | array &$opts |
| 86 | ); |
| 87 | |
| 88 | private function serialize(Operation $operation, array $args) |
| 89 | { |
| 90 | $opts = []; |
| 91 | $input = $operation->getInput(); |
| 92 | |
| 93 | // Apply the payload trait if present |
| 94 | if ($payload = $input['payload']) { |
| 95 | $this->applyPayload($input, $payload, $args, $opts); |
| 96 | } |
| 97 | |
| 98 | foreach ($args as $name => $value) { |
| 99 | if ($input->hasMember($name)) { |
| 100 | $member = $input->getMember($name); |
| 101 | $location = $member['location']; |
| 102 | if (!$payload && !$location) { |
| 103 | $bodyMembers[$name] = $value; |
| 104 | } elseif ($location == 'header') { |
| 105 | $this->applyHeader($name, $member, $value, $opts); |
| 106 | } elseif ($location == 'querystring') { |
| 107 | $this->applyQuery($name, $member, $value, $opts); |
| 108 | } elseif ($location == 'headers') { |
| 109 | $this->applyHeaderMap($name, $member, $value, $opts); |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if (isset($bodyMembers)) { |
| 115 | $this->payload($operation->getInput(), $bodyMembers, $opts); |
| 116 | } else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) { |
| 117 | $this->payload($operation->getInput(), [], $opts); |
| 118 | } |
| 119 | |
| 120 | return $opts; |
| 121 | } |
| 122 | |
| 123 | private function applyPayload(StructureShape $input, $name, array $args, array &$opts) |
| 124 | { |
| 125 | if (!isset($args[$name])) { |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | $m = $input->getMember($name); |
| 130 | |
| 131 | if ($m['streaming'] || |
| 132 | ($m['type'] == 'string' || $m['type'] == 'blob') |
| 133 | ) { |
| 134 | // Streaming bodies or payloads that are strings are |
| 135 | // always just a stream of data. |
| 136 | $opts['body'] = Psr7\Utils::streamFor($args[$name]); |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | $this->payload($m, $args[$name], $opts); |
| 141 | } |
| 142 | |
| 143 | private function applyHeader($name, Shape $member, $value, array &$opts) |
| 144 | { |
| 145 | if ($member->getType() === 'timestamp') { |
| 146 | $timestampFormat = !empty($member['timestampFormat']) |
| 147 | ? $member['timestampFormat'] |
| 148 | : 'rfc822'; |
| 149 | $value = TimestampShape::format($value, $timestampFormat); |
| 150 | } elseif ($member->getType() === 'boolean') { |
| 151 | $value = $value ? 'true' : 'false'; |
| 152 | } |
| 153 | |
| 154 | if ($member['jsonvalue']) { |
| 155 | $value = json_encode($value); |
| 156 | if (empty($value) && JSON_ERROR_NONE !== json_last_error()) { |
| 157 | throw new \InvalidArgumentException('Unable to encode the provided value' |
| 158 | . ' with \'json_encode\'. ' . json_last_error_msg()); |
| 159 | } |
| 160 | |
| 161 | $value = base64_encode($value); |
| 162 | } |
| 163 | |
| 164 | $opts['headers'][$member['locationName'] ?: $name] = $value; |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Note: This is currently only present in the Amazon S3 model. |
| 169 | */ |
| 170 | private function applyHeaderMap($name, Shape $member, array $value, array &$opts) |
| 171 | { |
| 172 | $prefix = $member['locationName']; |
| 173 | foreach ($value as $k => $v) { |
| 174 | $opts['headers'][$prefix . $k] = $v; |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | private function applyQuery($name, Shape $member, $value, array &$opts) |
| 179 | { |
| 180 | if ($member instanceof MapShape) { |
| 181 | $opts['query'] = isset($opts['query']) && is_array($opts['query']) |
| 182 | ? $opts['query'] + $value |
| 183 | : $value; |
| 184 | } elseif ($value !== null) { |
| 185 | $type = $member->getType(); |
| 186 | if ($type === 'boolean') { |
| 187 | $value = $value ? 'true' : 'false'; |
| 188 | } elseif ($type === 'timestamp') { |
| 189 | $timestampFormat = !empty($member['timestampFormat']) |
| 190 | ? $member['timestampFormat'] |
| 191 | : 'iso8601'; |
| 192 | $value = TimestampShape::format($value, $timestampFormat); |
| 193 | } |
| 194 | |
| 195 | $opts['query'][$member['locationName'] ?: $name] = $value; |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | private function buildEndpoint(Operation $operation, array $args, array $opts) |
| 200 | { |
| 201 | // Create an associative array of variable definitions used in expansions |
| 202 | $varDefinitions = $this->getVarDefinitions($operation, $args); |
| 203 | |
| 204 | $relative = preg_replace_callback( |
| 205 | '/\{([^\}]+)\}/', |
| 206 | function (array $matches) use ($varDefinitions) { |
| 207 | $isGreedy = substr($matches[1], -1, 1) == '+'; |
| 208 | $k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1]; |
| 209 | if (!isset($varDefinitions[$k])) { |
| 210 | return ''; |
| 211 | } |
| 212 | |
| 213 | if ($isGreedy) { |
| 214 | return str_replace('%2F', '/', rawurlencode($varDefinitions[$k])); |
| 215 | } |
| 216 | |
| 217 | return rawurlencode($varDefinitions[$k]); |
| 218 | }, |
| 219 | $operation['http']['requestUri'] |
| 220 | ); |
| 221 | |
| 222 | // Add the query string variables or appending to one if needed. |
| 223 | if (!empty($opts['query'])) { |
| 224 | $relative = $this->appendQuery($opts['query'], $relative); |
| 225 | } |
| 226 | |
| 227 | $path = $this->endpoint->getPath(); |
| 228 | |
| 229 | //Accounts for trailing '/' in path when custom endpoint |
| 230 | //is provided to endpointProviderV2 |
| 231 | if ($this->api->isModifiedModel() |
| 232 | && $this->api->getServiceName() === 's3' |
| 233 | ) { |
| 234 | if (substr($path, -1) === '/' && $relative[0] === '/') { |
| 235 | $path = rtrim($path, '/'); |
| 236 | } |
| 237 | $relative = $path . $relative; |
| 238 | |
| 239 | if (strpos($relative, '../') !== false) { |
| 240 | if ($relative[0] !== '/') { |
| 241 | $relative = '/' . $relative; |
| 242 | } |
| 243 | return new Uri($this->endpoint . $relative); |
| 244 | } |
| 245 | } |
| 246 | // If endpoint has path, remove leading '/' to preserve URI resolution. |
| 247 | if ($path && $relative[0] === '/') { |
| 248 | $relative = substr($relative, 1); |
| 249 | } |
| 250 | |
| 251 | //Append path to endpoint when leading '//...' |
| 252 | // present as uri cannot be properly resolved |
| 253 | if ($this->api->isModifiedModel() |
| 254 | && strpos($relative, '//') === 0 |
| 255 | ) { |
| 256 | return new Uri($this->endpoint . $relative); |
| 257 | } |
| 258 | |
| 259 | // Expand path place holders using Amazon's slightly different URI |
| 260 | // template syntax. |
| 261 | return UriResolver::resolve($this->endpoint, new Uri($relative)); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * @param StructureShape $input |
| 266 | */ |
| 267 | private function hasPayloadParam(StructureShape $input, $payload) |
| 268 | { |
| 269 | if ($payload) { |
| 270 | $potentiallyEmptyTypes = ['blob','string']; |
| 271 | if ($this->api->getMetadata('protocol') == 'rest-xml') { |
| 272 | $potentiallyEmptyTypes[] = 'structure'; |
| 273 | } |
| 274 | $payloadMember = $input->getMember($payload); |
| 275 | if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) { |
| 276 | return false; |
| 277 | } |
| 278 | } |
| 279 | foreach ($input->getMembers() as $member) { |
| 280 | if (!isset($member['location'])) { |
| 281 | return true; |
| 282 | } |
| 283 | } |
| 284 | return false; |
| 285 | } |
| 286 | |
| 287 | private function appendQuery($query, $endpoint) |
| 288 | { |
| 289 | $append = Psr7\Query::build($query); |
| 290 | return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}"; |
| 291 | } |
| 292 | |
| 293 | private function getVarDefinitions($command, $args) |
| 294 | { |
| 295 | $varDefinitions = []; |
| 296 | |
| 297 | foreach ($command->getInput()->getMembers() as $name => $member) { |
| 298 | if ($member['location'] == 'uri') { |
| 299 | $varDefinitions[$member['locationName'] ?: $name] = |
| 300 | isset($args[$name]) |
| 301 | ? $args[$name] |
| 302 | : null; |
| 303 | } |
| 304 | } |
| 305 | return $varDefinitions; |
| 306 | } |
| 307 | } |
| 308 |