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
18/**
19 * Curl based implementation of apiIO.
20 *
21 * @author Chris Chabot <chabotc@google.com>
22 * @author Chirag Shah <chirags@google.com>
23 */
24
25require_once 'Google_CacheParser.php';
26
27class Google_CurlIO extends Google_IO {
28  private static $ENTITY_HTTP_METHODS = array("POST" => null, "PUT" => null);
29  private static $HOP_BY_HOP = array(
30      'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
31      'te', 'trailers', 'transfer-encoding', 'upgrade');
32
33  private $curlParams = array (
34      CURLOPT_RETURNTRANSFER => true,
35      CURLOPT_FOLLOWLOCATION => 0,
36      CURLOPT_FAILONERROR => false,
37      CURLOPT_SSL_VERIFYPEER => true,
38      CURLOPT_HEADER => true,
39      CURLOPT_VERBOSE => false,
40  );
41
42  /**
43   * Check for cURL availability.
44   */
45  public function __construct() {
46    if (! function_exists('curl_init')) {
47      throw new Exception(
48        'Google CurlIO client requires the CURL PHP extension');
49    }
50  }
51
52  /**
53   * Perform an authenticated / signed apiHttpRequest.
54   * This function takes the apiHttpRequest, calls apiAuth->sign on it
55   * (which can modify the request in what ever way fits the auth mechanism)
56   * and then calls apiCurlIO::makeRequest on the signed request
57   *
58   * @param Google_HttpRequest $request
59   * @return Google_HttpRequest The resulting HTTP response including the
60   * responseHttpCode, responseHeaders and responseBody.
61   */
62  public function authenticatedRequest(Google_HttpRequest $request) {
63    $request = Google_Client::$auth->sign($request);
64    return $this->makeRequest($request);
65  }
66
67  /**
68   * Execute a apiHttpRequest
69   *
70   * @param Google_HttpRequest $request the http request to be executed
71   * @return Google_HttpRequest http request with the response http code, response
72   * headers and response body filled in
73   * @throws Google_IOException on curl or IO error
74   */
75  public function makeRequest(Google_HttpRequest $request) {
76    // First, check to see if we have a valid cached version.
77    $cached = $this->getCachedRequest($request);
78    if ($cached !== false) {
79      if (!$this->checkMustRevaliadateCachedRequest($cached, $request)) {
80        return $cached;
81      }
82    }
83
84    if (array_key_exists($request->getRequestMethod(),
85          self::$ENTITY_HTTP_METHODS)) {
86      $request = $this->processEntityRequest($request);
87    }
88
89    $ch = curl_init();
90    curl_setopt_array($ch, $this->curlParams);
91    curl_setopt($ch, CURLOPT_URL, $request->getUrl());
92    if ($request->getPostBody()) {
93      curl_setopt($ch, CURLOPT_POSTFIELDS, $request->getPostBody());
94    }
95
96    $requestHeaders = $request->getRequestHeaders();
97    if ($requestHeaders && is_array($requestHeaders)) {
98      $parsed = array();
99      foreach ($requestHeaders as $k => $v) {
100        $parsed[] = "$k: $v";
101      }
102      curl_setopt($ch, CURLOPT_HTTPHEADER, $parsed);
103    }
104
105    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request->getRequestMethod());
106    curl_setopt($ch, CURLOPT_USERAGENT, $request->getUserAgent());
107    $respData = curl_exec($ch);
108
109    // Retry if certificates are missing.
110    if (curl_errno($ch) == CURLE_SSL_CACERT) {
111      error_log('SSL certificate problem, verify that the CA cert is OK.'
112        . ' Retrying with the CA cert bundle from google-api-php-client.');
113      curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/cacerts.pem');
114      $respData = curl_exec($ch);
115    }
116
117    $respHeaderSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
118    $respHttpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
119    $curlErrorNum = curl_errno($ch);
120    $curlError = curl_error($ch);
121    curl_close($ch);
122    if ($curlErrorNum != CURLE_OK) {
123      throw new Google_IOException("HTTP Error: ($respHttpCode) $curlError");
124    }
125
126    // Parse out the raw response into usable bits
127    list($responseHeaders, $responseBody) =
128          self::parseHttpResponse($respData, $respHeaderSize);
129
130    if ($respHttpCode == 304 && $cached) {
131      // If the server responded NOT_MODIFIED, return the cached request.
132      $this->updateCachedRequest($cached, $responseHeaders);
133      return $cached;
134    }
135
136    // Fill in the apiHttpRequest with the response values
137    $request->setResponseHttpCode($respHttpCode);
138    $request->setResponseHeaders($responseHeaders);
139    $request->setResponseBody($responseBody);
140    // Store the request in cache (the function checks to see if the request
141    // can actually be cached)
142    $this->setCachedRequest($request);
143    // And finally return it
144    return $request;
145  }
146
147  /**
148   * Set options that update cURL's default behavior.
149   * The list of accepted options are:
150   * {@link http://php.net/manual/en/function.curl-setopt.php]
151   *
152   * @param array $optCurlParams Multiple options used by a cURL session.
153   */
154  public function setOptions($optCurlParams) {
155    foreach ($optCurlParams as $key => $val) {
156      $this->curlParams[$key] = $val;
157    }
158  }
159
160  /**
161   * @param $respData
162   * @param $headerSize
163   * @return array
164   */
165  private static function parseHttpResponse($respData, $headerSize) {
166    if (stripos($respData, parent::CONNECTION_ESTABLISHED) !== false) {
167      $respData = str_ireplace(parent::CONNECTION_ESTABLISHED, '', $respData);
168    }
169
170    if ($headerSize) {
171      $responseBody = substr($respData, $headerSize);
172      $responseHeaders = substr($respData, 0, $headerSize);
173    } else {
174      list($responseHeaders, $responseBody) = explode("\r\n\r\n", $respData, 2);
175    }
176
177    $responseHeaders = self::parseResponseHeaders($responseHeaders);
178    return array($responseHeaders, $responseBody);
179  }
180
181  private static function parseResponseHeaders($rawHeaders) {
182    $responseHeaders = array();
183
184    $responseHeaderLines = explode("\r\n", $rawHeaders);
185    foreach ($responseHeaderLines as $headerLine) {
186      if ($headerLine && strpos($headerLine, ':') !== false) {
187        list($header, $value) = explode(': ', $headerLine, 2);
188        $header = strtolower($header);
189        if (isset($responseHeaders[$header])) {
190          $responseHeaders[$header] .= "\n" . $value;
191        } else {
192          $responseHeaders[$header] = $value;
193        }
194      }
195    }
196    return $responseHeaders;
197  }
198}
199