Search moodle.org's
Developer Documentation

See Release Notes

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

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

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