Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
<?php
/**
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

if (!class_exists('Google_Client')) {
  require_once dirname(__FILE__) . '/../autoload.php';
}

/**
 * Manage large file uploads, which may be media but can be any type
 * of sizable data.
 */
> #[AllowDynamicProperties]
class Google_Http_MediaFileUpload { const UPLOAD_MEDIA_TYPE = 'media'; const UPLOAD_MULTIPART_TYPE = 'multipart'; const UPLOAD_RESUMABLE_TYPE = 'resumable'; /** @var string $mimeType */ private $mimeType; /** @var string $data */ private $data; /** @var bool $resumable */ private $resumable; /** @var int $chunkSize */ private $chunkSize; /** @var int $size */ private $size; /** @var string $resumeUri */ private $resumeUri; /** @var int $progress */ private $progress; /** @var Google_Client */ private $client; /** @var Google_Http_Request */ private $request; /** @var string */ private $boundary; /** * Result code from last HTTP call * @var int */ private $httpResultCode; /** * @param $mimeType string * @param $data string The bytes you want to upload. * @param $resumable bool * @param bool $chunkSize File will be uploaded in chunks of this many bytes. * only used if resumable=True */ public function __construct( Google_Client $client, Google_Http_Request $request, $mimeType, $data, $resumable = false, $chunkSize = false, $boundary = false ) { $this->client = $client; $this->request = $request; $this->mimeType = $mimeType; $this->data = $data; $this->size = strlen($this->data); $this->resumable = $resumable; if (!$chunkSize) { $chunkSize = 256 * 1024; } $this->chunkSize = $chunkSize; $this->progress = 0; $this->boundary = $boundary; // Process Media Request $this->process(); } /** * Set the size of the file that is being uploaded. * @param $size - int file size in bytes */ public function setFileSize($size) { $this->size = $size; } /** * Return the progress on the upload * @return int progress in bytes uploaded. */ public function getProgress() { return $this->progress; } /** * Return the HTTP result code from the last call made. * @return int code */ public function getHttpResultCode() { return $this->httpResultCode; } /** * Sends a PUT-Request to google drive and parses the response, * setting the appropiate variables from the response() * * @param Google_Http_Request $httpRequest the Reuqest which will be send * * @return false|mixed false when the upload is unfinished or the decoded http response * */ private function makePutRequest(Google_Http_Request $httpRequest) { if ($this->client->getClassConfig("Google_Http_Request", "enable_gzip_for_uploads")) { $httpRequest->enableGzip(); } else { $httpRequest->disableGzip(); } $response = $this->client->getIo()->makeRequest($httpRequest); $response->setExpectedClass($this->request->getExpectedClass()); $code = $response->getResponseHttpCode(); $this->httpResultCode = $code; if (308 == $code) { // Track the amount uploaded. $range = explode('-', $response->getResponseHeader('range')); $this->progress = $range[1] + 1; // Allow for changing upload URLs. $location = $response->getResponseHeader('location'); if ($location) { $this->resumeUri = $location; } // No problems, but upload not complete. return false; } else { return Google_Http_REST::decodeHttpResponse($response, $this->client); } } /** * Send the next part of the file to upload. * @param [$chunk] the next set of bytes to send. If false will used $data passed * at construct time. */ public function nextChunk($chunk = false) { if (false == $this->resumeUri) { $this->resumeUri = $this->fetchResumeUri(); } if (false == $chunk) { $chunk = substr($this->data, $this->progress, $this->chunkSize); } $lastBytePos = $this->progress + strlen($chunk) - 1; $headers = array( 'content-range' => "bytes $this->progress-$lastBytePos/$this->size", 'content-type' => $this->request->getRequestHeader('content-type'), 'content-length' => $this->chunkSize, 'expect' => '', ); $httpRequest = new Google_Http_Request( $this->resumeUri, 'PUT', $headers, $chunk ); return $this->makePutRequest($httpRequest); } /** * Resume a previously unfinished upload * @param $resumeUri the resume-URI of the unfinished, resumable upload. */ public function resume($resumeUri) { $this->resumeUri = $resumeUri; $headers = array( 'content-range' => "bytes */$this->size", 'content-length' => 0, ); $httpRequest = new Google_Http_Request( $this->resumeUri, 'PUT', $headers ); return $this->makePutRequest($httpRequest); } /** * @return array|bool * @visible for testing */ private function process() { $postBody = false; $contentType = false; $meta = $this->request->getPostBody(); $meta = is_string($meta) ? json_decode($meta, true) : $meta; $uploadType = $this->getUploadType($meta); $this->request->setQueryParam('uploadType', $uploadType); $this->transformToUploadUrl(); $mimeType = $this->mimeType ? $this->mimeType : $this->request->getRequestHeader('content-type'); if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { $contentType = $mimeType; $postBody = is_string($meta) ? $meta : json_encode($meta); } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) { $contentType = $mimeType; $postBody = $this->data; } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) { // This is a multipart/related upload. $boundary = $this->boundary ? $this->boundary : mt_rand(); $boundary = str_replace('"', '', $boundary); $contentType = 'multipart/related; boundary=' . $boundary; $related = "--$boundary\r\n"; $related .= "Content-Type: application/json; charset=UTF-8\r\n"; $related .= "\r\n" . json_encode($meta) . "\r\n"; $related .= "--$boundary\r\n"; $related .= "Content-Type: $mimeType\r\n"; $related .= "Content-Transfer-Encoding: base64\r\n"; $related .= "\r\n" . base64_encode($this->data) . "\r\n"; $related .= "--$boundary--"; $postBody = $related; } $this->request->setPostBody($postBody); if (isset($contentType) && $contentType) { $contentTypeHeader['content-type'] = $contentType; $this->request->setRequestHeaders($contentTypeHeader); } } private function transformToUploadUrl() { $base = $this->request->getBaseComponent(); $this->request->setBaseComponent($base . '/upload'); } /** * Valid upload types: * - resumable (UPLOAD_RESUMABLE_TYPE) * - media (UPLOAD_MEDIA_TYPE) * - multipart (UPLOAD_MULTIPART_TYPE) * @param $meta * @return string * @visible for testing */ public function getUploadType($meta) { if ($this->resumable) { return self::UPLOAD_RESUMABLE_TYPE; } if (false == $meta && $this->data) { return self::UPLOAD_MEDIA_TYPE; } return self::UPLOAD_MULTIPART_TYPE; } public function getResumeUri() { return ( $this->resumeUri !== null ? $this->resumeUri : $this->fetchResumeUri() ); } private function fetchResumeUri() { $result = null; $body = $this->request->getPostBody(); if ($body) { $headers = array( 'content-type' => 'application/json; charset=UTF-8', 'content-length' => Google_Utils::getStrLen($body), 'x-upload-content-type' => $this->mimeType, 'x-upload-content-length' => $this->size, 'expect' => '', ); $this->request->setRequestHeaders($headers); } $response = $this->client->getIo()->makeRequest($this->request); $location = $response->getResponseHeader('location'); $code = $response->getResponseHttpCode(); if (200 == $code && true == $location) { return $location; } $message = $code; $body = @json_decode($response->getResponseBody()); if (!empty($body->error->errors) ) { $message .= ': '; foreach ($body->error->errors as $error) { $message .= "{$error->domain}, {$error->message};"; } $message = rtrim($message, ';'); } $error = "Failed to start the resumable upload (HTTP {$message})"; $this->client->getLogger()->error($error); throw new Google_Exception($error); } public function setChunkSize($chunkSize) { $this->chunkSize = $chunkSize; } }