/google_api/IO/Abstract.php

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