1<?php
2/*
3 * Copyright 2013 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 * Http Streams based implementation of Google_IO.
20 *
21 * @author Stuart Langley <slangley@google.com>
22 */
23
24if (!class_exists('Google_Client')) {
25  require_once dirname(__FILE__) . '/../autoload.php';
26}
27
28class Google_IO_Stream extends Google_IO_Abstract
29{
30  const TIMEOUT = "timeout";
31  const ZLIB = "compress.zlib://";
32  private $options = array();
33  private $trappedErrorNumber;
34  private $trappedErrorString;
35
36  private static $DEFAULT_HTTP_CONTEXT = array(
37    "follow_location" => 0,
38    "ignore_errors" => 1,
39  );
40
41  private static $DEFAULT_SSL_CONTEXT = array(
42    "verify_peer" => true,
43  );
44
45  public function __construct(Google_Client $client)
46  {
47    if (!ini_get('allow_url_fopen')) {
48      $error = 'The stream IO handler requires the allow_url_fopen runtime ' .
49               'configuration to be enabled';
50      $client->getLogger()->critical($error);
51      throw new Google_IO_Exception($error);
52    }
53
54    parent::__construct($client);
55  }
56
57  /**
58   * Execute an HTTP Request
59   *
60   * @param Google_Http_Request $request the http request to be executed
61   * @return array containing response headers, body, and http code
62   * @throws Google_IO_Exception on curl or IO error
63   */
64  public function executeRequest(Google_Http_Request $request)
65  {
66    $default_options = stream_context_get_options(stream_context_get_default());
67
68    $requestHttpContext = array_key_exists('http', $default_options) ?
69        $default_options['http'] : array();
70
71    if ($request->getPostBody()) {
72      $requestHttpContext["content"] = $request->getPostBody();
73    }
74
75    $requestHeaders = $request->getRequestHeaders();
76    if ($requestHeaders && is_array($requestHeaders)) {
77      $headers = "";
78      foreach ($requestHeaders as $k => $v) {
79        $headers .= "$k: $v\r\n";
80      }
81      $requestHttpContext["header"] = $headers;
82    }
83
84    $requestHttpContext["method"] = $request->getRequestMethod();
85    $requestHttpContext["user_agent"] = $request->getUserAgent();
86
87    $requestSslContext = array_key_exists('ssl', $default_options) ?
88        $default_options['ssl'] : array();
89
90    if (!$this->client->isAppEngine() && !array_key_exists("cafile", $requestSslContext)) {
91      $requestSslContext["cafile"] = dirname(__FILE__) . '/cacerts.pem';
92    }
93
94    $options = array(
95        "http" => array_merge(
96            self::$DEFAULT_HTTP_CONTEXT,
97            $requestHttpContext
98        ),
99        "ssl" => array_merge(
100            self::$DEFAULT_SSL_CONTEXT,
101            $requestSslContext
102        )
103    );
104
105    $context = stream_context_create($options);
106
107    $url = $request->getUrl();
108
109    if ($request->canGzip()) {
110      $url = self::ZLIB . $url;
111    }
112
113    $this->client->getLogger()->debug(
114        'Stream request',
115        array(
116            'url' => $url,
117            'method' => $request->getRequestMethod(),
118            'headers' => $requestHeaders,
119            'body' => $request->getPostBody()
120        )
121    );
122
123    // We are trapping any thrown errors in this method only and
124    // throwing an exception.
125    $this->trappedErrorNumber = null;
126    $this->trappedErrorString = null;
127
128    // START - error trap.
129    set_error_handler(array($this, 'trapError'));
130    $fh = fopen($url, 'r', false, $context);
131    restore_error_handler();
132    // END - error trap.
133
134    if ($this->trappedErrorNumber) {
135      $error = sprintf(
136          "HTTP Error: Unable to connect: '%s'",
137          $this->trappedErrorString
138      );
139
140      $this->client->getLogger()->error('Stream ' . $error);
141      throw new Google_IO_Exception($error, $this->trappedErrorNumber);
142    }
143
144    $response_data = false;
145    $respHttpCode = self::UNKNOWN_CODE;
146    if ($fh) {
147      if (isset($this->options[self::TIMEOUT])) {
148        stream_set_timeout($fh, $this->options[self::TIMEOUT]);
149      }
150
151      $response_data = stream_get_contents($fh);
152      fclose($fh);
153
154      $respHttpCode = $this->getHttpResponseCode($http_response_header);
155    }
156
157    if (false === $response_data) {
158      $error = sprintf(
159          "HTTP Error: Unable to connect: '%s'",
160          $respHttpCode
161      );
162
163      $this->client->getLogger()->error('Stream ' . $error);
164      throw new Google_IO_Exception($error, $respHttpCode);
165    }
166
167    $responseHeaders = $this->getHttpResponseHeaders($http_response_header);
168
169    $this->client->getLogger()->debug(
170        'Stream response',
171        array(
172            'code' => $respHttpCode,
173            'headers' => $responseHeaders,
174            'body' => $response_data,
175        )
176    );
177
178    return array($response_data, $responseHeaders, $respHttpCode);
179  }
180
181  /**
182   * Set options that update the transport implementation's behavior.
183   * @param $options
184   */
185  public function setOptions($options)
186  {
187    $this->options = $options + $this->options;
188  }
189
190  /**
191   * Method to handle errors, used for error handling around
192   * stream connection methods.
193   */
194  public function trapError($errno, $errstr)
195  {
196    $this->trappedErrorNumber = $errno;
197    $this->trappedErrorString = $errstr;
198  }
199
200  /**
201   * Set the maximum request time in seconds.
202   * @param $timeout in seconds
203   */
204  public function setTimeout($timeout)
205  {
206    $this->options[self::TIMEOUT] = $timeout;
207  }
208
209  /**
210   * Get the maximum request time in seconds.
211   * @return timeout in seconds
212   */
213  public function getTimeout()
214  {
215    return $this->options[self::TIMEOUT];
216  }
217
218  /**
219   * Test for the presence of a cURL header processing bug
220   *
221   * {@inheritDoc}
222   *
223   * @return boolean
224   */
225  protected function needsQuirk()
226  {
227    return false;
228  }
229
230  protected function getHttpResponseCode($response_headers)
231  {
232    $header_count = count($response_headers);
233
234    for ($i = 0; $i < $header_count; $i++) {
235      $header = $response_headers[$i];
236      if (strncasecmp("HTTP", $header, strlen("HTTP")) == 0) {
237        $response = explode(' ', $header);
238        return $response[1];
239      }
240    }
241    return self::UNKNOWN_CODE;
242  }
243}
244