1<?php
2/*
3 * Copyright 2010 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Http;
19
20use Google\Auth\HttpHandler\HttpHandlerFactory;
21use Google\Client;
22use Google\Task\Runner;
23use Google\Service\Exception as GoogleServiceException;
24use GuzzleHttp\ClientInterface;
25use GuzzleHttp\Exception\RequestException;
26use GuzzleHttp\Psr7\Response;
27use Psr\Http\Message\RequestInterface;
28use Psr\Http\Message\ResponseInterface;
29
30/**
31 * This class implements the RESTful transport of apiServiceRequest()'s
32 */
33class REST
34{
35  /**
36   * Executes a Psr\Http\Message\RequestInterface and (if applicable) automatically retries
37   * when errors occur.
38   *
39   * @param Client $client
40   * @param RequestInterface $req
41   * @param string $expectedClass
42   * @param array $config
43   * @param array $retryMap
44   * @return mixed decoded result
45   * @throws \Google\Service\Exception on server side error (ie: not authenticated,
46   *  invalid or malformed post body, invalid url)
47   */
48  public static function execute(
49      ClientInterface $client,
50      RequestInterface $request,
51      $expectedClass = null,
52      $config = array(),
53      $retryMap = null
54  ) {
55    $runner = new Runner(
56        $config,
57        sprintf('%s %s', $request->getMethod(), (string) $request->getUri()),
58        array(get_class(), 'doExecute'),
59        array($client, $request, $expectedClass)
60    );
61
62    if (null !== $retryMap) {
63      $runner->setRetryMap($retryMap);
64    }
65
66    return $runner->run();
67  }
68
69  /**
70   * Executes a Psr\Http\Message\RequestInterface
71   *
72   * @param Client $client
73   * @param RequestInterface $request
74   * @param string $expectedClass
75   * @return array decoded result
76   * @throws \Google\Service\Exception on server side error (ie: not authenticated,
77   *  invalid or malformed post body, invalid url)
78   */
79  public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null)
80  {
81    try {
82      $httpHandler = HttpHandlerFactory::build($client);
83      $response = $httpHandler($request);
84    } catch (RequestException $e) {
85      // if Guzzle throws an exception, catch it and handle the response
86      if (!$e->hasResponse()) {
87        throw $e;
88      }
89
90      $response = $e->getResponse();
91      // specific checking for Guzzle 5: convert to PSR7 response
92      if ($response instanceof \GuzzleHttp\Message\ResponseInterface) {
93        $response = new Response(
94            $response->getStatusCode(),
95            $response->getHeaders() ?: [],
96            $response->getBody(),
97            $response->getProtocolVersion(),
98            $response->getReasonPhrase()
99        );
100      }
101    }
102
103    return self::decodeHttpResponse($response, $request, $expectedClass);
104  }
105
106  /**
107   * Decode an HTTP Response.
108   * @static
109   * @throws \Google\Service\Exception
110   * @param RequestInterface $response The http response to be decoded.
111   * @param ResponseInterface $response
112   * @param string $expectedClass
113   * @return mixed|null
114   */
115  public static function decodeHttpResponse(
116      ResponseInterface $response,
117      RequestInterface $request = null,
118      $expectedClass = null
119  ) {
120    $code = $response->getStatusCode();
121
122    // retry strategy
123    if (intVal($code) >= 400) {
124      // if we errored out, it should be safe to grab the response body
125      $body = (string) $response->getBody();
126
127      // Check if we received errors, and add those to the Exception for convenience
128      throw new GoogleServiceException($body, $code, null, self::getResponseErrors($body));
129    }
130
131    // Ensure we only pull the entire body into memory if the request is not
132    // of media type
133    $body = self::decodeBody($response, $request);
134
135    if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) {
136      $json = json_decode($body, true);
137
138      return new $expectedClass($json);
139    }
140
141    return $response;
142  }
143
144  private static function decodeBody(ResponseInterface $response, RequestInterface $request = null)
145  {
146    if (self::isAltMedia($request)) {
147      // don't decode the body, it's probably a really long string
148      return '';
149    }
150
151    return (string) $response->getBody();
152  }
153
154  private static function determineExpectedClass($expectedClass, RequestInterface $request = null)
155  {
156    // "false" is used to explicitly prevent an expected class from being returned
157    if (false === $expectedClass) {
158      return null;
159    }
160
161    // if we don't have a request, we just use what's passed in
162    if (null === $request) {
163      return $expectedClass;
164    }
165
166    // return what we have in the request header if one was not supplied
167    return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class');
168  }
169
170  private static function getResponseErrors($body)
171  {
172    $json = json_decode($body, true);
173
174    if (isset($json['error']['errors'])) {
175      return $json['error']['errors'];
176    }
177
178    return null;
179  }
180
181  private static function isAltMedia(RequestInterface $request = null)
182  {
183    if ($request && $qs = $request->getUri()->getQuery()) {
184      parse_str($qs, $query);
185      if (isset($query['alt']) && $query['alt'] == 'media') {
186        return true;
187      }
188    }
189
190    return false;
191  }
192}
193