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