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 * Abstract IO base class 20 */ 21 22 if (!class_exists('Google_Client')) { 23 require_once dirname(__FILE__) . '/../autoload.php'; 24 } 25 26 abstract class Google_IO_Abstract 27 { 28 const UNKNOWN_CODE = 0; 29 const FORM_URLENCODED = 'application/x-www-form-urlencoded'; 30 private static $CONNECTION_ESTABLISHED_HEADERS = array( 31 "HTTP/1.0 200 Connection established\r\n\r\n", 32 "HTTP/1.1 200 Connection established\r\n\r\n", 33 ); 34 private static $ENTITY_HTTP_METHODS = array("POST" => null, "PUT" => null); 35 private static $HOP_BY_HOP = array( 36 'connection' => true, 37 'keep-alive' => true, 38 'proxy-authenticate' => true, 39 'proxy-authorization' => true, 40 'te' => true, 41 'trailers' => true, 42 'transfer-encoding' => true, 43 'upgrade' => true 44 ); 45 46 47 /** @var Google_Client */ 48 protected $client; 49 50 public function __construct(Google_Client $client) 51 { 52 $this->client = $client; 53 $timeout = $client->getClassConfig('Google_IO_Abstract', 'request_timeout_seconds'); 54 if ($timeout > 0) { 55 $this->setTimeout($timeout); 56 } 57 } 58 59 /** 60 * Executes a Google_Http_Request 61 * @param Google_Http_Request $request the http request to be executed 62 * @return array containing response headers, body, and http code 63 * @throws Google_IO_Exception on curl or IO error 64 */ 65 abstract public function executeRequest(Google_Http_Request $request); 66 67 /** 68 * Set options that update the transport implementation's behavior. 69 * @param $options 70 */ 71 abstract public function setOptions($options); 72 73 /** 74 * Set the maximum request time in seconds. 75 * @param $timeout in seconds 76 */ 77 abstract public function setTimeout($timeout); 78 79 /** 80 * Get the maximum request time in seconds. 81 * @return timeout in seconds 82 */ 83 abstract public function getTimeout(); 84 85 /** 86 * Test for the presence of a cURL header processing bug 87 * 88 * The cURL bug was present in versions prior to 7.30.0 and caused the header 89 * length to be miscalculated when a "Connection established" header added by 90 * some proxies was present. 91 * 92 * @return boolean 93 */ 94 abstract protected function needsQuirk(); 95 96 /** 97 * @visible for testing. 98 * Cache the response to an HTTP request if it is cacheable. 99 * @param Google_Http_Request $request 100 * @return bool Returns true if the insertion was successful. 101 * Otherwise, return false. 102 */ 103 public function setCachedRequest(Google_Http_Request $request) 104 { 105 // Determine if the request is cacheable. 106 if (Google_Http_CacheParser::isResponseCacheable($request)) { 107 $this->client->getCache()->set($request->getCacheKey(), $request); 108 return true; 109 } 110 111 return false; 112 } 113 114 /** 115 * Execute an HTTP Request 116 * 117 * @param Google_Http_Request $request the http request to be executed 118 * @return Google_Http_Request http request with the response http code, 119 * response headers and response body filled in 120 * @throws Google_IO_Exception on curl or IO error 121 */ 122 public function makeRequest(Google_Http_Request $request) 123 { 124 // First, check to see if we have a valid cached version. 125 $cached = $this->getCachedRequest($request); 126 if ($cached !== false && $cached instanceof Google_Http_Request) { 127 if (!$this->checkMustRevalidateCachedRequest($cached, $request)) { 128 return $cached; 129 } 130 } 131 132 if (array_key_exists($request->getRequestMethod(), self::$ENTITY_HTTP_METHODS)) { 133 $request = $this->processEntityRequest($request); 134 } 135 136 list($responseData, $responseHeaders, $respHttpCode) = $this->executeRequest($request); 137 138 if ($respHttpCode == 304 && $cached) { 139 // If the server responded NOT_MODIFIED, return the cached request. 140 $this->updateCachedRequest($cached, $responseHeaders); 141 return $cached; 142 } 143 144 if (!isset($responseHeaders['Date']) && !isset($responseHeaders['date'])) { 145 $responseHeaders['date'] = date("r"); 146 } 147 148 $request->setResponseHttpCode($respHttpCode); 149 $request->setResponseHeaders($responseHeaders); 150 $request->setResponseBody($responseData); 151 // Store the request in cache (the function checks to see if the request 152 // can actually be cached) 153 $this->setCachedRequest($request); 154 return $request; 155 } 156 157 /** 158 * @visible for testing. 159 * @param Google_Http_Request $request 160 * @return Google_Http_Request|bool Returns the cached object or 161 * false if the operation was unsuccessful. 162 */ 163 public function getCachedRequest(Google_Http_Request $request) 164 { 165 if (false === Google_Http_CacheParser::isRequestCacheable($request)) { 166 return false; 167 } 168 169 return $this->client->getCache()->get($request->getCacheKey()); 170 } 171 172 /** 173 * @visible for testing 174 * Process an http request that contains an enclosed entity. 175 * @param Google_Http_Request $request 176 * @return Google_Http_Request Processed request with the enclosed entity. 177 */ 178 public function processEntityRequest(Google_Http_Request $request) 179 { 180 $postBody = $request->getPostBody(); 181 $contentType = $request->getRequestHeader("content-type"); 182 183 // Set the default content-type as application/x-www-form-urlencoded. 184 if (false == $contentType) { 185 $contentType = self::FORM_URLENCODED; 186 $request->setRequestHeaders(array('content-type' => $contentType)); 187 } 188 189 // Force the payload to match the content-type asserted in the header. 190 if ($contentType == self::FORM_URLENCODED && is_array($postBody)) { 191 $postBody = http_build_query($postBody, '', '&'); 192 $request->setPostBody($postBody); 193 } 194 195 // Make sure the content-length header is set. 196 if (!$postBody || is_string($postBody)) { 197 $postsLength = strlen($postBody); 198 $request->setRequestHeaders(array('content-length' => $postsLength)); 199 } 200 201 return $request; 202 } 203 204 /** 205 * Check if an already cached request must be revalidated, and if so update 206 * the request with the correct ETag headers. 207 * @param Google_Http_Request $cached A previously cached response. 208 * @param Google_Http_Request $request The outbound request. 209 * return bool If the cached object needs to be revalidated, false if it is 210 * still current and can be re-used. 211 */ 212 protected function checkMustRevalidateCachedRequest($cached, $request) 213 { 214 if (Google_Http_CacheParser::mustRevalidate($cached)) { 215 $addHeaders = array(); 216 if ($cached->getResponseHeader('etag')) { 217 // [13.3.4] If an entity tag has been provided by the origin server, 218 // we must use that entity tag in any cache-conditional request. 219 $addHeaders['If-None-Match'] = $cached->getResponseHeader('etag'); 220 } elseif ($cached->getResponseHeader('date')) { 221 $addHeaders['If-Modified-Since'] = $cached->getResponseHeader('date'); 222 } 223 224 $request->setRequestHeaders($addHeaders); 225 return true; 226 } else { 227 return false; 228 } 229 } 230 231 /** 232 * Update a cached request, using the headers from the last response. 233 * @param Google_Http_Request $cached A previously cached response. 234 * @param mixed Associative array of response headers from the last request. 235 */ 236 protected function updateCachedRequest($cached, $responseHeaders) 237 { 238 $hopByHop = self::$HOP_BY_HOP; 239 if (!empty($responseHeaders['connection'])) { 240 $connectionHeaders = array_map( 241 'strtolower', 242 array_filter( 243 array_map('trim', explode(',', $responseHeaders['connection'])) 244 ) 245 ); 246 $hopByHop += array_fill_keys($connectionHeaders, true); 247 } 248 249 $endToEnd = array_diff_key($responseHeaders, $hopByHop); 250 $cached->setResponseHeaders($endToEnd); 251 } 252 253 /** 254 * Used by the IO lib and also the batch processing. 255 * 256 * @param $respData 257 * @param $headerSize 258 * @return array 259 */ 260 public function parseHttpResponse($respData, $headerSize) 261 { 262 // check proxy header 263 foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) { 264 if (stripos($respData, $established_header) !== false) { 265 // existed, remove it 266 $respData = str_ireplace($established_header, '', $respData); 267 // Subtract the proxy header size unless the cURL bug prior to 7.30.0 268 // is present which prevented the proxy header size from being taken into 269 // account. 270 if (!$this->needsQuirk()) { 271 $headerSize -= strlen($established_header); 272 } 273 break; 274 } 275 } 276 277 if ($headerSize) { 278 $responseBody = substr($respData, $headerSize); 279 $responseHeaders = substr($respData, 0, $headerSize); 280 } else { 281 $responseSegments = explode("\r\n\r\n", $respData, 2); 282 $responseHeaders = $responseSegments[0]; 283 $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : 284 null; 285 } 286 287 $responseHeaders = $this->getHttpResponseHeaders($responseHeaders); 288 return array($responseHeaders, $responseBody); 289 } 290 291 /** 292 * Parse out headers from raw headers 293 * @param rawHeaders array or string 294 * @return array 295 */ 296 public function getHttpResponseHeaders($rawHeaders) 297 { 298 if (is_array($rawHeaders)) { 299 return $this->parseArrayHeaders($rawHeaders); 300 } else { 301 return $this->parseStringHeaders($rawHeaders); 302 } 303 } 304 305 private function parseStringHeaders($rawHeaders) 306 { 307 $headers = array(); 308 $responseHeaderLines = explode("\r\n", $rawHeaders); 309 foreach ($responseHeaderLines as $headerLine) { 310 if ($headerLine && strpos($headerLine, ':') !== false) { 311 list($header, $value) = explode(': ', $headerLine, 2); 312 $header = strtolower($header); 313 if (isset($headers[$header])) { 314 $headers[$header] .= "\n" . $value; 315 } else { 316 $headers[$header] = $value; 317 } 318 } 319 } 320 return $headers; 321 } 322 323 private function parseArrayHeaders($rawHeaders) 324 { 325 $header_count = count($rawHeaders); 326 $headers = array(); 327 328 for ($i = 0; $i < $header_count; $i++) { 329 $header = $rawHeaders[$i]; 330 // Times will have colons in - so we just want the first match. 331 $header_parts = explode(': ', $header, 2); 332 if (count($header_parts) == 2) { 333 $headers[strtolower($header_parts[0])] = $header_parts[1]; 334 } 335 } 336 337 return $headers; 338 } 339 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body