1<?php
2/*
3 * Copyright 2012 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\Client;
21use Google\Http\REST;
22use Google\Service\Exception as GoogleServiceException;
23use GuzzleHttp\Psr7;
24use GuzzleHttp\Psr7\Request;
25use GuzzleHttp\Psr7\Response;
26use Psr\Http\Message\RequestInterface;
27use Psr\Http\Message\ResponseInterface;
28
29/**
30 * Class to handle batched requests to the Google API service.
31 *
32 * Note that calls to `Google\Http\Batch::execute()` do not clear the queued
33 * requests. To start a new batch, be sure to create a new instance of this
34 * class.
35 */
36class Batch
37{
38  const BATCH_PATH = 'batch';
39
40  private static $CONNECTION_ESTABLISHED_HEADERS = array(
41    "HTTP/1.0 200 Connection established\r\n\r\n",
42    "HTTP/1.1 200 Connection established\r\n\r\n",
43  );
44
45  /** @var string Multipart Boundary. */
46  private $boundary;
47
48  /** @var array service requests to be executed. */
49  private $requests = array();
50
51  /** @var Client */
52  private $client;
53
54  private $rootUrl;
55
56  private $batchPath;
57
58  public function __construct(
59      Client $client,
60      $boundary = false,
61      $rootUrl = null,
62      $batchPath = null
63  ) {
64    $this->client = $client;
65    $this->boundary = $boundary ?: mt_rand();
66    $this->rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/');
67    $this->batchPath = $batchPath ?: self::BATCH_PATH;
68  }
69
70  public function add(RequestInterface $request, $key = false)
71  {
72    if (false == $key) {
73      $key = mt_rand();
74    }
75
76    $this->requests[$key] = $request;
77  }
78
79  public function execute()
80  {
81    $body = '';
82    $classes = array();
83    $batchHttpTemplate = <<<EOF
84--%s
85Content-Type: application/http
86Content-Transfer-Encoding: binary
87MIME-Version: 1.0
88Content-ID: %s
89
90%s
91%s%s
92
93
94EOF;
95
96    /** @var RequestInterface $req */
97    foreach ($this->requests as $key => $request) {
98      $firstLine = sprintf(
99          '%s %s HTTP/%s',
100          $request->getMethod(),
101          $request->getRequestTarget(),
102          $request->getProtocolVersion()
103      );
104
105      $content = (string) $request->getBody();
106
107      $headers = '';
108      foreach ($request->getHeaders() as $name => $values) {
109          $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values));
110      }
111
112      $body .= sprintf(
113          $batchHttpTemplate,
114          $this->boundary,
115          $key,
116          $firstLine,
117          $headers,
118          $content ? "\n".$content : ''
119      );
120
121      $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class');
122    }
123
124    $body .= "--{$this->boundary}--";
125    $body = trim($body);
126    $url = $this->rootUrl . '/' . $this->batchPath;
127    $headers = array(
128      'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary),
129      'Content-Length' => strlen($body),
130    );
131
132    $request = new Request(
133        'POST',
134        $url,
135        $headers,
136        $body
137    );
138
139    $response = $this->client->execute($request);
140
141    return $this->parseResponse($response, $classes);
142  }
143
144  public function parseResponse(ResponseInterface $response, $classes = array())
145  {
146    $contentType = $response->getHeaderLine('content-type');
147    $contentType = explode(';', $contentType);
148    $boundary = false;
149    foreach ($contentType as $part) {
150      $part = explode('=', $part, 2);
151      if (isset($part[0]) && 'boundary' == trim($part[0])) {
152        $boundary = $part[1];
153      }
154    }
155
156    $body = (string) $response->getBody();
157    if (!empty($body)) {
158      $body = str_replace("--$boundary--", "--$boundary", $body);
159      $parts = explode("--$boundary", $body);
160      $responses = array();
161      $requests = array_values($this->requests);
162
163      foreach ($parts as $i => $part) {
164        $part = trim($part);
165        if (!empty($part)) {
166          list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2);
167          $headers = $this->parseRawHeaders($rawHeaders);
168
169          $status = substr($part, 0, strpos($part, "\n"));
170          $status = explode(" ", $status);
171          $status = $status[1];
172
173          list($partHeaders, $partBody) = $this->parseHttpResponse($part, false);
174          $response = new Response(
175              $status,
176              $partHeaders,
177              Psr7\Utils::streamFor($partBody)
178          );
179
180          // Need content id.
181          $key = $headers['content-id'];
182
183          try {
184            $response = REST::decodeHttpResponse($response, $requests[$i-1]);
185          } catch (GoogleServiceException $e) {
186            // Store the exception as the response, so successful responses
187            // can be processed.
188            $response = $e;
189          }
190
191          $responses[$key] = $response;
192        }
193      }
194
195      return $responses;
196    }
197
198    return null;
199  }
200
201  private function parseRawHeaders($rawHeaders)
202  {
203    $headers = array();
204    $responseHeaderLines = explode("\r\n", $rawHeaders);
205    foreach ($responseHeaderLines as $headerLine) {
206      if ($headerLine && strpos($headerLine, ':') !== false) {
207        list($header, $value) = explode(': ', $headerLine, 2);
208        $header = strtolower($header);
209        if (isset($headers[$header])) {
210          $headers[$header] = array_merge((array)$headers[$header], (array)$value);
211        } else {
212          $headers[$header] = $value;
213        }
214      }
215    }
216    return $headers;
217  }
218
219  /**
220   * Used by the IO lib and also the batch processing.
221   *
222   * @param $respData
223   * @param $headerSize
224   * @return array
225   */
226  private function parseHttpResponse($respData, $headerSize)
227  {
228    // check proxy header
229    foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) {
230      if (stripos($respData, $established_header) !== false) {
231        // existed, remove it
232        $respData = str_ireplace($established_header, '', $respData);
233        // Subtract the proxy header size unless the cURL bug prior to 7.30.0
234        // is present which prevented the proxy header size from being taken into
235        // account.
236        // @TODO look into this
237        // if (!$this->needsQuirk()) {
238        //   $headerSize -= strlen($established_header);
239        // }
240        break;
241      }
242    }
243
244    if ($headerSize) {
245      $responseBody = substr($respData, $headerSize);
246      $responseHeaders = substr($respData, 0, $headerSize);
247    } else {
248      $responseSegments = explode("\r\n\r\n", $respData, 2);
249      $responseHeaders = $responseSegments[0];
250      $responseBody = isset($responseSegments[1]) ? $responseSegments[1] :
251                                                    null;
252    }
253
254    $responseHeaders = $this->parseRawHeaders($responseHeaders);
255
256    return array($responseHeaders, $responseBody);
257  }
258}
259