Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

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