Issues (96)

lib/Google/Http/MediaFileUpload.php (8 issues)

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
use GuzzleHttp\Psr7;
19
use GuzzleHttp\Psr7\Request;
20
use GuzzleHttp\Psr7\Uri;
21
use Psr\Http\Message\RequestInterface;
22
23
/**
24
 * Manage large file uploads, which may be media but can be any type
25
 * of sizable data.
26
 */
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 Psr\Http\Message\RequestInterface */
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
      RequestInterface $request,
79
      $mimeType,
80
      $data,
81
      $resumable = false,
82
      $chunkSize = false
83
  ) {
84
    $this->client = $client;
85
    $this->request = $request;
86
    $this->mimeType = $mimeType;
87
    $this->data = $data;
88
    $this->resumable = $resumable;
89
    $this->chunkSize = $chunkSize;
0 ignored issues
show
Documentation Bug introduced by
The property $chunkSize was declared of type integer, but $chunkSize is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
90
    $this->progress = 0;
91
92
    $this->process();
93
  }
94
95
  /**
96
   * Set the size of the file that is being uploaded.
97
   * @param $size - int file size in bytes
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
98
   */
99
  public function setFileSize($size)
100
  {
101
    $this->size = $size;
102
  }
103
104
  /**
105
   * Return the progress on the upload
106
   * @return int progress in bytes uploaded.
107
   */
108
  public function getProgress()
109
  {
110
    return $this->progress;
111
  }
112
113
  /**
114
   * Send the next part of the file to upload.
115
   * @param [$chunk] the next set of bytes to send. If false will used $data passed
0 ignored issues
show
The type the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
116
   * at construct time.
117
   */
118
  public function nextChunk($chunk = false)
119
  {
120
    $resumeUri = $this->getResumeUri();
121
122
    if (false == $chunk) {
123
      $chunk = substr($this->data, $this->progress, $this->chunkSize);
124
    }
125
126
    $lastBytePos = $this->progress + strlen($chunk) - 1;
127
    $headers = array(
128
      'content-range' => "bytes $this->progress-$lastBytePos/$this->size",
129
      'content-length' => strlen($chunk),
130
      'expect' => '',
131
    );
132
133
    $request = new Request(
134
        'PUT',
135
        $resumeUri,
136
        $headers,
137
        Psr7\stream_for($chunk)
0 ignored issues
show
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

137
        /** @scrutinizer ignore-call */ 
138
        Psr7\stream_for($chunk)
Loading history...
138
    );
139
140
    return $this->makePutRequest($request);
141
  }
142
143
  /**
144
   * Return the HTTP result code from the last call made.
145
   * @return int code
146
   */
147
  public function getHttpResultCode()
148
  {
149
    return $this->httpResultCode;
150
  }
151
152
  /**
153
  * Sends a PUT-Request to google drive and parses the response,
154
  * setting the appropiate variables from the response()
155
  *
156
  * @param Google_Http_Request $httpRequest the Reuqest which will be send
0 ignored issues
show
The type Google_Http_Request was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
157
  *
158
  * @return false|mixed false when the upload is unfinished or the decoded http response
159
  *
160
  */
161
  private function makePutRequest(RequestInterface $request)
162
  {
163
    $response = $this->client->execute($request);
164
    $this->httpResultCode = $response->getStatusCode();
165
166
    if (308 == $this->httpResultCode) {
167
      // Track the amount uploaded.
168
      $range = $response->getHeaderLine('range');
169
      if ($range) {
170
        $range_array = explode('-', $range);
171
        $this->progress = $range_array[1] + 1;
172
      }
173
174
      // Allow for changing upload URLs.
175
      $location = $response->getHeaderLine('location');
176
      if ($location) {
177
        $this->resumeUri = $location;
178
      }
179
180
      // No problems, but upload not complete.
181
      return false;
182
    }
183
184
    return Google_Http_REST::decodeHttpResponse($response, $this->request);
185
  }
186
187
  /**
188
   * Resume a previously unfinished upload
189
   * @param $resumeUri the resume-URI of the unfinished, resumable upload.
190
   */
191
  public function resume($resumeUri)
192
  {
193
     $this->resumeUri = $resumeUri;
194
     $headers = array(
195
       'content-range' => "bytes */$this->size",
196
       'content-length' => 0,
197
     );
198
     $httpRequest = new Request(
199
         'PUT',
200
         $this->resumeUri,
201
         $headers
202
     );
203
204
     return $this->makePutRequest($httpRequest);
205
  }
206
207
  /**
208
   * @return Psr\Http\Message\RequestInterface $request
209
   * @visible for testing
210
   */
211
  private function process()
