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.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  /**
   3   * Copyright 2012 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  if (!class_exists('Google_Client')) {
  19    require_once dirname(__FILE__) . '/../autoload.php';
  20  }
  21  
  22  /**
  23   * Manage large file uploads, which may be media but can be any type
  24   * of sizable data.
  25   */
  26  class Google_Http_MediaFileUpload
  27  {
  28    const UPLOAD_MEDIA_TYPE = 'media';
  29    const UPLOAD_MULTIPART_TYPE = 'multipart';
  30    const UPLOAD_RESUMABLE_TYPE = 'resumable';
  31  
  32    /** @var string $mimeType */
  33    private $mimeType;
  34  
  35    /** @var string $data */
  36    private $data;
  37  
  38    /** @var bool $resumable */
  39    private $resumable;
  40  
  41    /** @var int $chunkSize */
  42    private $chunkSize;
  43  
  44    /** @var int $size */
  45    private $size;
  46  
  47    /** @var string $resumeUri */
  48    private $resumeUri;
  49  
  50    /** @var int $progress */
  51    private $progress;
  52  
  53    /** @var Google_Client */
  54    private $client;
  55  
  56    /** @var Google_Http_Request */
  57    private $request;
  58  
  59    /** @var string */
  60    private $boundary;
  61  
  62    /**
  63     * Result code from last HTTP call
  64     * @var int
  65     */
  66    private $httpResultCode;
  67  
  68    /**
  69     * @param $mimeType string
  70     * @param $data string The bytes you want to upload.
  71     * @param $resumable bool
  72     * @param bool $chunkSize File will be uploaded in chunks of this many bytes.
  73     * only used if resumable=True
  74     */
  75    public function __construct(
  76        Google_Client $client,
  77        Google_Http_Request $request,
  78        $mimeType,
  79        $data,
  80        $resumable = false,
  81        $chunkSize = false,
  82        $boundary = false
  83    ) {
  84      $this->client = $client;
  85      $this->request = $request;
  86      $this->mimeType = $mimeType;
  87      $this->data = $data;
  88      $this->size = strlen($this->data);
  89      $this->resumable = $resumable;
  90      if (!$chunkSize) {
  91        $chunkSize = 256 * 1024;
  92      }
  93      $this->chunkSize = $chunkSize;
  94      $this->progress = 0;
  95      $this->boundary = $boundary;
  96  
  97      // Process Media Request
  98      $this->process();
  99    }
 100  
 101    /**
 102     * Set the size of the file that is being uploaded.
 103     * @param $size - int file size in bytes
 104     */
 105    public function setFileSize($size)
 106    {
 107      $this->size = $size;
 108    }
 109  
 110    /**
 111     * Return the progress on the upload
 112     * @return int progress in bytes uploaded.
 113     */
 114    public function getProgress()
 115    {
 116      return $this->progress;
 117    }
 118  
 119    /**
 120     * Return the HTTP result code from the last call made.
 121     * @return int code
 122     */
 123    public function getHttpResultCode()
 124    {
 125      return $this->httpResultCode;
 126    }
 127  
 128    /**
 129    * Sends a PUT-Request to google drive and parses the response,
 130    * setting the appropiate variables from the response()
 131    *
 132    * @param Google_Http_Request $httpRequest the Reuqest which will be send
 133    *
 134    * @return false|mixed false when the upload is unfinished or the decoded http response
 135    *
 136    */
 137    private function makePutRequest(Google_Http_Request $httpRequest)
 138    {
 139      if ($this->client->getClassConfig("Google_Http_Request", "enable_gzip_for_uploads")) {
 140        $httpRequest->enableGzip();
 141      } else {
 142        $httpRequest->disableGzip();
 143      }
 144  
 145      $response = $this->client->getIo()->makeRequest($httpRequest);
 146      $response->setExpectedClass($this->request->getExpectedClass());
 147      $code = $response->getResponseHttpCode();
 148      $this->httpResultCode = $code;
 149  
 150      if (308 == $code) {
 151        // Track the amount uploaded.
 152        $range = explode('-', $response->getResponseHeader('range'));
 153        $this->progress = $range[1] + 1;
 154  
 155        // Allow for changing upload URLs.
 156        $location = $response->getResponseHeader('location');
 157        if ($location) {
 158          $this->resumeUri = $location;
 159        }
 160  
 161        // No problems, but upload not complete.
 162        return false;
 163      } else {
 164        return Google_Http_REST::decodeHttpResponse($response, $this->client);
 165      }
 166    }
 167  
 168    /**
 169     * Send the next part of the file to upload.
 170     * @param [$chunk] the next set of bytes to send. If false will used $data passed
 171     * at construct time.
 172     */
 173    public function nextChunk($chunk = false)
 174    {
 175      if (false == $this->resumeUri) {
 176        $this->resumeUri = $this->fetchResumeUri();
 177      }
 178  
 179      if (false == $chunk) {
 180        $chunk = substr($this->data, $this->progress, $this->chunkSize);
 181      }
 182      $lastBytePos = $this->progress + strlen($chunk) - 1;
 183      $headers = array(
 184        'content-range' => "bytes $this->progress-$lastBytePos/$this->size",
 185        'content-type' => $this->request->getRequestHeader('content-type'),
 186        'content-length' => $this->chunkSize,
 187        'expect' => '',
 188      );
 189  
 190      $httpRequest = new Google_Http_Request(
 191          $this->resumeUri,
 192          'PUT',
 193          $headers,
 194          $chunk
 195      );
 196      return $this->makePutRequest($httpRequest);
 197    }
 198  
 199    /**
 200     * Resume a previously unfinished upload
 201     * @param $resumeUri the resume-URI of the unfinished, resumable upload.
 202     */
 203    public function resume($resumeUri)
 204    {
 205       $this->resumeUri = $resumeUri;
 206       $headers = array(
 207         'content-range' => "bytes */$this->size",
 208         'content-length' => 0,
 209       );
 210       $httpRequest = new Google_Http_Request(
 211           $this->resumeUri,
 212           'PUT',
 213           $headers
 214       );
 215       return $this->makePutRequest($httpRequest);
 216    }
 217  
 218    /**
 219     * @return array|bool
 220     * @visible for testing
 221     */
 222    private function process()
 223    {
 224      $postBody = false;
 225      $contentType = false;
 226  
 227      $meta = $this->request->getPostBody();
 228      $meta = is_string($meta) ? json_decode($meta, true) : $meta;
 229  
 230      $uploadType = $this->getUploadType($meta);
 231      $this->request->setQueryParam('uploadType', $uploadType);
 232      $this->transformToUploadUrl();
 233      $mimeType = $this->mimeType ?
 234          $this->mimeType :
 235          $this->request->getRequestHeader('content-type');
 236  
 237      if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
 238        $contentType = $mimeType;
 239        $postBody = is_string($meta) ? $meta : json_encode($meta);
 240      } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) {
 241        $contentType = $mimeType;
 242        $postBody = $this->data;
 243      } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
 244        // This is a multipart/related upload.
 245        $boundary = $this->boundary ? $this->boundary : mt_rand();
 246        $boundary = str_replace('"', '', $boundary);
 247        $contentType = 'multipart/related; boundary=' . $boundary;
 248        $related = "--$boundary\r\n";
 249        $related .= "Content-Type: application/json; charset=UTF-8\r\n";
 250        $related .= "\r\n" . json_encode($meta) . "\r\n";
 251        $related .= "--$boundary\r\n";
 252        $related .= "Content-Type: $mimeType\r\n";
 253        $related .= "Content-Transfer-Encoding: base64\r\n";
 254        $related .= "\r\n" . base64_encode($this->data) . "\r\n";
 255        $related .= "--$boundary--";
 256        $postBody = $related;
 257      }
 258  
 259      $this->request->setPostBody($postBody);
 260  
 261      if (isset($contentType) && $contentType) {
 262        $contentTypeHeader['content-type'] = $contentType;
 263        $this->request->setRequestHeaders($contentTypeHeader);
 264      }
 265    }
 266  
 267    private function transformToUploadUrl()
 268    {
 269      $base = $this->request->getBaseComponent();
 270      $this->request->setBaseComponent($base . '/upload');
 271    }
 272  
 273    /**
 274     * Valid upload types:
 275     * - resumable (UPLOAD_RESUMABLE_TYPE)
 276     * - media (UPLOAD_MEDIA_TYPE)
 277     * - multipart (UPLOAD_MULTIPART_TYPE)
 278     * @param $meta
 279     * @return string
 280     * @visible for testing
 281     */
 282    public function getUploadType($meta)
 283    {
 284      if ($this->resumable) {
 285        return self::UPLOAD_RESUMABLE_TYPE;
 286      }
 287  
 288      if (false == $meta && $this->data) {
 289        return self::UPLOAD_MEDIA_TYPE;
 290      }
 291  
 292      return self::UPLOAD_MULTIPART_TYPE;
 293    }
 294  
 295    public function getResumeUri()
 296    {
 297      return ( $this->resumeUri !== null ? $this->resumeUri : $this->fetchResumeUri() );
 298    }
 299  
 300    private function fetchResumeUri()
 301    {
 302      $result = null;
 303      $body = $this->request->getPostBody();
 304      if ($body) {
 305        $headers = array(
 306          'content-type' => 'application/json; charset=UTF-8',
 307          'content-length' => Google_Utils::getStrLen($body),
 308          'x-upload-content-type' => $this->mimeType,
 309          'x-upload-content-length' => $this->size,
 310          'expect' => '',
 311        );
 312        $this->request->setRequestHeaders($headers);
 313      }
 314  
 315      $response = $this->client->getIo()->makeRequest($this->request);
 316      $location = $response->getResponseHeader('location');
 317      $code = $response->getResponseHttpCode();
 318  
 319      if (200 == $code && true == $location) {
 320        return $location;
 321      }
 322      $message = $code;
 323      $body = @json_decode($response->getResponseBody());
 324      if (!empty($body->error->errors) ) {
 325        $message .= ': ';
 326        foreach ($body->error->errors as $error) {
 327          $message .= "{$error->domain}, {$error->message};";
 328        }
 329        $message = rtrim($message, ';');
 330      }
 331  
 332      $error = "Failed to start the resumable upload (HTTP {$message})";
 333      $this->client->getLogger()->error($error);
 334      throw new Google_Exception($error);
 335    }
 336  
 337    public function setChunkSize($chunkSize)
 338    {
 339      $this->chunkSize = $chunkSize;
 340    }
 341  }