* */ class Yoast_Google_MediaFileUpload { const UPLOAD_MEDIA_TYPE = 'media'; const UPLOAD_MULTIPART_TYPE = 'multipart'; const UPLOAD_RESUMABLE_TYPE = 'resumable'; /** @var string $mimeType */ public $mimeType; /** @var string $data */ public $data; /** @var bool $resumable */ public $resumable; /** @var int $chunkSize */ public $chunkSize; /** @var int $size */ public $size; /** @var string $resumeUri */ public $resumeUri; /** @var int $progress */ public $progress; /** * @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($mimeType, $data, $resumable=false, $chunkSize=false) { $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; } public function setFileSize($size) { $this->size = $size; } /** * @static * @param $meta * @param $params * @return array|bool */ public static function process($meta, &$params) { $payload = array(); $meta = is_string($meta) ? json_decode($meta, true) : $meta; $uploadType = self::getUploadType($meta, $payload, $params); if (!$uploadType) { // Process as a normal API request. return false; } // Process as a media upload request. $params['uploadType'] = array( 'type' => 'string', 'location' => 'query', 'value' => $uploadType, ); $mimeType = isset($params['mimeType']) ? $params['mimeType']['value'] : false; unset($params['mimeType']); if (!$mimeType) { $mimeType = $payload['content-type']; } if (isset($params['file'])) { // This is a standard file upload with curl. $file = $params['file']['value']; unset($params['file']); return self::processFileUpload($file, $mimeType); } $data = isset($params['data']) ? $params['data']['value'] : false; unset($params['data']); if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { $payload['content-type'] = $mimeType; $payload['postBody'] = is_string($meta) ? $meta : json_encode($meta); } elseif (self::UPLOAD_MEDIA_TYPE == $uploadType) { // This is a simple media upload. $payload['content-type'] = $mimeType; $payload['postBody'] = $data; } elseif (self::UPLOAD_MULTIPART_TYPE == $uploadType) { // This is a multipart/related upload. $boundary = isset($params['boundary']['value']) ? $params['boundary']['value'] : mt_rand(); $boundary = str_replace('"', '', $boundary); $payload['content-type'] = '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($data) . "\r\n"; $related .= "--$boundary--"; $payload['postBody'] = $related; } return $payload; } /** * Prepares a standard file upload via cURL. * @param $file * @param $mime * @return array Includes the processed file name. * @visible For testing. */ public static function processFileUpload($file, $mime) { if (!$file) return array(); if (substr($file, 0, 1) != '@') { $file = '@' . $file; } // This is a standard file upload with curl. $params = array('postBody' => array('file' => $file)); if ($mime) { $params['content-type'] = $mime; } return $params; } /** * Valid upload types: * - resumable (UPLOAD_RESUMABLE_TYPE) * - media (UPLOAD_MEDIA_TYPE) * - multipart (UPLOAD_MULTIPART_TYPE) * - none (false) * @param $meta * @param $payload * @param $params * @return bool|string */ public static function getUploadType($meta, &$payload, &$params) { if (isset($params['mediaUpload']) && get_class($params['mediaUpload']['value']) == 'Google_MediaFileUpload') { $upload = $params['mediaUpload']['value']; unset($params['mediaUpload']); $payload['content-type'] = $upload->mimeType; if (isset($upload->resumable) && $upload->resumable) { return self::UPLOAD_RESUMABLE_TYPE; } } // Allow the developer to override the upload type. if (isset($params['uploadType'])) { return $params['uploadType']['value']; } $data = isset($params['data']['value']) ? $params['data']['value'] : false; if (false == $data && false == isset($params['file'])) { // No upload data available. return false; } if (isset($params['file'])) { return self::UPLOAD_MEDIA_TYPE; } if (false == $meta) { return self::UPLOAD_MEDIA_TYPE; } return self::UPLOAD_MULTIPART_TYPE; } public function nextChunk(Yoast_Google_HttpRequest $req, $chunk=false) { if (false == $this->resumeUri) { $this->resumeUri = $this->getResumeUri($req); } 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' => $req->getRequestHeader('content-type'), 'content-length' => $this->chunkSize, 'expect' => '', ); $httpRequest = new Yoast_Google_HttpRequest($this->resumeUri, 'PUT', $headers, $chunk); $response = Yoast_Google_Client::$io->authenticatedRequest($httpRequest); $code = $response->getResponseHttpCode(); if (308 == $code) { $range = explode('-', $response->getResponseHeader('range')); $this->progress = $range[1] + 1; return false; } else { return Yoast_Google_REST::decodeHttpResponse($response); } } private function getResumeUri(Yoast_Google_HttpRequest $httpRequest) { $result = null; $body = $httpRequest->getPostBody(); if ($body) { $httpRequest->setRequestHeaders(array( 'content-type' => 'application/json; charset=UTF-8', 'content-length' => Yoast_Google_Utils::getStrLen($body), 'x-upload-content-type' => $this->mimeType, 'x-upload-content-length' => $this->size, 'expect' => '', )); } $response = Yoast_Google_Client::$io->makeRequest($httpRequest); $location = $response->getResponseHeader('location'); $code = $response->getResponseHttpCode(); if (200 == $code && true == $location) { return $location; } throw new Yoast_Google_Exception("Failed to start the resumable upload"); } }