Test Failed
Push — master ( d99c6b...fb4ca3 )
by Stiofan
15:44
created

Google_IO_Abstract::makeRequest()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 13
nop 1
dl 0
loc 34
rs 4.909
c 0
b 0
f 0
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 View Code Duplication
  public function setCachedRequest(Google_Http_Request $request)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
104
  {
105
    // Determine if the request is cacheable.
106
    if (Google_Http_CacheParser::isResponseCacheable($request)) {
107
      $this->client->getCache()->set($request->getCacheKey(), $request);
0 ignored issues
show
Documentation introduced by
$request is of type object<Google_Http_Request>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getCachedRequest($request); of type Google_Http_Request|boolean adds the type boolean to the return on line 141 which is incompatible with the return type documented by Google_IO_Abstract::makeRequest of type Google_Http_Request.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $cached defined by $this->getCachedRequest($request) on line 125 can also be of type boolean; however, Google_IO_Abstract::updateCachedRequest() does only seem to accept object<Google_Http_Request>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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 View Code Duplication
  public function getCachedRequest(Google_Http_Request $request)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
}
340