Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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  }