123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- <?php
- namespace Aws\Api\Serializer;
- use Aws\Api\MapShape;
- use Aws\Api\Service;
- use Aws\Api\Operation;
- use Aws\Api\Shape;
- use Aws\Api\StructureShape;
- use Aws\Api\TimestampShape;
- use Aws\CommandInterface;
- use Aws\EndpointV2\EndpointProviderV2;
- use Aws\EndpointV2\EndpointV2SerializerTrait;
- use Aws\EndpointV2\Ruleset\RulesetEndpoint;
- use GuzzleHttp\Psr7;
- use GuzzleHttp\Psr7\Request;
- use GuzzleHttp\Psr7\Uri;
- use GuzzleHttp\Psr7\UriResolver;
- use Psr\Http\Message\RequestInterface;
- /**
- * Serializes HTTP locations like header, uri, payload, etc...
- * @internal
- */
- abstract class RestSerializer
- {
- use EndpointV2SerializerTrait;
- /** @var Service */
- private $api;
- /** @var Uri */
- private $endpoint;
- /**
- * @param Service $api Service API description
- * @param string $endpoint Endpoint to connect to
- */
- public function __construct(Service $api, $endpoint)
- {
- $this->api = $api;
- $this->endpoint = Psr7\Utils::uriFor($endpoint);
- }
- /**
- * @param CommandInterface $command Command to serialize into a request.
- * @param $endpointProvider Provider used for dynamic endpoint resolution.
- * @param $clientArgs Client arguments used for dynamic endpoint resolution.
- *
- * @return RequestInterface
- */
- public function __invoke(
- CommandInterface $command,
- $endpoint = null
- )
- {
- $operation = $this->api->getOperation($command->getName());
- $commandArgs = $command->toArray();
- $opts = $this->serialize($operation, $commandArgs);
- $headers = isset($opts['headers']) ? $opts['headers'] : [];
- if ($endpoint instanceof RulesetEndpoint) {
- $this->setEndpointV2RequestOptions($endpoint, $headers);
- }
- $uri = $this->buildEndpoint($operation, $commandArgs, $opts);
- return new Request(
- $operation['http']['method'],
- $uri,
- $headers,
- isset($opts['body']) ? $opts['body'] : null
- );
- }
- /**
- * Modifies a hash of request options for a payload body.
- *
- * @param StructureShape $member Member to serialize
- * @param array $value Value to serialize
- * @param array $opts Request options to modify.
- */
- abstract protected function payload(
- StructureShape $member,
- array $value,
- array &$opts
- );
- private function serialize(Operation $operation, array $args)
- {
- $opts = [];
- $input = $operation->getInput();
- // Apply the payload trait if present
- if ($payload = $input['payload']) {
- $this->applyPayload($input, $payload, $args, $opts);
- }
- foreach ($args as $name => $value) {
- if ($input->hasMember($name)) {
- $member = $input->getMember($name);
- $location = $member['location'];
- if (!$payload && !$location) {
- $bodyMembers[$name] = $value;
- } elseif ($location == 'header') {
- $this->applyHeader($name, $member, $value, $opts);
- } elseif ($location == 'querystring') {
- $this->applyQuery($name, $member, $value, $opts);
- } elseif ($location == 'headers') {
- $this->applyHeaderMap($name, $member, $value, $opts);
- }
- }
- }
- if (isset($bodyMembers)) {
- $this->payload($operation->getInput(), $bodyMembers, $opts);
- } else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
- $this->payload($operation->getInput(), [], $opts);
- }
- return $opts;
- }
- private function applyPayload(StructureShape $input, $name, array $args, array &$opts)
- {
- if (!isset($args[$name])) {
- return;
- }
- $m = $input->getMember($name);
- if ($m['streaming'] ||
- ($m['type'] == 'string' || $m['type'] == 'blob')
- ) {
- // Streaming bodies or payloads that are strings are
- // always just a stream of data.
- $opts['body'] = Psr7\Utils::streamFor($args[$name]);
- return;
- }
- $this->payload($m, $args[$name], $opts);
- }
- private function applyHeader($name, Shape $member, $value, array &$opts)
- {
- if ($member->getType() === 'timestamp') {
- $timestampFormat = !empty($member['timestampFormat'])
- ? $member['timestampFormat']
- : 'rfc822';
- $value = TimestampShape::format($value, $timestampFormat);
- } elseif ($member->getType() === 'boolean') {
- $value = $value ? 'true' : 'false';
- }
- if ($member['jsonvalue']) {
- $value = json_encode($value);
- if (empty($value) && JSON_ERROR_NONE !== json_last_error()) {
- throw new \InvalidArgumentException('Unable to encode the provided value'
- . ' with \'json_encode\'. ' . json_last_error_msg());
- }
- $value = base64_encode($value);
- }
- $opts['headers'][$member['locationName'] ?: $name] = $value;
- }
- /**
- * Note: This is currently only present in the Amazon S3 model.
- */
- private function applyHeaderMap($name, Shape $member, array $value, array &$opts)
- {
- $prefix = $member['locationName'];
- foreach ($value as $k => $v) {
- $opts['headers'][$prefix . $k] = $v;
- }
- }
- private function applyQuery($name, Shape $member, $value, array &$opts)
- {
- if ($member instanceof MapShape) {
- $opts['query'] = isset($opts['query']) && is_array($opts['query'])
- ? $opts['query'] + $value
- : $value;
- } elseif ($value !== null) {
- $type = $member->getType();
- if ($type === 'boolean') {
- $value = $value ? 'true' : 'false';
- } elseif ($type === 'timestamp') {
- $timestampFormat = !empty($member['timestampFormat'])
- ? $member['timestampFormat']
- : 'iso8601';
- $value = TimestampShape::format($value, $timestampFormat);
- }
- $opts['query'][$member['locationName'] ?: $name] = $value;
- }
- }
- private function buildEndpoint(Operation $operation, array $args, array $opts)
- {
- // Create an associative array of variable definitions used in expansions
- $varDefinitions = $this->getVarDefinitions($operation, $args);
- $relative = preg_replace_callback(
- '/\{([^\}]+)\}/',
- function (array $matches) use ($varDefinitions) {
- $isGreedy = substr($matches[1], -1, 1) == '+';
- $k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
- if (!isset($varDefinitions[$k])) {
- return '';
- }
- if ($isGreedy) {
- return str_replace('%2F', '/', rawurlencode($varDefinitions[$k]));
- }
- return rawurlencode($varDefinitions[$k]);
- },
- $operation['http']['requestUri']
- );
- // Add the query string variables or appending to one if needed.
- if (!empty($opts['query'])) {
- $relative = $this->appendQuery($opts['query'], $relative);
- }
- $path = $this->endpoint->getPath();
- //Accounts for trailing '/' in path when custom endpoint
- //is provided to endpointProviderV2
- if ($this->api->isModifiedModel()
- && $this->api->getServiceName() === 's3'
- ) {
- if (substr($path, -1) === '/' && $relative[0] === '/') {
- $path = rtrim($path, '/');
- }
- $relative = $path . $relative;
- if (strpos($relative, '../') !== false
- || substr($relative, -2) === '..'
- ) {
- if ($relative[0] !== '/') {
- $relative = '/' . $relative;
- }
- return new Uri($this->endpoint->withPath('') . $relative);
- }
- }
- // If endpoint has path, remove leading '/' to preserve URI resolution.
- if ($path && $relative[0] === '/') {
- $relative = substr($relative, 1);
- }
- //Append path to endpoint when leading '//...'
- // present as uri cannot be properly resolved
- if ($this->api->isModifiedModel()
- && strpos($relative, '//') === 0
- ) {
- return new Uri($this->endpoint . $relative);
- }
- // Expand path place holders using Amazon's slightly different URI
- // template syntax.
- return UriResolver::resolve($this->endpoint, new Uri($relative));
- }
- /**
- * @param StructureShape $input
- */
- private function hasPayloadParam(StructureShape $input, $payload)
- {
- if ($payload) {
- $potentiallyEmptyTypes = ['blob','string'];
- if ($this->api->getMetadata('protocol') == 'rest-xml') {
- $potentiallyEmptyTypes[] = 'structure';
- }
- $payloadMember = $input->getMember($payload);
- if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
- return false;
- }
- }
- foreach ($input->getMembers() as $member) {
- if (!isset($member['location'])) {
- return true;
- }
- }
- return false;
- }
- private function appendQuery($query, $endpoint)
- {
- $append = Psr7\Query::build($query);
- return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}";
- }
- private function getVarDefinitions($command, $args)
- {
- $varDefinitions = [];
- foreach ($command->getInput()->getMembers() as $name => $member) {
- if ($member['location'] == 'uri') {
- $varDefinitions[$member['locationName'] ?: $name] =
- isset($args[$name])
- ? $args[$name]
- : null;
- }
- }
- return $varDefinitions;
- }
- }
|