PluginProbe
ManageWP Worker / 4.0.1
ManageWP Worker v4.0.1
4.9.38 4.9.37 4.9.36 4.9.35 4.9.34 3.8.7 3.8.8 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.18 3.9.19 3.9.2 3.9.20 3.9.21 3.9.22 3.9.23 3.9.24 All 73 releases
worker / src / Dropbox / Client.php

Client.php in ManageWP Worker 4.0.1, at src/Dropbox/Client.php

1,682 lines 60.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The class used to make most Dropbox API calls. You can use this once you've gotten an
5 * {@link AccessToken} via {@link WebAuth}.
6 *
7 * This class is stateless so it can be shared/reused.
8 */
9 class Dropbox_Client
10 {
11 /**
12 * The access token used by this client to make authenticated API calls. You can get an
13 * access token via {@link WebAuth}.
14 *
15 * @return string AccessToken
16 */
17 public function getAccessToken()
18 {
19 return $this->accessToken;
20 }
21
22 /** @var string AccessToken */
23 private $accessToken;
24
25 /**
26 * An identifier for the API client, typically of the form "Name/Version".
27 * This is used to set the HTTP <code>User-Agent</code> header when making API requests.
28 * Example: <code>"PhotoEditServer/1.3"</code>
29 *
30 * If you're the author a higher-level library on top of the basic SDK, and the
31 * "Photo Edit" app's server code is using your library to access Dropbox, you should append
32 * your library's name and version to form the full identifier. For example,
33 * if your library is called "File Picker", you might set this field to:
34 * <code>"PhotoEditServer/1.3 FilePicker/0.1-beta"</code>
35 *
36 * The exact format of the <code>User-Agent</code> header is described in
37 * <a href="http://tools.ietf.org/html/rfc2616#section-3.8">section 3.8 of the HTTP specification</a>.
38 *
39 * Note that underlying HTTP client may append other things to the <code>User-Agent</code>, such as
40 * the name of the library being used to actually make the HTTP request (such as cURL).
41 *
42 * @return string
43 */
44 public function getClientIdentifier()
45 {
46 return $this->clientIdentifier;
47 }
48
49 /** @var string */
50 private $clientIdentifier;
51
52 /**
53 * The locale of the user of your application. Some API calls return localized
54 * data and error messages; this "user locale" setting determines which locale
55 * the server should use to localize those strings.
56 *
57 * @return null|string
58 */
59 public function getUserLocale()
60 {
61 return $this->userLocale;
62 }
63
64 /** @var null|string */
65 private $userLocale;
66
67 /**
68 * The {@link Host} object that determines the hostnames we make requests to.
69 *
70 * @return Dropbox_Host
71 */
72 public function getHost()
73 {
74 return $this->host;
75 }
76
77 /**
78 * Constructor.
79 *
80 * @param string $accessToken
81 * See {@link getAccessToken()}
82 * @param string $clientIdentifier
83 * See {@link getClientIdentifier()}
84 * @param null|string $userLocale
85 * See {@link getUserLocale()}
86 */
87 public function __construct($accessToken, $clientIdentifier, $userLocale = null)
88 {
89 self::checkAccessTokenArg("accessToken", $accessToken);
90 self::checkClientIdentifierArg("clientIdentifier", $clientIdentifier);
91 Dropbox_Checker::argStringNonEmptyOrNull("userLocale", $userLocale);
92
93 $this->accessToken = $accessToken;
94 $this->clientIdentifier = $clientIdentifier;
95 $this->userLocale = $userLocale;
96
97 // The $host parameter is sort of internal. We don't include it in the param list because
98 // we don't want it to be included in the documentation. Use PHP arg list hacks to get at
99 // it.
100 $host = null;
101 if (func_num_args() == 4) {
102 $host = func_get_arg(3);
103 Dropbox_Host::checkArgOrNull("host", $host);
104 }
105 if ($host === null) {
106 $host = Dropbox_Host::getDefault();
107 }
108 $this->host = $host;
109
110 // These fields are redundant, but it makes these values a little more convenient
111 // to access.
112 $this->apiHost = $host->getApi();
113 $this->contentHost = $host->getContent();
114 }
115
116 /** @var string */
117 private $apiHost;
118 /** @var string */
119 private $contentHost;
120
121 /**
122 * Given a <code>$base</code> path for an API endpoint (for example, "/files"), append
123 * a Dropbox API file path to the end of that URL. Special characters in the file will
124 * be encoded properly.
125 *
126 * This is for endpoints like "/files" takes the path on the URL and not as a separate
127 * query or POST parameter.
128 *
129 * @param string $base
130 * @param string $path
131 *
132 * @return string
133 */
134 public function appendFilePath($base, $path)
135 {
136 return $base."/auto/".rawurlencode(substr($path, 1));
137 }
138
139 /**
140 * Make an API call to disable the access token that you constructed this <code>Client</code>
141 * with. After calling this, API calls made with this <code>Client</code> will fail.
142 *
143 * See <a href="https://www.dropbox.com/developers/core/docs#disable-token">/disable_access_token</a>.
144 *
145 * @throws Dropbox_Exception
146 */
147 public function disableAccessToken()
148 {
149 $response = $this->doPost($this->apiHost, "1/disable_access_token");
150 if ($response->statusCode !== 200) {
151 throw Dropbox_RequestUtil::unexpectedStatus($response);
152 }
153 }
154
155 /**
156 * Make an API call to get basic account and quota information.
157 *
158 * <code>
159 * $client = ...
160 * $accountInfo = $client->getAccountInfo();
161 * print_r($accountInfo);
162 * </code>
163 *
164 * @return array
165 * See <a href="https://www.dropbox.com/developers/core/docs#account-info">/account/info</a>.
166 *
167 * @throws Dropbox_Exception
168 */
169 public function getAccountInfo()
170 {
171 $response = $this->doGet($this->apiHost, "1/account/info");
172 if ($response->statusCode !== 200) {
173 throw Dropbox_RequestUtil::unexpectedStatus($response);
174 }
175
176 return Dropbox_RequestUtil::parseResponseJson($response->body);
177 }
178
179 /**
180 * Downloads a file from Dropbox. The file's contents are written to the
181 * given <code>$outStream</code> and the file's metadata is returned.
182 *
183 * <code>
184 * $client = ...;
185 * $fd = fopen("./Frog.jpeg", "wb");
186 * $metadata = $client->getFile("/Photos/Frog.jpeg", $fd);
187 * fclose($fd);
188 * print_r($metadata);
189 * </code>
190 *
191 * @param string $path
192 * The path to the file on Dropbox (UTF-8).
193 *
194 * @param resource $outStream
195 * If the file exists, the file contents will be written to this stream.
196 *
197 * @param string|null $rev
198 * If you want the latest revision of the file at the given path, pass in <code>null</code>.
199 * If you want a specific version of a file, pass in value of the file metadata's "rev" field.
200 *
201 * @return null|array
202 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
203 * object</a> for the file at the given $path and $rev, or <code>null</code> if the file
204 * doesn't exist,
205 *
206 * @throws Dropbox_Exception
207 */
208 public function getFile($path, $outStream, $rev = null)
209 {
210 Dropbox_Path::checkArgNonRoot("path", $path);
211 Dropbox_Checker::argResource("outStream", $outStream);
212 Dropbox_Checker::argStringNonEmptyOrNull("rev", $rev);
213
214 $url = $this->buildUrlForGetOrPut(
215 $this->contentHost,
216 $this->appendFilePath("1/files", $path),
217 array("rev" => $rev));
218
219 $curl = $this->mkCurl($url);
220 $metadataCatcher = new Dropbox_DropboxMetadataHeaderCatcher($curl->handle);
221 $streamRelay = new Dropbox_CurlStreamRelay($curl->handle, $outStream);
222
223 $response = $curl->exec();
224
225 if ($response->statusCode === 404) {
226 return null;
227 }
228
229 if ($response->statusCode !== 200) {
230 $response->body = $streamRelay->getErrorBody();
231 throw Dropbox_RequestUtil::unexpectedStatus($response);
232 }
233
234 return $metadataCatcher->getMetadata();
235 }
236
237 /**
238 * Calling 'uploadFile' with <code>$numBytes</code> less than this value, will cause this SDK
239 * to use the standard /files_put endpoint. When <code>$numBytes</code> is greater than this
240 * value, we'll use the /chunked_upload endpoint.
241 *
242 * @var int
243 */
244 public static $AUTO_CHUNKED_UPLOAD_THRESHOLD = 9863168; // 8 MB
245
246 /**
247 * @var int
248 */
249 public static $DEFAULT_CHUNK_SIZE = 4194304; // 4 MB
250
251 /**
252 * Creates a file on Dropbox, using the data from <code>$inStream</code> for the file contents.
253 *
254 * <code>
255 * use \Dropbox as dbx;
256 * $client = ...;
257 * $fd = fopen("./frog.jpeg", "rb");
258 * $md1 = $client->uploadFile("/Photos/Frog.jpeg",
259 * dbx\WriteMode::add(), $fd);
260 * fclose($fd);
261 * print_r($md1);
262 * $rev = $md1["rev"];
263 *
264 * // Re-upload with WriteMode::update(...), which will overwrite the
265 * // file if it hasn't been modified from our original upload.
266 * $fd = fopen("./frog-new.jpeg", "rb");
267 * $md2 = $client->uploadFile("/Photos/Frog.jpeg",
268 * dbx\WriteMode::update($rev), $fd);
269 * fclose($fd);
270 * print_r($md2);
271 * </code>
272 *
273 * @param string $path
274 * The Dropbox path to save the file to (UTF-8).
275 *
276 * @param Dropbox_WriteMode $writeMode
277 * What to do if there's already a file at the given path.
278 *
279 * @param resource $inStream
280 * The data to use for the file contents.
281 *
282 * @param int|null $numBytes
283 * You can pass in <code>null</code> if you don't know. If you do provide the size, we can
284 * perform a slightly more efficient upload (fewer network round-trips) for files smaller
285 * than 8 MB.
286 *
287 * @param Callable|null $callback
288 * Curl progress callback.
289 *
290 * @return mixed
291 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
292 * object</a> for the newly-added file.
293 *
294 * @throws Dropbox_Exception
295 */
296 public function uploadFile($path, $writeMode, $inStream, $numBytes = null, $callback = null)
297 {
298 Dropbox_Path::checkArgNonRoot("path", $path);
299 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
300 Dropbox_Checker::argResource("inStream", $inStream);
301 Dropbox_Checker::argNatOrNull("numBytes", $numBytes);
302
303 if ($callback !== null) {
304 Dropbox_Checker::argCallable("callback", $callback);
305 }
306
307 // If we don't know how many bytes are coming, we have to use chunked upload.
308 // If $numBytes is large, we elect to use chunked upload.
309 // In all other cases, use regular upload.
310 if ($numBytes === null || $numBytes > self::$AUTO_CHUNKED_UPLOAD_THRESHOLD) {
311 $metadata = $this->_uploadFileChunked($path, $writeMode, $inStream, $numBytes,
312 self::$DEFAULT_CHUNK_SIZE, $callback);
313 } else {
314 $config = new Dropbox_Closure_CurlConfigInStream($inStream, $numBytes);
315 $metadata = $this->_uploadFile($path, $writeMode, $config, $callback);
316 }
317
318 return $metadata;
319 }
320
321 /**
322 * Creates a file on Dropbox, using the given $data string as the file contents.
323 *
324 * <code>
325 * use \Dropbox as dbx;
326 * $client = ...;
327 * $md = $client->uploadFileFromString("/Grocery List.txt",
328 * dbx\WriteMode::add(),
329 * "1. Coke\n2. Popcorn\n3. Toothpaste\n");
330 * print_r($md);
331 * </code>
332 *
333 * @param string $path
334 * The Dropbox path to save the file to (UTF-8).
335 *
336 * @param Dropbox_WriteMode $writeMode
337 * What to do if there's already a file at the given path.
338 *
339 * @param string $data
340 * The data to use for the contents of the file.
341 *
342 * @return mixed
343 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
344 * object</a> for the newly-added file.
345 *
346 * @throws Dropbox_Exception
347 */
348 public function uploadFileFromString($path, $writeMode, $data)
349 {
350 Dropbox_Path::checkArgNonRoot("path", $path);
351 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
352 Dropbox_Checker::argString("data", $data);
353
354 $config = new Dropbox_Closure_CurlConfigOctetStream($data);
355
356 return $this->_uploadFile($path, $writeMode, $config);
357 }
358
359 /**
360 * Creates a file on Dropbox, using the data from $inStream as the file contents.
361 *
362 * This version of <code>uploadFile</code> splits uploads the file ~4MB chunks at a time and
363 * will retry a few times if one chunk fails to upload. Uses {@link chunkedUploadStart()},
364 * {@link chunkedUploadContinue()}, and {@link chunkedUploadFinish()}.
365 *
366 * @param string $path
367 * The Dropbox path to save the file to (UTF-8).
368 *
369 * @param Dropbox_WriteMode $writeMode
370 * What to do if there's already a file at the given path.
371 *
372 * @param resource $inStream
373 * The data to use for the file contents.
374 *
375 * @param int|null $numBytes
376 * The number of bytes available from $inStream.
377 * You can pass in <code>null</code> if you don't know.
378 *
379 * @param int|null $chunkSize
380 * The number of bytes to upload in each chunk. You can omit this (or pass in
381 * <code>null</code> and the library will use a reasonable default.
382 *
383 * @return mixed
384 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
385 * object</a> for the newly-added file.
386 *
387 * @throws Dropbox_Exception
388 */
389 public function uploadFileChunked($path, $writeMode, $inStream, $numBytes = null, $chunkSize = null)
390 {
391 if ($chunkSize === null) {
392 $chunkSize = self::$DEFAULT_CHUNK_SIZE;
393 }
394
395 Dropbox_Path::checkArgNonRoot("path", $path);
396 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
397 Dropbox_Checker::argResource("inStream", $inStream);
398 Dropbox_Checker::argNatOrNull("numBytes", $numBytes);
399 Dropbox_Checker::argIntPositive("chunkSize", $chunkSize);
400
401 return $this->_uploadFileChunked($path, $writeMode, $inStream, $numBytes, $chunkSize);
402 }
403
404 /**
405 * @param string $path
406 *
407 * @param Dropbox_WriteMode $writeMode
408 * What to do if there's already a file at the given path (UTF-8).
409 *
410 * @param resource $inStream
411 * The source of data to upload.
412 *
413 * @param int|null $numBytes
414 * You can pass in <code>null</code>. But if you know how many bytes you expect, pass in
415 * that value and this function will do a sanity check at the end to make sure the number of
416 * bytes read from $inStream matches up.
417 *
418 * @param int $chunkSize
419 *
420 * @return array
421 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
422 * object</a> for the newly-added file.
423 *
424 * @throws InvalidArgumentException
425 * @throws Dropbox_Exception_BadResponse
426 */
427 private function _uploadFileChunked($path, $writeMode, $inStream, $numBytes, $chunkSize, $callback = null)
428 {
429 Dropbox_Path::checkArg("path", $path);
430 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
431 Dropbox_Checker::argResource("inStream", $inStream);
432 Dropbox_Checker::argNatOrNull("numBytes", $numBytes);
433 Dropbox_Checker::argNat("chunkSize", $chunkSize);
434
435 if ($callback !== null) {
436 Dropbox_Checker::argCallable("callback", $callback);
437 }
438
439 // NOTE: This function performs 3 retries on every call. This is maybe not the right
440 // layer to make retry decisions. It's also awkward because none of the other calls
441 // perform retries.
442
443 assert($chunkSize > 0);
444
445 $data = self::readFully($inStream, $chunkSize);
446 $len = strlen($data);
447
448 $client = $this;
449 $uploadStart = new Dropbox_Closure_ChunkedUploadStartAction($client, $data, $callback);
450 $uploadId = Dropbox_RequestUtil::runWithRetry(3, $uploadStart);
451 unset($uploadStart);
452
453 $byteOffset = $len;
454
455 while (!feof($inStream)) {
456 unset($data);
457 $data = self::readFully($inStream, $chunkSize);
458 $len = strlen($data);
459
460 while (true) {
461 $uploadContinue = new Dropbox_Closure_ChunkedUploadContinueAction($client, $uploadId, $byteOffset, $data, $callback);
462 $r = Dropbox_RequestUtil::runWithRetry(3, $uploadContinue);
463 unset($uploadContinue);
464
465 if ($r === true) { // Chunk got uploaded!
466 $byteOffset += $len;
467 break;
468 }
469 if ($r === false) { // Server didn't recognize our upload ID
470 // This is very unlikely since we're uploading all the chunks in sequence.
471 throw new Dropbox_Exception_BadResponse("Server forgot our uploadId");
472 }
473
474 // Otherwise, the server is at a different byte offset from us.
475 $serverByteOffset = $r;
476 assert($serverByteOffset !== $byteOffset); // chunkedUploadContinue ensures this.
477 // An earlier byte offset means the server has lost data we sent earlier.
478 if ($serverByteOffset < $byteOffset) {
479 throw new Dropbox_Exception_BadResponse(
480 "Server is at an ealier byte offset: us=$byteOffset, server=$serverByteOffset");
481 }
482 $diff = $serverByteOffset - $byteOffset;
483 // If the server is past where we think it could possibly be, something went wrong.
484 if ($diff > $len) {
485 throw new Dropbox_Exception_BadResponse(
486 "Server is more than a chunk ahead: us=$byteOffset, server=$serverByteOffset");
487 }
488 // The normal case is that the server is a bit further along than us because of a
489 // partially-uploaded chunk. Finish it off.
490 $byteOffset += $diff;
491 if ($diff === $len) {
492 break;
493 } // If the server is at the end, we're done.
494 $data = substr($data, $diff);
495 }
496 }
497
498 if ($numBytes !== null && $byteOffset !== $numBytes) {
499 throw new InvalidArgumentException(
500 "You passed numBytes=$numBytes but the stream had $byteOffset bytes.");
501 }
502
503 $uploadFinish = new Dropbox_Closure_ChunkedUploadFinishAction($client, $uploadId, $path, $writeMode);
504 $metadata = Dropbox_RequestUtil::runWithRetry(3, $uploadFinish);
505
506 return $metadata;
507 }
508
509 /**
510 * Sometimes fread() returns less than the request number of bytes (for example, when reading
511 * from network streams). This function repeatedly calls fread until the requested number of
512 * bytes have been read or we've reached EOF.
513 *
514 * @param resource $inStream
515 * @param int $numBytes
516 *
517 * @throws Dropbox_StreamReadException
518 * @return string
519 */
520 private static function readFully($inStream, $numBytes)
521 {
522 Dropbox_Checker::argNat("numBytes", $numBytes);
523
524 $full = '';
525 $bytesRemaining = $numBytes;
526 while (!feof($inStream) && $bytesRemaining > 0) {
527 $part = fread($inStream, $bytesRemaining);
528 if ($part === false) {
529 throw new Dropbox_StreamReadException("Error reading from \$inStream.");
530 }
531 if ($full === '') {
532 $full = $part;
533 } else {
534 $full .= $part;
535 }
536 $bytesRemaining -= strlen($part);
537 }
538
539 return $full;
540 }
541
542 /**
543 * @param string $path
544 * @param Dropbox_WriteMode $writeMode
545 * @param Dropbox_Closure_CurlConfigInterface $curlConfigClosure
546 *
547 * @return array
548 *
549 * @throws Dropbox_Exception
550 */
551 private function _uploadFile($path, $writeMode, Dropbox_Closure_CurlConfigInterface $curlConfigClosure, $callback = null)
552 {
553 Dropbox_Path::checkArg("path", $path);
554 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
555
556 $url = $this->buildUrlForGetOrPut(
557 $this->contentHost,
558 $this->appendFilePath("1/files_put", $path),
559 $writeMode->getExtraParams());
560
561 $curl = $this->mkCurl($url);
562
563 $curlConfigClosure->configure($curl);
564
565 if ($callback) {
566 $curl->set(CURLOPT_NOPROGRESS, false);
567 $curl->set(CURLOPT_PROGRESSFUNCTION, $callback);
568 }
569
570 $curl->set(CURLOPT_RETURNTRANSFER, true);
571 $response = $curl->exec();
572
573 if ($response->statusCode !== 200) {
574 throw Dropbox_RequestUtil::unexpectedStatus($response);
575 }
576
577 return Dropbox_RequestUtil::parseResponseJson($response->body);
578 }
579
580 /**
581 * Start a new chunked upload session and upload the first chunk of data.
582 *
583 * @param string $data
584 * The data to start off the chunked upload session.
585 *
586 * @return array
587 * A pair of <code>(string $uploadId, int $byteOffset)</code>. <code>$uploadId</code>
588 * is a unique identifier for this chunked upload session. You pass this in to
589 * {@link chunkedUploadContinue} and {@link chuunkedUploadFinish}. <code>$byteOffset</code>
590 * is the number of bytes that were successfully uploaded.
591 *
592 * @throws Dropbox_Exception
593 */
594 public function chunkedUploadStart($data, $callback = null)
595 {
596 Dropbox_Checker::argString("data", $data);
597
598 $response = $this->_chunkedUpload(array(), $data, $callback);
599
600 if ($response->statusCode === 404) {
601 throw new Dropbox_Exception_BadResponse("Got a 404, but we didn't send up an 'upload_id'");
602 }
603
604 $correction = self::_chunkedUploadCheckForOffsetCorrection($response);
605 if ($correction !== null) {
606 throw new Dropbox_Exception_BadResponse(
607 "Got an offset-correcting 400 response, but we didn't send an offset");
608 }
609
610 if ($response->statusCode !== 200) {
611 throw Dropbox_RequestUtil::unexpectedStatus($response);
612 }
613
614 list($uploadId, $byteOffset) = self::_chunkedUploadParse200Response($response->body);
615 $len = strlen($data);
616 if ($byteOffset !== $len) {
617 throw new Dropbox_Exception_BadResponse(
618 "We sent $len bytes, but server returned an offset of $byteOffset");
619 }
620
621 return $uploadId;
622 }
623
624 /**
625 * Append another chunk data to a previously-started chunked upload session.
626 *
627 * @param string $uploadId
628 * The unique identifier for the chunked upload session. This is obtained via
629 * {@link chunkedUploadStart}.
630 *
631 * @param int $byteOffset
632 * The number of bytes you think you've already uploaded to the given chunked upload
633 * session. The server will append the new chunk of data after that point.
634 *
635 * @param string $data
636 * The data to append to the existing chunked upload session.
637 *
638 * @param Callable $callback
639 *
640 * @return int|bool
641 * If <code>false</code>, it means the server didn't know about the given
642 * <code>$uploadId</code>. This may be because the chunked upload session has expired
643 * (they last around 24 hours).
644 * If <code>true</code>, the chunk was successfully uploaded. If an integer, it means
645 * you and the server don't agree on the current <code>$byteOffset</code>. The returned
646 * integer is the server's internal byte offset for the chunked upload session. You need
647 * to adjust your input to match.
648 *
649 * @throws Dropbox_Exception
650 */
651 public function chunkedUploadContinue($uploadId, $byteOffset, $data, $callback = null)
652 {
653 Dropbox_Checker::argStringNonEmpty("uploadId", $uploadId);
654 Dropbox_Checker::argNat("byteOffset", $byteOffset);
655 Dropbox_Checker::argString("data", $data);
656
657 $response = $this->_chunkedUpload(
658 array("upload_id" => $uploadId, "offset" => $byteOffset), $data, $callback);
659
660 if ($response->statusCode === 404) {
661 // The server doesn't know our upload ID. Maybe it expired?
662 return false;
663 }
664
665 $correction = self::_chunkedUploadCheckForOffsetCorrection($response);
666 if ($correction !== null) {
667 list($correctedUploadId, $correctedByteOffset) = $correction;
668 if ($correctedUploadId !== $uploadId) {
669 throw new Dropbox_Exception_BadResponse(
670 "Corrective 400 upload_id mismatch: us=".
671 self::q($uploadId)." server=".self::q($correctedUploadId));
672 }
673 if ($correctedByteOffset === $byteOffset) {
674 throw new Dropbox_Exception_BadResponse(
675 "Corrective 400 offset is the same as ours: $byteOffset");
676 }
677
678 return $correctedByteOffset;
679 }
680
681 if ($response->statusCode !== 200) {
682 throw Dropbox_RequestUtil::unexpectedStatus($response);
683 }
684 list($retUploadId, $retByteOffset) = self::_chunkedUploadParse200Response($response->body);
685
686 $nextByteOffset = $byteOffset + strlen($data);
687 if ($uploadId !== $retUploadId) {
688 throw new Dropbox_Exception_BadResponse(
689 "upload_id mismatch: us=".self::q($uploadId).", server=".self::q($uploadId));
690 }
691 if ($nextByteOffset !== $retByteOffset) {
692 throw new Dropbox_Exception_BadResponse(
693 "next-offset mismatch: us=$nextByteOffset, server=$retByteOffset");
694 }
695
696 return true;
697 }
698
699 /**
700 * @param string $body
701 *
702 * @return array
703 */
704 private static function _chunkedUploadParse200Response($body)
705 {
706 $j = Dropbox_RequestUtil::parseResponseJson($body);
707 $uploadId = self::getField($j, "upload_id");
708 $byteOffset = self::getField($j, "offset");
709
710 return array($uploadId, $byteOffset);
711 }
712
713 /**
714 * @param Dropbox_HttpResponse $response
715 *
716 * @return array|null
717 */
718 private static function _chunkedUploadCheckForOffsetCorrection($response)
719 {
720 if ($response->statusCode !== 400) {
721 return null;
722 }
723 $j = json_decode($response->body, true);
724 if ($j === null) {
725 return null;
726 }
727 if (!array_key_exists("upload_id", $j) || !array_key_exists("offset", $j)) {
728 return null;
729 }
730 $uploadId = $j["upload_id"];
731 $byteOffset = $j["offset"];
732
733 return array($uploadId, $byteOffset);
734 }
735
736 /**
737 * Creates a file on Dropbox using the accumulated contents of the given chunked upload session.
738 *
739 * See <a href="https://www.dropbox.com/developers/core/docs#commit-chunked-upload">/commit_chunked_upload</a>.
740 *
741 * @param string $uploadId
742 * The unique identifier for the chunked upload session. This is obtained via
743 * {@link chunkedUploadStart}.
744 *
745 * @param string $path
746 * The Dropbox path to save the file to ($path).
747 *
748 * @param Dropbox_WriteMode $writeMode
749 * What to do if there's already a file at the given path.
750 *
751 * @return array|null
752 * If <code>null</code>, it means the Dropbox server wasn't aware of the
753 * <code>$uploadId</code> you gave it.
754 * Otherwise, you get back the
755 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>
756 * for the newly-created file.
757 *
758 * @throws Dropbox_Exception
759 */
760 public function chunkedUploadFinish($uploadId, $path, $writeMode)
761 {
762 Dropbox_Checker::argStringNonEmpty("uploadId", $uploadId);
763 Dropbox_Path::checkArgNonRoot("path", $path);
764 Dropbox_WriteMode::checkArg("writeMode", $writeMode);
765
766 $params = array_merge(array("upload_id" => $uploadId), $writeMode->getExtraParams());
767
768 $response = $this->doPost(
769 $this->contentHost,
770 $this->appendFilePath("1/commit_chunked_upload", $path),
771 $params);
772
773 if ($response->statusCode === 404) {
774 return null;
775 }
776 if ($response->statusCode !== 200) {
777 throw Dropbox_RequestUtil::unexpectedStatus($response);
778 }
779
780 return Dropbox_RequestUtil::parseResponseJson($response->body);
781 }
782
783 /**
784 * @param array $params
785 * @param string $data
786 * @param callable $callback
787 *
788 * @return Dropbox_HttpResponse
789 */
790 protected function _chunkedUpload($params, $data, $callback = null)
791 {
792 $url = $this->buildUrlForGetOrPut(
793 $this->contentHost, "1/chunked_upload", $params);
794
795 $curl = $this->mkCurl($url);
796
797 // We can't use CURLOPT_PUT because it wants a stream, but we already have $data in memory.
798 $curl->set(CURLOPT_CUSTOMREQUEST, "PUT");
799 $curl->set(CURLOPT_POSTFIELDS, $data);
800 $curl->addHeader("Content-Type: application/octet-stream");
801
802 if ($callback) {
803 $curl->set(CURLOPT_NOPROGRESS, false);
804 $curl->set(CURLOPT_PROGRESSFUNCTION, $callback);
805 }
806
807 $curl->set(CURLOPT_RETURNTRANSFER, true);
808
809 return $curl->exec();
810 }
811
812 /**
813 * Returns the metadata for whatever file or folder is at the given path.
814 *
815 * <code>
816 * $client = ...;
817 * $md = $client->getMetadata("/Photos/Frog.jpeg");
818 * print_r($md);
819 * </code>
820 *
821 * @param string $path
822 * The Dropbox path to a file or folder (UTF-8).
823 *
824 * @return array|null
825 * If there is a file or folder at the given path, you'll get back the
826 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>
827 * for that file or folder. If not, you'll get back <code>null</code>.
828 *
829 * @throws Dropbox_Exception
830 */
831 public function getMetadata($path)
832 {
833 Dropbox_Path::checkArg("path", $path);
834
835 return $this->_getMetadata($path, array("list" => "false"));
836 }
837
838 /**
839 * Returns the metadata for whatever file or folder is at the given path and, if it's a folder,
840 * also include the metadata for all the immediate children of that folder.
841 *
842 * <code>
843 * $client = ...;
844 * $md = $client->getMetadataWithChildren("/Photos");
845 * print_r($md);
846 * </code>
847 *
848 * @param string $path
849 * The Dropbox path to a file or folder (UTF-8).
850 *
851 * @return array|null
852 * If there is a file or folder at the given path, you'll get back the
853 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>
854 * for that file or folder, along with all immediate children if it's a folder. If not,
855 * you'll get back <code>null</code>.
856 *
857 * @throws Dropbox_Exception
858 */
859 public function getMetadataWithChildren($path)
860 {
861 Dropbox_Path::checkArg("path", $path);
862
863 return $this->_getMetadata($path, array("list" => "true", "file_limit" => "25000"));
864 }
865
866 /**
867 * @param string $path
868 * @param array $params
869 *
870 * @return array
871 *
872 * @throws Dropbox_Exception
873 */
874 private function _getMetadata($path, $params)
875 {
876 $response = $this->doGet(
877 $this->apiHost,
878 $this->appendFilePath("1/metadata", $path),
879 $params);
880
881 if ($response->statusCode === 404) {
882 return null;
883 }
884 if ($response->statusCode !== 200) {
885 throw Dropbox_RequestUtil::unexpectedStatus($response);
886 }
887
888 $metadata = Dropbox_RequestUtil::parseResponseJson($response->body);
889 if (array_key_exists("is_deleted", $metadata) && $metadata["is_deleted"]) {
890 return null;
891 }
892
893 return $metadata;
894 }
895
896 /**
897 * If you've previously retrieved the metadata for a folder and its children, this method will
898 * retrieve updated metadata only if something has changed. This is more efficient than
899 * calling {@link getMetadataWithChildren} if you have a cache of previous results.
900 *
901 * <code>
902 * $client = ...;
903 * $md = $client->getMetadataWithChildren("/Photos");
904 * print_r($md);
905 * assert($md["is_dir"], "expecting \"/Photos\" to be a folder");
906 *
907 * sleep(10);
908 *
909 * // Now see if anything changed...
910 * list($changed, $new_md) = $client->getMetadataWithChildrenIfChanged(
911 * "/Photos", $md["hash"]);
912 * if ($changed) {
913 * echo "Folder changed.\n";
914 * print_r($new_md);
915 * } else {
916 * echo "Folder didn't change.\n";
917 * }
918 * </code>
919 *
920 * @param string $path
921 * The Dropbox path to a folder (UTF-8).
922 *
923 * @param string $previousFolderHash
924 * The "hash" field from the previously retrieved folder metadata.
925 *
926 * @return array
927 * A <code>list(boolean $changed, array $metadata)</code>. If the metadata hasn't changed,
928 * you'll get <code>list(false, null)</code>. If the metadata of the folder or any of its
929 * children has changed, you'll get <code>list(true, $newMetadata)</code>. $metadata is a
930 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>.
931 *
932 * @throws Dropbox_Exception
933 */
934 public function getMetadataWithChildrenIfChanged($path, $previousFolderHash)
935 {
936 Dropbox_Path::checkArg("path", $path);
937 Dropbox_Checker::argStringNonEmpty("previousFolderHash", $previousFolderHash);
938
939 $params = array("list" => "true", "file_limit" => "25000", "hash" => $previousFolderHash);
940
941 $response = $this->doGet(
942 $this->apiHost,
943 $this->appendFilePath("1/metadata", $path),
944 $params);
945
946 if ($response->statusCode === 304) {
947 return array(false, null);
948 }
949 if ($response->statusCode === 404) {
950 return array(true, null);
951 }
952 if ($response->statusCode !== 200) {
953 throw Dropbox_RequestUtil::unexpectedStatus($response);
954 }
955
956 $metadata = Dropbox_RequestUtil::parseResponseJson($response->body);
957 if (array_key_exists("is_deleted", $metadata) && $metadata["is_deleted"]) {
958 return array(true, null);
959 }
960
961 return array(true, $metadata);
962 }
963
964 /**
965 * A way of letting you keep up with changes to files and folders in a user's Dropbox.
966 *
967 * @param string|null $cursor
968 * If this is the first time you're calling this, pass in <code>null</code>. Otherwise,
969 * pass in whatever cursor was returned by the previous call.
970 *
971 * @param string|null $pathPrefix
972 * If <code>null</code>, you'll get results for the entire folder (either the user's
973 * entire Dropbox or your App Folder). If you set <code>$path_prefix</code> to
974 * "/Photos/Vacation", you'll only get results for that path and any files and folders
975 * under it.
976 *
977 * @return array
978 * A <a href="https://www.dropbox.com/developers/core/docs#delta">delta page</a>, which
979 * contains a list of changes to apply along with a new "cursor" that should be passed into
980 * future <code>getDelta</code> calls. If the "reset" field is <code>true</code>, you
981 * should clear your local state before applying the changes. If the "has_more" field is
982 * <code>true</code>, call <code>getDelta</code> immediately to get more results, otherwise
983 * wait a while (at least 5 minutes) before calling <code>getDelta</code> again.
984 *
985 * @throws Dropbox_Exception
986 */
987 public function getDelta($cursor = null, $pathPrefix = null)
988 {
989 Dropbox_Checker::argStringNonEmptyOrNull("cursor", $cursor);
990 Dropbox_Path::checkArgOrNull("pathPrefix", $pathPrefix);
991
992 $response = $this->doPost($this->apiHost, "1/delta", array(
993 "cursor" => $cursor,
994 "path_prefix" => $pathPrefix, ));
995
996 if ($response->statusCode !== 200) {
997 throw Dropbox_RequestUtil::unexpectedStatus($response);
998 }
999
1000 return Dropbox_RequestUtil::parseResponseJson($response->body);
1001 }
1002
1003 /**
1004 * Gets the metadata for all the file revisions (up to a limit) for a given path.
1005 *
1006 * See <a href="https://www.dropbox.com/developers/core/docs#revisions">/revisions</a>.
1007 *
1008 * @param string $path
1009 * The Dropbox path that you want file revision metadata for (UTF-8).
1010 *
1011 * @param int|null limit
1012 * The maximum number of revisions to return.
1013 *
1014 * @return array|null
1015 * A list of <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
1016 * objects</a>, one for each file revision. The later revisions appear first in the list.
1017 * If <code>null</code>, then there were too many revisions at that path.
1018 *
1019 * @throws Dropbox_Exception
1020 */
1021 public function getRevisions($path, $limit = null)
1022 {
1023 Dropbox_Path::checkArgNonRoot("path", $path);
1024 Dropbox_Checker::argIntPositiveOrNull("limit", $limit);
1025
1026 $response = $this->doGet(
1027 $this->apiHost,
1028 $this->appendFilePath("1/revisions", $path),
1029 array("rev_limit" => $limit));
1030
1031 if ($response->statusCode === 406) {
1032 return null;
1033 }
1034 if ($response->statusCode !== 200) {
1035 throw Dropbox_RequestUtil::unexpectedStatus($response);
1036 }
1037
1038 return Dropbox_RequestUtil::parseResponseJson($response->body);
1039 }
1040
1041 /**
1042 * Takes a copy of the file at the given revision and saves it over the current copy. This
1043 * will create a new revision, but the file contents will match the revision you specified.
1044 *
1045 * See <a href="https://www.dropbox.com/developers/core/docs#restore">/restore</a>.
1046 *
1047 * @param string $path
1048 * The Dropbox path of the file to restore (UTF-8).
1049 *
1050 * @param string $rev
1051 * The revision to restore the contents to.
1052 *
1053 * @return mixed
1054 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
1055 * object</a>
1056 *
1057 * @throws Dropbox_Exception
1058 */
1059 public function restoreFile($path, $rev)
1060 {
1061 Dropbox_Path::checkArgNonRoot("path", $path);
1062 Dropbox_Checker::argStringNonEmpty("rev", $rev);
1063
1064 $response = $this->doPost(
1065 $this->apiHost,
1066 $this->appendFilePath("1/restore", $path),
1067 array("rev" => $rev));
1068
1069 if ($response->statusCode === 404) {
1070 return null;
1071 }
1072 if ($response->statusCode !== 200) {
1073 throw Dropbox_RequestUtil::unexpectedStatus($response);
1074 }
1075
1076 return Dropbox_RequestUtil::parseResponseJson($response->body);
1077 }
1078
1079 /**
1080 * Returns metadata for all files and folders whose filename matches the query string.
1081 *
1082 * See <a href="https://www.dropbox.com/developers/core/docs#search">/search</a>.
1083 *
1084 * @param string $basePath
1085 * The path to limit the search to (UTF-8). Pass in "/" to search everything.
1086 *
1087 * @param string $query
1088 * A space-separated list of substrings to search for. A file matches only if it contains
1089 * all the substrings.
1090 *
1091 * @param int|null $limit
1092 * The maximum number of results to return.
1093 *
1094 * @param bool $includeDeleted
1095 * Whether to include deleted files in the results.
1096 *
1097 * @return mixed
1098 * A list of <a href="https://www.dropbox.com/developers/core/docs#metadata-details>metadata
1099 * objects</a> of files that match the search query.
1100 *
1101 * @throws Dropbox_Exception
1102 */
1103 public function searchFileNames($basePath, $query, $limit = null, $includeDeleted = false)
1104 {
1105 Dropbox_Path::checkArg("basePath", $basePath);
1106 Dropbox_Checker::argStringNonEmpty("query", $query);
1107 Dropbox_Checker::argNatOrNull("limit", $limit);
1108 Dropbox_Checker::argBool("includeDeleted", $includeDeleted);
1109
1110 $response = $this->doPost(
1111 $this->apiHost,
1112 $this->appendFilePath("1/search", $basePath),
1113 array(
1114 "query" => $query,
1115 "file_limit" => $limit,
1116 "include_deleted" => $includeDeleted,
1117 ));
1118
1119 if ($response->statusCode !== 200) {
1120 throw Dropbox_RequestUtil::unexpectedStatus($response);
1121 }
1122
1123 return Dropbox_RequestUtil::parseResponseJson($response->body);
1124 }
1125
1126 /**
1127 * Creates and returns a public link to a file or folder's "preview page". This link can be
1128 * used without authentication. The preview page may contain a thumbnail or some other
1129 * preview of the file, along with a download link to download the actual file.
1130 *
1131 * See <a href="https://www.dropbox.com/developers/core/docs#shares">/shares</a>.
1132 *
1133 * @param string $path
1134 * The Dropbox path to the file or folder you want to create a shareable link to (UTF-8).
1135 *
1136 * @return string
1137 * The URL of the preview page.
1138 *
1139 * @throws Dropbox_Exception
1140 */
1141 public function createShareableLink($path)
1142 {
1143 Dropbox_Path::checkArg("path", $path);
1144
1145 $response = $this->doPost(
1146 $this->apiHost,
1147 $this->appendFilePath("1/shares", $path),
1148 array(
1149 "short_url" => "false",
1150 ));
1151
1152 if ($response->statusCode === 404) {
1153 return null;
1154 }
1155 if ($response->statusCode !== 200) {
1156 throw Dropbox_RequestUtil::unexpectedStatus($response);
1157 }
1158
1159 $j = Dropbox_RequestUtil::parseResponseJson($response->body);
1160
1161 return self::getField($j, "url");
1162 }
1163
1164 /**
1165 * Creates and returns a direct link to a file. This link can be used without authentication.
1166 * This link will expire in a few hours.
1167 *
1168 * See <a href="https://www.dropbox.com/developers/core/docs#media">/media</a>.
1169 *
1170 * @param string $path
1171 * The Dropbox path to a file or folder (UTF-8).
1172 *
1173 * @return array
1174 * A <code>list(string $url, \DateTime $expires)</code> where <code>$url</code> is a direct
1175 * link to the requested file and <code>$expires</code> is a standard PHP
1176 * <code>\DateTime</code> representing when <code>$url</code> will stop working.
1177 *
1178 * @throws Dropbox_Exception
1179 */
1180 public function createTemporaryDirectLink($path)
1181 {
1182 Dropbox_Path::checkArgNonRoot("path", $path);
1183
1184 $response = $this->doPost(
1185 $this->apiHost,
1186 $this->appendFilePath("1/media", $path));
1187
1188 if ($response->statusCode === 404) {
1189 return null;
1190 }
1191 if ($response->statusCode !== 200) {
1192 throw Dropbox_RequestUtil::unexpectedStatus($response);
1193 }
1194
1195 $j = Dropbox_RequestUtil::parseResponseJson($response->body);
1196 $url = self::getField($j, "url");
1197 $expires = self::parseDateTime(self::getField($j, "expires"));
1198
1199 return array($url, $expires);
1200 }
1201
1202 /**
1203 * Creates and returns a "copy ref" to a file. A copy ref can be used to copy a file across
1204 * different Dropbox accounts without downloading and re-uploading.
1205 *
1206 * For example: Create a <code>Client</code> using the access token from one account and call
1207 * <code>createCopyRef</code>. Then, create a <code>Client</code> using the access token for
1208 * another account and call <code>copyFromCopyRef</code> using the copy ref. (You need to use
1209 * the same app key both times.)
1210 *
1211 * See <a href="https://www.dropbox.com/developers/core/docs#copy_ref">/copy_ref</a>.
1212 *
1213 * @param string $path
1214 * The Dropbox path of the file or folder you want to create a copy ref for (UTF-8).
1215 *
1216 * @return string
1217 * The copy ref (just a string that you keep track of).
1218 *
1219 * @throws Dropbox_Exception
1220 */
1221 public function createCopyRef($path)
1222 {
1223 Dropbox_Path::checkArg("path", $path);
1224
1225 $response = $this->doGet(
1226 $this->apiHost,
1227 $this->appendFilePath("1/copy_ref", $path));
1228
1229 if ($response->statusCode === 404) {
1230 return null;
1231 }
1232 if ($response->statusCode !== 200) {
1233 throw Dropbox_RequestUtil::unexpectedStatus($response);
1234 }
1235
1236 $j = Dropbox_RequestUtil::parseResponseJson($response->body);
1237
1238 return self::getField($j, "copy_ref");
1239 }
1240
1241 /**
1242 * Gets a thumbnail image representation of the file at the given path.
1243 *
1244 * See <a href="https://www.dropbox.com/developers/core/docs#thumbnails">/thumbnails</a>.
1245 *
1246 * @param string $path
1247 * The path to the file you want a thumbnail for (UTF-8).
1248 *
1249 * @param string $format
1250 * One of the two image formats: "jpeg" or "png".
1251 *
1252 * @param string $size
1253 * One of the predefined image size names, as a string:
1254 * <ul>
1255 * <li>"xs" - 32x32</li>
1256 * <li>"s" - 64x64</li>
1257 * <li>"m" - 128x128</li>
1258 * <li>"l" - 640x480</li>
1259 * <li>"xl" - 1024x768</li>
1260 * </ul>
1261 *
1262 * @return array|null
1263 * If the file exists, you'll get <code>list(array $metadata, string $data)</code> where
1264 * <code>$metadata</code> is the file's
1265 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>
1266 * and $data is the raw data for the thumbnail image. If the file doesn't exist, you'll
1267 * get <code>null</code>.
1268 *
1269 * @throws Dropbox_Exception
1270 * @throws InvalidArgumentException
1271 */
1272 public function getThumbnail($path, $format, $size)
1273 {
1274 Dropbox_Path::checkArgNonRoot("path", $path);
1275 Dropbox_Checker::argString("format", $format);
1276 Dropbox_Checker::argString("size", $size);
1277 if (!in_array($format, array("jpeg", "png"))) {
1278 throw new InvalidArgumentException("Invalid 'format': ".self::q($format));
1279 }
1280 if (!in_array($size, array("xs", "s", "m", "l", "xl"))) {
1281 throw new InvalidArgumentException("Invalid 'size': ".self::q($format));
1282 }
1283
1284 $url = $this->buildUrlForGetOrPut(
1285 $this->contentHost,
1286 $this->appendFilePath("1/thumbnails", $path),
1287 array("size" => $size, "format" => $format));
1288
1289 $curl = $this->mkCurl($url);
1290 $metadataCatcher = new Dropbox_DropboxMetadataHeaderCatcher($curl->handle);
1291
1292 $curl->set(CURLOPT_RETURNTRANSFER, true);
1293 $response = $curl->exec();
1294
1295 if ($response->statusCode === 404) {
1296 return null;
1297 }
1298 if ($response->statusCode !== 200) {
1299 throw Dropbox_RequestUtil::unexpectedStatus($response);
1300 }
1301
1302 $metadata = $metadataCatcher->getMetadata();
1303
1304 return array($metadata, $response->body);
1305 }
1306
1307 /**
1308 * Copies a file or folder to a new location
1309 *
1310 * See <a href="https://www.dropbox.com/developers/core/docs#fileops-copy">/fileops/copy</a>.
1311 *
1312 * @param string $fromPath
1313 * The Dropbox path of the file or folder you want to copy (UTF-8).
1314 *
1315 * @param string $toPath
1316 * The destination Dropbox path (UTF-8).
1317 *
1318 * @return mixed
1319 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
1320 * object</a> for the new file or folder.
1321 *
1322 * @throws Dropbox_Exception
1323 */
1324 public function copy($fromPath, $toPath)
1325 {
1326 Dropbox_Path::checkArg("fromPath", $fromPath);
1327 Dropbox_Path::checkArgNonRoot("toPath", $toPath);
1328
1329 $response = $this->doPost(
1330 $this->apiHost,
1331 "1/fileops/copy",
1332 array(
1333 "root" => "auto",
1334 "from_path" => $fromPath,
1335 "to_path" => $toPath,
1336 ));
1337
1338 if ($response->statusCode !== 200) {
1339 throw Dropbox_RequestUtil::unexpectedStatus($response);
1340 }
1341
1342 return Dropbox_RequestUtil::parseResponseJson($response->body);
1343 }
1344
1345 /**
1346 * Creates a file or folder based on an existing copy ref (possibly from a different Dropbox
1347 * account).
1348 *
1349 * See <a href="https://www.dropbox.com/developers/core/docs#fileops-copy">/fileops/copy</a>.
1350 *
1351 * @param string $copyRef
1352 * A copy ref obtained via the {@link createCopyRef()} call.
1353 *
1354 * @param string $toPath
1355 * The Dropbox path you want to copy the file or folder to (UTF-8).
1356 *
1357 * @return mixed
1358 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
1359 * object</a> for the new file or folder.
1360 *
1361 * @throws Dropbox_Exception
1362 */
1363 public function copyFromCopyRef($copyRef, $toPath)
1364 {
1365 Dropbox_Checker::argStringNonEmpty("copyRef", $copyRef);
1366 Dropbox_Path::checkArgNonRoot("toPath", $toPath);
1367
1368 $response = $this->doPost(
1369 $this->apiHost,
1370 "1/fileops/copy",
1371 array(
1372 "root" => "auto",
1373 "from_copy_ref" => $copyRef,
1374 "to_path" => $toPath,
1375 )
1376 );
1377
1378 if ($response->statusCode !== 200) {
1379 throw Dropbox_RequestUtil::unexpectedStatus($response);
1380 }
1381
1382 return Dropbox_RequestUtil::parseResponseJson($response->body);
1383 }
1384
1385 /**
1386 * Creates a folder.
1387 *
1388 * See <a href="https://www.dropbox.com/developers/core/docs#fileops-create-folder">/fileops/create_folder</a>.
1389 *
1390 * @param string $path
1391 * The Dropbox path at which to create the folder (UTF-8).
1392 *
1393 * @return array|null
1394 * If successful, you'll get back the
1395 * <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata object</a>
1396 * for the newly-created folder. If not successful, you'll get <code>null</code>.
1397 *
1398 * @throws Dropbox_Exception
1399 */
1400 public function createFolder($path)
1401 {
1402 Dropbox_Path::checkArgNonRoot("path", $path);
1403
1404 $response = $this->doPost(
1405 $this->apiHost,
1406 "1/fileops/create_folder",
1407 array(
1408 "root" => "auto",
1409 "path" => $path,
1410 ));
1411
1412 if ($response->statusCode === 403) {
1413 return null;
1414 }
1415 if ($response->statusCode !== 200) {
1416 throw Dropbox_RequestUtil::unexpectedStatus($response);
1417 }
1418
1419 return Dropbox_RequestUtil::parseResponseJson($response->body);
1420 }
1421
1422 /**
1423 * Deletes a file or folder
1424 *
1425 * See <a href="https://www.dropbox.com/developers/core/docs#fileops-delete">/fileops/delete</a>.
1426 *
1427 * @param string $path
1428 * The Dropbox path of the file or folder to delete (UTF-8).
1429 *
1430 * @return mixed
1431 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
1432 * object</a> for the deleted file or folder.
1433 *
1434 * @throws Dropbox_Exception
1435 */
1436 public function delete($path)
1437 {
1438 Dropbox_Path::checkArgNonRoot("path", $path);
1439
1440 $response = $this->doPost(
1441 $this->apiHost,
1442 "1/fileops/delete",
1443 array(
1444 "root" => "auto",
1445 "path" => $path,
1446 ));
1447
1448 if ($response->statusCode !== 200) {
1449 throw Dropbox_RequestUtil::unexpectedStatus($response);
1450 }
1451
1452 return Dropbox_RequestUtil::parseResponseJson($response->body);
1453 }
1454
1455 /**
1456 * Moves a file or folder to a new location.
1457 *
1458 * See <a href="https://www.dropbox.com/developers/core/docs#fileops-move">/fileops/move</a>.
1459 *
1460 * @param string $fromPath
1461 * The source Dropbox path (UTF-8).
1462 *
1463 * @param string $toPath
1464 * The destination Dropbox path (UTF-8).
1465 *
1466 * @return mixed
1467 * The <a href="https://www.dropbox.com/developers/core/docs#metadata-details">metadata
1468 * object</a> for the destination file or folder.
1469 *
1470 * @throws Dropbox_Exception
1471 */
1472 public function move($fromPath, $toPath)
1473 {
1474 Dropbox_Path::checkArgNonRoot("fromPath", $fromPath);
1475 Dropbox_Path::checkArgNonRoot("toPath", $toPath);
1476
1477 $response = $this->doPost(
1478 $this->apiHost,
1479 "1/fileops/move",
1480 array(
1481 "root" => "auto",
1482 "from_path" => $fromPath,
1483 "to_path" => $toPath,
1484 ));
1485
1486 if ($response->statusCode !== 200) {
1487 throw Dropbox_RequestUtil::unexpectedStatus($response);
1488 }
1489
1490 return Dropbox_RequestUtil::parseResponseJson($response->body);
1491 }
1492
1493 /**
1494 * Build a URL for making a GET or PUT request. Will add the "locale"
1495 * parameter.
1496 *
1497 * @param $host
1498 * Either the "API" or "API content" hostname from {@link getHost()}.
1499 * @param $path
1500 * The "path" part of the URL. For example, "/account/info".
1501 * @param null $params
1502 * URL parameters. For POST requests, do not put the parameters here.
1503 * Include them in the request body instead.
1504 *
1505 * @return string
1506 */
1507 public function buildUrlForGetOrPut($host, $path, $params = null)
1508 {
1509 return Dropbox_RequestUtil::buildUrlForGetOrPut($this->userLocale, $host, $path, $params);
1510 }
1511
1512 /**
1513 * Perform an OAuth-2-authorized GET request to the Dropbox API. Will automatically
1514 * fill in "User-Agent" and "locale" as well.
1515 *
1516 * @param string $host
1517 * Either the "API" or "API content" hostname from {@link getHost()}.
1518 * @param string $path
1519 * The "path" part of the URL. For example, "/account/info".
1520 * @param array|null $params
1521 * GET parameters.
1522 *
1523 * @return Dropbox_HttpResponse
1524 *
1525 * @throws Dropbox_Exception
1526 */
1527 public function doGet($host, $path, $params = null)
1528 {
1529 Dropbox_Checker::argString("host", $host);
1530 Dropbox_Checker::argString("path", $path);
1531
1532 return Dropbox_RequestUtil::doGet($this->clientIdentifier, $this->accessToken, $this->userLocale,
1533 $host, $path, $params);
1534 }
1535
1536 /**
1537 * Perform an OAuth-2-authorized POST request to the Dropbox API. Will automatically
1538 * fill in "User-Agent" and "locale" as well.
1539 *
1540 * @param string $host
1541 * Either the "API" or "API content" hostname from {@link getHost()}.
1542 * @param string $path
1543 * The "path" part of the URL. For example, "/commit_chunked_upload".
1544 * @param array|null $params
1545 * POST parameters.
1546 *
1547 * @return Dropbox_HttpResponse
1548 *
1549 * @throws Dropbox_Exception
1550 */
1551 public function doPost($host, $path, $params = null)
1552 {
1553 Dropbox_Checker::argString("host", $host);
1554 Dropbox_Checker::argString("path", $path);
1555
1556 return Dropbox_RequestUtil::doPost($this->clientIdentifier, $this->accessToken, $this->userLocale,
1557 $host, $path, $params);
1558 }
1559
1560 /**
1561 * Create a {@link Curl} object that is pre-configured with {@link getClientIdentifier()},
1562 * and the proper OAuth 2 "Authorization" header.
1563 *
1564 * @param string $url
1565 * Generate this URL using {@link buildUrl()}.
1566 *
1567 * @return Dropbox_Curl
1568 */
1569 public function mkCurl($url)
1570 {
1571 return Dropbox_RequestUtil::mkCurlWithOAuth($this->clientIdentifier, $url, $this->accessToken);
1572 }
1573
1574 /**
1575 * Parses date/time strings returned by the Dropbox API. The Dropbox API returns date/times
1576 * formatted like: <code>"Sat, 21 Aug 2010 22:31:20 +0000"</code>.
1577 *
1578 * @param string $apiDateTimeString
1579 * A date/time string returned by the API.
1580 *
1581 * @return \DateTime
1582 * A standard PHP <code>\DateTime</code> instance.
1583 *
1584 * @throws Dropbox_Exception_BadResponse
1585 * Thrown if <code>$apiDateTimeString</code> isn't correctly formatted.
1586 */
1587 public static function parseDateTime($apiDateTimeString)
1588 {
1589 $dt = DateTime::createFromFormat(self::$dateTimeFormat, $apiDateTimeString);
1590 if ($dt === false) {
1591 throw new Dropbox_Exception_BadResponse(
1592 "Bad date/time from server: ".self::q($apiDateTimeString));
1593 }
1594
1595 return $dt;
1596 }
1597
1598 private static $dateTimeFormat = "D, d M Y H:i:s T";
1599
1600 /**
1601 * @internal
1602 */
1603 public static function q($object)
1604 {
1605 return var_export($object, true);
1606 }
1607
1608 /**
1609 * @internal
1610 */
1611 public static function getField($j, $fieldName)
1612 {
1613 if (!array_key_exists($fieldName, $j)) {
1614 throw new Dropbox_Exception_BadResponse(
1615 "missing field \"$fieldName\" in ".self::q($j));
1616 }
1617
1618 return $j[$fieldName];
1619 }
1620
1621 /**
1622 * Given an OAuth 2 access token, returns <code>null</code> if it is well-formed (though
1623 * not necessarily valid). Otherwise, returns a string describing what's wrong with it.
1624 *
1625 * @param string $s
1626 *
1627 * @return string
1628 */
1629 public static function getAccessTokenError($s)
1630 {
1631 if ($s === null) {
1632 return "can't be null";
1633 }
1634 if (strlen($s) === 0) {
1635 return "can't be empty";
1636 }
1637
1638 // if (preg_match('@[^-=_~/A-Za-z0-9\.\+]@', $s) === 1) return "contains invalid character";
1639 return null;
1640 }
1641
1642 /**
1643 * @internal
1644 */
1645 public static function checkAccessTokenArg($argName, $accessToken)
1646 {
1647 $error = self::getAccessTokenError($accessToken);
1648 if ($error !== null) {
1649 throw new InvalidArgumentException("'$argName' invalid: $error");
1650 }
1651 }
1652
1653 /**
1654 * @internal
1655 */
1656 public static function getClientIdentifierError($s)
1657 {
1658 if ($s === null) {
1659 return "can't be null";
1660 }
1661 if (strlen($s) === 0) {
1662 return "can't be empty";
1663 }
1664 if (preg_match('@[\x00-\x1f\x7f]@', $s) === 1) {
1665 return "contains control character";
1666 }
1667
1668 return null;
1669 }
1670
1671 /**
1672 * @internal
1673 */
1674 public static function checkClientIdentifierArg($argName, $accessToken)
1675 {
1676 $error = self::getClientIdentifierError($accessToken);
1677 if ($error !== null) {
1678 throw new InvalidArgumentException("'$argName' invalid: $error");
1679 }
1680 }
1681 }
1682