RestSerializer.php 10.0 KB

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