212
  {
213
    $this->transformToUploadUrl();
214
    $request = $this->request;
215
216
    $postBody = '';
217
    $contentType = false;
218
219
    $meta = (string) $request->getBody();
220
    $meta = is_string($meta) ? json_decode($meta, true) : $meta;
0 ignored issues
show
The condition is_string($meta) is always true.
Loading history...
221
222
    $uploadType = $this->getUploadType($meta);
223
    $request = $request->withUri(
224
        Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType)
225
    );
226
227
    $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type');
228
229
    if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
230
      $contentType = $mimeType;
231
      $postBody = is_string($meta) ? $meta : json_encode($meta);
232
    } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) {
233
      $contentType = $mimeType;
234
      $postBody = $this->data;
235
    } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
236
      // This is a multipart/related upload.
237
      $boundary = $this->boundary ?: mt_rand();
238
      $boundary = str_replace('"', '', $boundary);
239
      $contentType = 'multipart/related; boundary=' . $boundary;
240
      $related = "--$boundary\r\n";
241
      $related .= "Content-Type: application/json; charset=UTF-8\r\n";
242
      $related .= "\r\n" . json_encode($meta) . "\r\n";
243
      $related .= "--$boundary\r\n";
244
      $related .= "Content-Type: $mimeType\r\n";
245
      $related .= "Content-Transfer-Encoding: base64\r\n";
246
      $related .= "\r\n" . base64_encode($this->data) . "\r\n";
247
      $related .= "--$boundary--";
248
      $postBody = $related;
249
    }
250
251
    $request = $request->withBody(Psr7\stream_for($postBody));
0 ignored issues
show
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

251
    $request = $request->withBody(/** @scrutinizer ignore-call */ Psr7\stream_for($postBody));
Loading history...
252
253
    if (isset($contentType) && $contentType) {
254
      $request = $request->withHeader('content-type', $contentType);
255
    }
256
257
    return $this->request = $request;
258
  }
259
260
  /**
261
   * Valid upload types:
262
   * - resumable (UPLOAD_RESUMABLE_TYPE)
263
   * - media (UPLOAD_MEDIA_TYPE)
264
   * - multipart (UPLOAD_MULTIPART_TYPE)
265
   * @param $meta
266
   * @return string
267
   * @visible for testing
268
   */
269
  public function getUploadType($meta)
270
  {
271
    if ($this->resumable) {
272
      return self::UPLOAD_RESUMABLE_TYPE;
273
    }
274
275
    if (false == $meta && $this->data) {
276
      return self::UPLOAD_MEDIA_TYPE;
277
    }
278
279
    return self::UPLOAD_MULTIPART_TYPE;
280
  }
281
282
  public function getResumeUri()
283
  {
284
    if (null === $this->resumeUri) {
285
      $this->resumeUri = $this->fetchResumeUri();
286
    }
287
288
    return $this->resumeUri;
289
  }
290
291
  private function fetchResumeUri()
292
  {
293
    $body = $this->request->getBody();
294
    if ($body) {
0 ignored issues
show
$body is of type Psr\Http\Message\StreamInterface, thus it always evaluated to true.
Loading history...
295
      $headers = array(
296
        'content-type' => 'application/json; charset=UTF-8',
297
        'content-length' => $body->getSize(),
298
        'x-upload-content-type' => $this->mimeType,
299
        'x-upload-content-length' => $this->size,
300
        'expect' => '',
301
      );
302
      foreach ($headers as $key => $value) {
303
        $this->request = $this->request->withHeader($key, $value);
304
      }
305
    }
306
307
    $response = $this->client->execute($this->request, false);
308
    $location = $response->getHeaderLine('location');
309
    $code = $response->getStatusCode();
310
311
    if (200 == $code && true == $location) {
312
      return $location;
313
    }
314
315
    $message = $code;
316
    $body = json_decode((string) $this->request->getBody(), true);
317
    if (isset($body['error']['errors'])) {
318
      $message .= ': ';
319
      foreach ($body['error']['errors'] as $error) {
320
        $message .= "{$error['domain']}, {$error['message']};";
321
      }
322
      $message = rtrim($message, ';');
323
    }
324
325
    $error = "Failed to start the resumable upload (HTTP {$message})";
326
    $this->client->getLogger()->error($error);
327
328
    throw new Google_Exception($error);
329
  }
330
331
  private function transformToUploadUrl()
332
  {
333
    $parts = parse_url((string) $this->request->getUri());
334
    if (!isset($parts['path'])) {
335
      $parts['path'] = '';
336
    }
337
    $parts['path'] = '/upload' . $parts['path'];
338
    $uri = Uri::fromParts($parts);
339
    $this->request = $this->request->withUri($uri);
340
  }
341
342
  public function setChunkSize($chunkSize)
343
  {
344
    $this->chunkSize = $chunkSize;
345
  }
346
347
  public function getRequest()
348
  {
349
    return $this->request;
350
  }
351
}
352