PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.7
Backup Migration v2.1.7
2.1.7 2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / external / dropbox.php
backup-backup / includes / external Last commit date
contracts 2 weeks ago backupbliss.php 2 weeks ago controller.php 2 weeks ago dropbox.php 2 weeks ago external-storage-manager.php 2 weeks ago ftp.php 2 weeks ago google-drive.php 2 weeks ago s3.php 2 weeks ago
dropbox.php
1084 lines
1 <?php
2
3 namespace BMI\Plugin\External;
4
5
6 // Exit on direct access
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 use BMI\Plugin\BMI_Logger as Logger;
12 use BMI\Plugin\Dashboard as Dashboard;
13 use BMI\Plugin\Backup_Migration_Plugin as BMP;
14 use BMI\Plugin\Scanner\BMI_BackupsScanner as Backups;
15 use BMI\Plugin\External\Contracts\DeleteBackup;
16
17 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'external' . DIRECTORY_SEPARATOR . 'contracts' . DIRECTORY_SEPARATOR . 'interface-delete-backup.php';
18
19 /**
20 * BMI_External_Dropbox
21 *
22 * This class is responsible for handling all Dropbox related operations
23 */
24
25 class BMI_External_Dropbox implements DeleteBackup
26 {
27 public $dropboxId = 'bmip_dropbox';
28 public $dropboxAuthCodeOption = 'bmip_dropbox_auth_code';
29 public $dropboxAccessToken = 'bmip_dropbox_access_token';
30 public $dropboxApiUrl = 'https://api.dropboxapi.com/2/';
31 public $dropboxContentUrl = 'https://content.dropboxapi.com/2/';
32
33 public function __construct()
34 {
35 add_action('bmi_premium_remove_backup_file', [&$this, 'deleteBackup']);
36 add_action('bmi_premium_remove_backup_json_file', [&$this, 'deleteDropboxBackupJson']);
37 add_action('delete_transient_bmip_dropbox_issue', [&$this, 'deleteDropboxIssue']);
38 }
39
40 public function deleteDropboxIssue()
41 {
42 delete_option('bmip_dropbox_correct_offset');
43 delete_option('bmip_dropbox_required_space');
44 delete_option('bmip_dropbox_dismiss_issue');
45 }
46
47 /**
48 * request make a request to Dropbox API using cURL
49 * @param string $endpoint
50 * @param array|string $params
51 * @param array $headers
52 * @param string $format "rpc" or "content" for different Dropbox API endpoints
53 * @param array $loggingData data to be logged
54 * @return string response from the request or "error" if error
55 */
56 public function request($endpoint, $params = array(), $headers = array(), $format = 'rpc', $loggingData = array())
57 {
58
59 $accessToken = get_transient($this->dropboxAccessToken);
60
61 if (get_transient('bmip_dropbox_issue') == 'auth_error' || !$accessToken) {
62 $accessToken = $this->configureAccessToken(get_transient('bmip_dropbox_issue') == 'auth_error' && $accessToken);
63 if ($accessToken !== false && get_transient('bmip_dropbox_issue') == 'auth_error') delete_transient('bmip_dropbox_issue');
64 }
65
66
67 if (!$accessToken) {
68 if (in_array(get_transient('bmip_dropbox_issue'), ['auth_error', false])) {
69 set_transient('bmip_dropbox_issue', 'auth_error_disconnected');
70 }
71 return "error";
72 }
73
74
75 $headers[] = 'Authorization: Bearer ' . $accessToken;
76
77
78 $ch = curl_init();
79 $apiUrl = $format == 'rpc' ? $this->dropboxApiUrl : $this->dropboxContentUrl;
80 $timeout = $format == 'rpc' ? 100 : 300;
81 curl_setopt($ch, CURLOPT_URL, $apiUrl . $endpoint);
82 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
83 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
84 curl_setopt($ch, CURLOPT_POST, true);
85 if ($params) {
86 curl_setopt($ch, CURLOPT_POSTFIELDS, is_string($params) ? $params : json_encode($params));
87 }
88
89 // @see https://stackoverflow.com/questions/35031236/could-not-access-dropbox-api-via-parse-cloud-code-although-works-with-curl
90 if ($endpoint == 'users/get_space_usage') {
91 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(null));
92 }
93 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
94 curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
95
96
97 $response = curl_exec($ch);
98
99 if (curl_errno($ch)) {
100 $error_message = curl_error($ch);
101 Logger::error('[BMI PRO] Something went wrong with cURL request: ' . $error_message);
102 return 'error';
103 }
104
105 $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
106 $retryAfter = BMP::getRetryAfterIfAvailable($ch, $response);
107
108 if (is_resource($ch)) {
109 curl_close($ch);
110 }
111
112 $data = array(
113 'response' => $response,
114 'code' => $code,
115 'loggingData' => $loggingData,
116 'retryAfter' => $retryAfter
117 );
118 $this->afterProcess($data);
119
120 if (!in_array($code, [200, 206])) {
121 return 'error';
122 }
123
124 return $response;
125 }
126
127 /**
128 * createFolder create a folder in Dropbox
129 * @param string $folderName full path of the folder
130 * @return false|string false if error, folder id if success
131 */
132 public function createFolder($folderName)
133 {
134 $params = array(
135 'path' => '/' . $folderName,
136 'autorename' => false
137 );
138
139 $getFolderMeta = $this->getFileMeta($folderName); // Avoid already existing folder error
140 if ($getFolderMeta) {
141 return $getFolderMeta['id'];
142 }
143
144 $headers = array(
145 'Content-Type: application/json'
146 );
147
148 $response = $this->request('files/create_folder_v2', $params, $headers);
149
150 if ($response === 'error') {
151 return false;
152 }
153
154 $response = json_decode($response, true);
155
156 return $response['metadata']['id'];
157 }
158
159 /**
160 * getFileMeta get the file id of a file in Dropbox
161 * @param string $fileName name of file or file id of the file in Dropbox
162 * @return false|array false if error, array of metadata if success
163 */
164 public function getFileMeta($fileName)
165 {
166 $isId = (substr($fileName, 0, 3) == 'id:');
167 if (!$isId && $fileName[0] != '/') {
168 $fileName = '/' . $fileName;
169 }
170
171 $params = array(
172 'path' => $fileName
173 );
174
175 $headers = array(
176 'Content-Type: application/json'
177 );
178
179 $response = $this->request('files/get_metadata', $params, $headers);
180
181 if ($response === 'error') {
182 return false;
183 }
184
185 $response = json_decode($response, true);
186
187 return $response;
188 }
189
190 /**
191 * deleteFile delete a file/folder in Dropbox
192 * if it is a folder, all files inside the folder will be deleted
193 * @param string $fileName name of file or file id of the file in Dropbox
194 * @return bool true if success, false if error
195 */
196 public function deleteFile($fileName)
197 {
198 $isId = (substr($fileName, 0, 3) == 'id:');
199 if (!$isId && $fileName[0] != '/') {
200 $fileName = '/' . $fileName;
201 }
202
203 $params = array(
204 'path' => $fileName
205 );
206
207 $headers = array(
208 'Content-Type: application/json'
209 );
210
211 $response = $this->request('files/delete_v2', $params, $headers);
212
213 if ($response === 'error') {
214 return false;
215 }
216
217 return true;
218 }
219
220 /**
221 * download get the content of a file in Dropbox
222 * @param string $fileName name of file or file id of the file in Dropbox
223 * @param string $range range of the file to download (optional) e.g. '0-100' for first 100 bytes
224 * @return false|string false if error, string response if success (content of the file)
225 */
226 public function getFileContent($fileName, $range = '')
227 {
228 $isId = (substr($fileName, 0, 3) == 'id:');
229 if (!$isId && $fileName[0] != '/') {
230 $fileName = '/' . $fileName;
231 }
232
233 $headers = array(
234 'Dropbox-API-Arg: {"path": "' . $fileName . '"}',
235 'Content-Type: text/plain'
236 );
237
238 if ($range) {
239 $headers[] = 'Range: bytes=' . $range;
240 }
241
242 $response = $this->request('files/download', array(), $headers, 'content');
243
244 if ($response === 'error') {
245 return false;
246 }
247
248 return $response;
249 }
250
251 /**
252 * listFiles list all files in a folder in Dropbox
253 * @return false|array[] false if error, array of entries if success
254 * @see https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
255 */
256 public function listFiles()
257 {
258
259 $params = array(
260 'path' => '',
261 'include_non_downloadable_files' => false
262 );
263
264 $headers = array(
265 'Content-Type: application/json'
266 );
267
268 $response = $this->request('files/list_folder', $params, $headers);
269
270 if ($response === 'error') {
271 return false;
272 }
273
274 $response = json_decode($response, true);
275
276 $entries = $response['entries'];
277
278 if (isset($response['has_more']) && $response['has_more'] === true) { // In Almost all cases, this will be false
279 $cursor = $response['cursor'];
280 $entries = array_merge($entries, $this->listFilesContinue($cursor));
281 }
282
283 return $entries;
284 }
285
286 private function listFilesContinue($cursor)
287 {
288 $params = array(
289 'cursor' => $cursor
290 );
291
292 $headers = array(
293 'Content-Type: application/json'
294 );
295
296 $response = $this->request('files/list_folder/continue', $params, $headers);
297
298 if ($response === 'error') {
299 return false;
300 }
301
302 $files = [];
303 $response = json_decode($response, true);
304
305 foreach ($response['entries'] as $entry) {
306 $files[] = $entry['name'];
307 }
308
309 if (isset($response['has_more']) && $response['has_more'] === true) {
310 $cursor = $response['cursor'];
311 $files = array_merge($files, $this->listFilesContinue($cursor));
312 }
313
314 return $files;
315 }
316
317
318 /**
319 * startUploadSession start an upload session in Dropbox
320 *
321 * @return false|string false if error, session id if success
322 */
323 public function startUploadSession()
324 {
325
326 $header = array(
327 'Dropbox-API-Arg: {"close": false}',
328 'Content-Type: application/octet-stream'
329 );
330
331 $response = $this->request('files/upload_session/start', array(), $header, 'content');
332
333 if ($response === 'error') {
334 return false;
335 }
336
337 $response = json_decode($response, true);
338
339 return $response['session_id'];
340 }
341
342 /**
343 * uploadChunk upload a chunk of a file to Dropbox using upload session
344 *
345 * @param string $sessionId valid session id
346 * @param string $filePath full path of the file
347 * @param int $offset offset of the file
348 * @return false|int false if error, size of the chunk uploaded if success
349 */
350 public function uploadChunk($sessionId, $filePath, $offset, $maxRetries = 3)
351 {
352 if (!file_exists($filePath)) {
353 Logger::error('[BMI PRO] File not found: ' . $filePath);
354 return false;
355 }
356
357 $fileSize = filesize($filePath);
358 $availableMemory = BMP::getAvailableMemoryInBytes();
359
360 if (($availableMemory / 4) < 4194304) {
361 $response = [
362 'error_summary' => 'not_enough_memory'
363 ];
364 $this->errorHandler($response, 500);
365 Logger::error('[BMI PRO] Not enough memory to upload file: ' . $filePath);
366 return false;
367 }
368
369 $chunkSize = min($availableMemory / 4, 10485760); // Max 10MB
370 $chunkSize = $chunkSize - ($chunkSize % 4194304); // Round down to nearest multiple of 4MB
371
372 $retryCount = 0;
373 while ($retryCount < $maxRetries) {
374 if ($offset + $chunkSize > $fileSize) {
375 $chunkSize = $fileSize - $offset;
376 }
377
378 $header = array(
379 'Dropbox-API-Arg: {"cursor": {"session_id": "' . $sessionId . '", "offset": ' . $offset . '}, "close": ' . ($offset + $chunkSize == $fileSize ? 'true' : 'false') . '}',
380 'Content-Type: application/octet-stream'
381 );
382
383 if (($stream = fopen($filePath, 'r')) && $offset < $fileSize) {
384 fseek($stream, $offset);
385 $chunk = fread($stream, $chunkSize);
386 fclose($stream);
387 } else {
388 Logger::error('[BMI PRO] Could not open file: ' . $filePath);
389 return false;
390 }
391
392 $response = $this->request('files/upload_session/append_v2', $chunk, $header, 'content');
393
394 if ($response === 'error') {
395 $issue = get_transient('bmip_dropbox_issue');
396 if ($issue == 'incorrect_offset') {
397 $correctOffset = get_option('bmip_dropbox_correct_offset', false);
398 if ($correctOffset) {
399 $offset = $correctOffset;
400 delete_option('bmip_dropbox_correct_offset');
401 $retryCount++;
402 continue;
403 }
404 }
405 return false;
406 }
407
408 return $offset + $chunkSize;
409 }
410 Logger::error('[BMI PRO] Max retries reached for uploading chunk');
411 return false;
412 }
413
414
415 /**
416 * finishUpload finish the upload session of a file in Dropbox
417 *
418 * @param string $sessionId valid session id
419 * @param string $filePathOnDropbox full path of the file
420 * @param int $offset offset of the file
421 * @return false|string false if error, file id if success
422 */
423 public function finishUpload($sessionId, $filePathOnDropbox, $offset)
424 {
425 $filePathOnDropbox = '/' . basename($filePathOnDropbox);
426
427 $header = array(
428 'Dropbox-API-Arg: {"cursor": {"session_id": "' . $sessionId . '", "offset": ' . $offset . '}, "commit": {"path": "' . $filePathOnDropbox . '", "mode": "add", "autorename": true, "mute": false}}',
429 'Content-Type: application/octet-stream'
430 );
431
432 $loggingData = array(
433 'fileSize' => $offset
434 );
435
436 $response = $this->request('files/upload_session/finish', array(), $header, 'content', $loggingData);
437
438 if ($response === 'error') {
439 return false;
440 }
441
442 $response = json_decode($response, true);
443
444 return $response['id'];
445 }
446
447 /**
448 * uploadFile upload a file to Dropbox
449 * used for files less than 10MB
450 *
451 * @param string $filePath full path of the file
452 * @return false|string false if error, file id if success
453 */
454 public function uploadFile($filePath)
455 {
456 if (file_exists($filePath)) {
457 $fileSize = filesize($filePath);
458 } else {
459 Logger::error('[BMI PRO] File not found: ' . $filePath);
460 return false;
461 }
462
463 if ($fileSize > 10485760) { // 10MB
464 Logger::error('[BMI PRO] File size is greater than 10MB: ' . $filePath);
465 return false;
466 }
467
468 $filePathOnDropbox = '/' . basename($filePath);
469
470 $header = array(
471 'Dropbox-API-Arg: {"path": "' . $filePathOnDropbox . '"}',
472 'Content-Type: application/octet-stream'
473 );
474
475 if ($stream = fopen($filePath, 'r')) {
476 $params = stream_get_contents($stream);
477 fclose($stream);
478 } else {
479 Logger::error('[BMI PRO] Could not open file: ' . $filePath);
480 return false;
481 }
482
483 $loggingData = array(
484 'fileSize' => $fileSize
485 );
486
487 $response = $this->request('files/upload', $params, $header, 'content', $loggingData);
488
489 if ($response === 'error') {
490 return false;
491 }
492
493 $response = json_decode($response, true);
494
495 return $response['id'];
496
497 }
498
499 /**
500 * afterProcess handle the response of a request to Dropbox API
501 * if success, clear any previous issues
502 * if error, handle the error response
503 *
504 * @param array $data response data from request in format ['code' => int, 'response' => string, 'retryAfter' => int, 'loggingData' => array]
505 * @return void
506 */
507 public function afterProcess($data)
508 {
509 $code = $data['code'];
510 $response = json_decode($data['response'], true);
511 $retryAfter = $data['retryAfter'];
512 $fileSize = isset($data['loggingData']['fileSize']) ? $data['loggingData']['fileSize'] : null;
513 if (!in_array($code, [200, 206])) {
514 $this->errorHandler($response, $code, $retryAfter, $fileSize);
515 }
516
517 }
518
519 /**
520 * errorHandler handle the error response of a request to Dropbox API
521 *
522 * @param array $response response data
523 * @param int $code response code
524 * @param int $retryAfter retry after time
525 * @param int $fileSize file size (optional) for insufficient space error
526 * @return void
527 * @see https://www.dropbox.com/developers/documentation/http/documentation#error-handling
528 */
529 public function errorHandler($response, $code, $retryAfter = HOUR_IN_SECONDS, $fileSize = null)
530 {
531 switch ($code) {
532 case 401:
533 Logger::debug('[BMI PRO] Unauthorized access to Dropbox API: ' . json_encode($response));
534 set_transient('bmip_dropbox_issue', 'auth_error', HOUR_IN_SECONDS);
535 break;
536 case 403:
537 Logger::debug('[BMI PRO] Forbidden access to Dropbox API: ' . json_encode($response));
538 set_transient('bmip_dropbox_issue', 'forbidden', HOUR_IN_SECONDS);
539 break;
540 case 500:
541 Logger::debug('[BMI PRO] Internal server error: ' . json_encode($response));
542 set_transient('bmip_dropbox_issue', 'internal_error', HOUR_IN_SECONDS);
543 break;
544 case 429:
545 Logger::debug('[BMI PRO] Too many requests to Dropbox API. Retry after: ' . $retryAfter);
546 set_transient('bmip_dropbox_issue', 'rate_limit', $retryAfter);
547 break;
548 case 409:
549 Logger::debug('[BMI PRO] Conflict in Dropbox API: ' . json_encode($response));
550 if (isset($response['error_summary']) && strpos($response['error_summary'], 'incorrect_offset') !== false) {
551 set_transient('bmip_dropbox_issue', 'incorrect_offset', HOUR_IN_SECONDS);
552 $correctOffset = false;
553 if (isset($response['error']['correct_offset'])) $correctOffset = $response['error']['correct_offset'];
554 elseif (isset($response['error']['lookup_failed']['correct_offset'])) $correctOffset = $response['error']['lookup_failed']['correct_offset'];
555 if ($correctOffset !== false && is_int($correctOffset)) update_option('bmip_dropbox_correct_offset', $correctOffset);
556 }
557 if (isset($response['error_summary']) && strpos($response['error_summary'], 'path_lookup') !== false || strpos($response['error_summary'], 'path_not_found') !== false || strpos($response['error_summary'], 'folder_not_found') !== false) {
558 set_transient('bmip_dropbox_issue', 'path_lookup', HOUR_IN_SECONDS);
559 }
560 if (isset($response['error_summary']) && strpos($response['error_summary'], 'insufficient_space') !== false) {
561 set_transient('bmip_dropbox_issue', 'insufficient_space', HOUR_IN_SECONDS);
562 if ($fileSize) {
563 update_option('bmip_dropbox_required_space', $fileSize);
564 }
565 }
566 break;
567 default:
568 Logger::debug('[BMI PRO] Unknown error in Dropbox API: ' . (is_string($response) ? $response : json_encode($response)));
569 break;
570 }
571 update_option('bmip_dropbox_dismiss_issue', false);
572 }
573
574 /**
575 * getParsedFiles get the list of files in Dropbox folder and their metadata (JSON files)
576 *
577 * @return array[]|bool compact array of zip files and json files if success, false if error
578 * format: ['zipFilesName' => ['filename.zip' => ['id' => 'file_id', 'size' => 'file_size']], 'jsonFilesPath' => ['path_lower']]
579 */
580 public function getParsedFiles()
581 {
582 $files = $this->listFiles();
583 if ($files === false) return false;
584 $zipFilesName = [];
585 $jsonFilesPath = [];
586 foreach ($files as $file) {
587 $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
588 if (in_array($ext, ['zip', 'tar', 'gz'])) {
589 $zipFilesName[$file['name']] = ['id' => $file['id'], 'size' => $file['size']];
590 } else if (strpos($file['name'], '.json') !== false) {
591 $jsonFilesPath[] = $file['path_lower'][0] == '/' ? substr($file['path_lower'], 1) : $file['path_lower'];
592 }
593
594 }
595 return compact('zipFilesName', 'jsonFilesPath');
596 }
597
598 /**
599 * getAvailableSpace get the space usage of Dropbox account
600 *
601 * @return false|array false if error, array of space usage if success
602 */
603 public function getSpaceUsage()
604 {
605
606 $header = array(
607 'Content-Type: application/json'
608 );
609
610 $response = $this->request('users/get_space_usage', array(), $header);
611
612 if ($response === 'error') {
613 return false;
614 }
615
616 $response = json_decode($response, true);
617
618 return $response;
619 }
620
621
622 /************************************************************************************************************* */
623 /********************* DELETE DROPBOX BACKUP **************************************************************** */
624 /************************************************************************************************************* */
625
626 /**
627 * @inheritDoc
628 */
629 public function deleteBackup($md5){
630 if ($this->verifyConnection()['result'] != 'connected') {
631 return false;
632 }
633
634 $manifestFile = $md5 . '.json';
635 if (file_exists(BMI_BACKUPS . DIRECTORY_SEPARATOR . $manifestFile)) {
636 $manifestContent = json_decode(file_get_contents(BMI_BACKUPS . DIRECTORY_SEPARATOR . $manifestFile), true);
637 } else {
638 $manifestContent = json_decode($this->getFileContent('/' . $manifestFile), true);
639 }
640 if ($manifestContent == false) {
641 return false;
642 }
643 $backupName = $manifestContent['name'];
644 $deleteManifest = $this->deleteFile($manifestFile);
645 $deleteZip = $this->deleteFile('/' . $backupName);
646 if ($deleteManifest && $deleteZip) {
647 return true;
648 }
649 return false;
650 }
651
652 /**
653 * @deprecated Use deleteBackup() instead.
654 */
655 public function deleteDropboxBackup($md5) {
656 return $this->deleteBackup($md5);
657 }
658
659 /**
660 * @deprecated Use deleteBackup() instead.
661 */
662 public function deleteDropboxBackupJson($manifestFile){
663 if ($this->verifyConnection()['result'] != 'connected') {
664 return false;
665 }
666
667 $deleteManifest = $this->deleteFile('/' . $manifestFile);
668 if ($deleteManifest) {
669 return true;
670 }
671 return false;
672 }
673
674
675
676 /************************************************************************************************************* */
677 /********************* Dropbox Plugin Functions ************************************************************ */
678 /************************************************************************************************************* */
679
680
681 /**
682 * uploadDropboxBackup - Uploads a backup to Dropbox
683 * @param string $sessionId - session id of the upload
684 * @param string $backupName - name of the backup to upload
685 * @param int $offset - offset of the file to upload
686 * @param string $md5 - md5 hash of the backup to get the manifest file
687 * @return array explain the status of the upload process in format
688 * [
689 * 'status' => 'finished' | 'error' | 'continue',
690 * (status == 'continue') ? 'offset' => int : null,
691 * (status == 'error') ? 'error' => 'internal_file_not_found' | 'not_enough_memory' | 'could_not_start_session' | 'could_not_upload_chunk' | 'could_not_finish_upload' | 'could_not_upload_backup_in_one_go' | 'insufficient_space' | 'could_not_upload_manifest' : null
692 * ]
693 */
694 public function uploadDropboxBackup($sessionId, $backupName, $offset, $md5)
695 {
696 $backupPath = BMI_BACKUPS . DIRECTORY_SEPARATOR . $backupName;
697 $manifestPath = BMI_BACKUPS . DIRECTORY_SEPARATOR . $md5 . '.json';
698
699 if (!file_exists($backupPath) || !file_exists($manifestPath)) {
700 Logger::error('[BMI PRO] File not found: ' . $backupName);
701 return [
702 'status' => 'error',
703 'error' => 'internal_file_not_found'
704 ];
705 }
706
707 $spaceUsage = $this->getSpaceUsage();
708 if ($spaceUsage === false) {
709 return [
710 'status' => 'error',
711 'error' => 'could_not_get_space_usage'
712 ];
713 }
714 $availableSpace = $spaceUsage['allocation']['allocated'] - $spaceUsage['used'];
715
716 if ($availableSpace < filesize($backupPath)) {
717 Logger::error('[BMI PRO] Not enough space to upload file: ' . $backupName);
718 update_option('bmip_dropbox_dismiss_issue', false);
719 update_option('bmip_dropbox_required_space', filesize($backupPath));
720 set_transient('bmip_dropbox_issue', 'insufficient_space', HOUR_IN_SECONDS);
721 return [
722 'status' => 'error',
723 'error' => 'insufficient_space'
724 ];
725 }
726
727 if ($sessionId == '') {
728 $fileSize = filesize($backupPath);
729 $availableMemory = BMP::getAvailableMemoryInBytes();
730
731 if (($availableMemory / 4) < 4194304) {
732 Logger::error('[BMI PRO] Not enough memory to upload file: ' . $backupName);
733 update_option('bmip_dropbox_dismiss_issue', false);
734 set_transient('bmip_dropbox_issue', 'not_enough_memory', HOUR_IN_SECONDS);
735 return [
736 'status' => 'error',
737 'error' => 'not_enough_memory'
738 ];
739 }
740
741 if (($availableMemory / 4) <= $fileSize && $fileSize < 10485760) {
742 $uploadResult = $this->uploadFile($backupPath);
743 if ($uploadResult) {
744 $manifestUploadResult = $this->uploadFile($manifestPath);
745 if ($manifestUploadResult) return ['status' => 'finished'];
746 else return ['status' => 'error', 'error' => 'could_not_upload_manifest'];
747 } else {
748 return ['status' => 'error', 'error' => 'could_not_upload_backup_in_one_go'];
749 }
750 }
751 if ($sessionId == ''){
752 $sessionId = $this->startUploadSession();
753 }
754 if ($sessionId === false) {
755 return [
756 'status' => 'error',
757 'error' => 'could_not_start_session'
758 ];
759 }
760 return [
761 'status' => 'continue',
762 'offset' => 0,
763 'sessionId' => $sessionId
764 ];
765 } else {
766 if ($offset < filesize($backupPath)){
767 $newOffset = $this->uploadChunk($sessionId, $backupPath, $offset);
768 if ($newOffset === false) {
769 return [
770 'status' => 'error',
771 'error' => 'could_not_upload_chunk'
772 ];
773 }
774 return [
775 'status' => 'continue',
776 'offset' => $newOffset
777 ];
778 } else {
779 $fileId = $this->finishUpload($sessionId, $backupPath, $offset);
780 if ($fileId) {
781 $manifestUploadResult = $this->uploadFile($manifestPath);
782 if ($manifestUploadResult) return ['status' => 'success'];
783 else return ['status' => 'error', 'error' => 'could_not_upload_manifest'];
784 } else {
785 return ['status' => 'error', 'error' => 'could_not_finish_upload'];
786 }
787 }
788
789 }
790
791 }
792
793 /**
794 * checkForBackupsToUploadToDropbox - Checks for backups to upload to Dropbox
795 * update bmip_to_be_uploaded option with the list of backups to upload
796 *
797 * @return array explain the status of the upload process in format
798 * [
799 * 'status' => 'success'
800 * ]
801 */
802 public function checkForBackupsToUploadToDropbox() {
803
804 $isEnabled = Dashboard\bmi_get_config('STORAGE::EXTERNAL::DROPBOX');
805 if (!($isEnabled === true || $isEnabled === 'true')) {
806 return ['status' => 'not_enabled'];
807 }
808
809 $requiresUpload = get_option('bmip_to_be_uploaded', [
810 'current_upload' => [],
811 'queue' => [],
812 'failed' => []
813 ]);
814
815 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'scanner' . DIRECTORY_SEPARATOR . 'backups.php';
816 $backups = Backups::getInstance();
817 $backupsAvailable = $backups->getAvailableBackups("local");
818 $localBackups = $backupsAvailable['local'];
819 $parsedDropboxFiles = $this->getParsedFiles();
820 if($parsedDropboxFiles === false) return ['status' => 'error'];
821 $backupsFileName = isset($parsedDropboxFiles['zipFilesName']) ? $parsedDropboxFiles['zipFilesName'] : [];
822 $manifestFilesPath = isset($parsedDropboxFiles['jsonFilesPath']) ? $parsedDropboxFiles['jsonFilesPath'] : [];
823 $availableManifests = array_map(function($path) {
824 return pathinfo($path, PATHINFO_FILENAME);
825 }, $manifestFilesPath);
826 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
827
828
829
830 foreach($localBackups as $name => $details) {
831 $md5 = $details[7];
832 if (isset($uploadedBackupStatus[$md5]) && isset($uploadedBackupStatus[$md5]['dropbox'])) {
833 continue;
834 }
835 $isBackupNotExists = !in_array($md5, $availableManifests) || !in_array($name, array_keys($backupsFileName));
836 if ($isBackupNotExists && !(isset($requiresUpload['current_upload']['task']) && $requiresUpload['current_upload']['task'] == 'dropbox_' . $md5)) {
837 $requiresUpload['queue']['dropbox_' . $md5] = [
838 'name' => $name,
839 'md5' => $details[7],
840 'json' => $details[7] . '.json'
841 ];
842 }
843 }
844
845 update_option('bmip_to_be_uploaded', $requiresUpload);
846 return ['status' => 'success'];
847 }
848
849 /**
850 * restartUpload - Restarts the upload process of a backup to Dropbox.
851 *
852 * This function clears the current and failed Dropbox uploads, then checks for any backups that need to be uploaded again. (Instead of deleting uploads option for all external storages)
853 *
854 * @return array explain the status of the upload process in format
855 * [
856 * 'status' => 'success'
857 * ]
858 */
859 public function restartUploadprocess() {
860 $requiredToUpload = get_option('bmip_to_be_uploaded', [
861 'current_upload' => [],
862 'queue' => [],
863 'failed' => []
864 ]);
865
866 if (isset($requiredToUpload['current_upload']['task']) && strpos($requiredToUpload['current_upload']['task'], 'dropbox') !== false) {
867 unset($requiredToUpload['current_upload']);
868 }
869
870 foreach ($requiredToUpload['failed'] as $key => $value) {
871 if (strpos($key, 'dropbox') !== false) {
872 unset($requiredToUpload['failed'][$key]);
873 }
874 }
875
876 update_option('bmip_to_be_uploaded', $requiredToUpload);
877 return $this->checkForBackupsToUploadToDropbox();
878 }
879
880
881
882 /************************************************************************************************************* */
883 /********************* Dropbox Authorization Functions ***************************************************** */
884 /************************************************************************************************************* */
885
886 /**
887 * configureAccessToken - Configures the access token for Dropbox
888 *
889 * @param bool $forceGetNewAccessToken - force to get a new access token
890 *
891 * @return string|bool access token if success, false if error
892 */
893 public function configureAccessToken($forceGetNewAccessToken = false)
894 {
895 $uri = home_url();
896 if (substr($uri, 0, 4) != 'http') {
897 if (is_ssl()) $uri = 'https://' . home_url();
898 else $uri = 'http://' . home_url();
899 }
900 $authorizationCode = get_option($this->dropboxAuthCodeOption, '');
901 $dropboxId = get_option($this->dropboxId, '');
902 $issue = get_transient('bmip_dropbox_issue');
903
904 $url = 'https://authentication.backupbliss.com/v1/dropbox/token';
905 $response = wp_remote_post($url, array(
906 'method' => 'POST',
907 'timeout' => 15,
908 'redirection' => 2,
909 'httpversion' => '1.0',
910 'blocking' => true,
911 'body' => array(
912 'client_id' => $authorizationCode,
913 'site_token' => $dropboxId,
914 'force_refresh' => $forceGetNewAccessToken,
915 'redirect_uri' => $uri
916 )
917 ));
918
919 if (is_wp_error($response)) {
920 $error_message = $response->get_error_message();
921 Logger::error('[BMI PRO] Something went wrong during getting dropbox token:' . $error_message);
922 return false;
923 } else {
924 $result = json_decode($response['body']);
925 if (isset($result->expiration) && isset($result->access_token)) {
926 $expiresInSeconds = intval($result->expiration) - intval(microtime(true));
927 $accessToken = $result->access_token;
928 set_transient($this->dropboxAccessToken, $accessToken, $expiresInSeconds);
929 if (in_array($issue, ['auth_error', 'auth_error_disconnected'])) delete_transient('bmip_dropbox_issue');
930 return $accessToken;
931 }
932 if ($issue == 'auth_error') set_transient('bmip_dropbox_issue', 'auth_error_disconnected');
933 return false;
934 }
935 }
936
937
938 /**
939 * verifyDropboxConnection - Checks if the Dropbox is still granted and tokens are not expired
940 *
941 * @param bool $forceGetNewAccessToken - force to get a new access token
942 *
943 * @return array explain the status of the connection in format
944 * [
945 * 'status' => 'success' | 'error',
946 * 'result' => 'connected' | 'disconnected'
947 * ]
948 */
949 public function verifyConnection( $forceGetNewAccessToken = false ) {
950
951 $tempKeyDropboxFile = BMI_TMP . DIRECTORY_SEPARATOR . 'dropboxKeys.php';
952 if (file_exists($tempKeyDropboxFile)) {
953 $dropboxKeys = file_get_contents($tempKeyDropboxFile);
954 if (strpos($dropboxKeys, "\n") !== false) {
955 $lines = explode("\n", $dropboxKeys);
956 if (sizeof($lines) == 4) {
957 $dropboxId = substr($lines[1], 2);
958 $dropboxAuthCode = substr($lines[2], 2);
959 if (function_exists('wp_load_alloptions')) {
960 wp_load_alloptions(true);
961 }
962 delete_option($this->dropboxId);
963 delete_option($this->dropboxAuthCodeOption);
964 if (function_exists('wp_load_alloptions')) {
965 wp_load_alloptions(true);
966 }
967 update_option($this->dropboxId, $dropboxId);
968 update_option($this->dropboxAuthCodeOption, $dropboxAuthCode);
969 }
970 }
971 if (strpos(site_url(), 'tastewp') !== false) {
972 if (function_exists('wp_load_alloptions')) {
973 wp_load_alloptions(true);
974 }
975
976 update_option('__tastewp_redirection_performed', true);
977 update_option('auto_smart_tastewp_redirect_performed', 1);
978 update_option('tastewp_auto_activated', true);
979 update_option('__tastewp_sub_requested', true);
980 }
981
982 unlink($tempKeyDropboxFile);
983 }
984
985 $baseurl = home_url();
986 if (substr($baseurl, 0, 4) != 'http') {
987 if (is_ssl()) $baseurl = 'https://' . home_url();
988 else $baseurl = 'http://' . home_url();
989 }
990
991 $dropboxAuthCode = get_option($this->dropboxAuthCodeOption, '');
992 $dropboxId = get_option($this->dropboxId, '');
993 $currentAccessToken = get_transient($this->dropboxAccessToken);
994 $issue = get_transient('bmip_dropbox_issue');
995
996
997 $url = 'https://authentication.backupbliss.com/v1/dropbox/verify';
998 $response = wp_remote_post($url, array(
999 'method' => 'POST',
1000 'timeout' => 15,
1001 'redirection' => 2,
1002 'httpversion' => '1.0',
1003 'blocking' => true,
1004 'body' => array(
1005 'client_id' => $dropboxAuthCode,
1006 'site_token' => $dropboxId,
1007 'force_refresh' => $forceGetNewAccessToken || ($issue == 'auth_error' && $currentAccessToken),
1008 'redirect_uri' => $baseurl
1009 )
1010 ));
1011
1012 $res = 'disconnected';
1013 if (is_wp_error($response)) {
1014 $error_message = $response->get_error_message();
1015 Logger::error('[BMI PRO] Something went wrong during Dropbox connection verification:' . $error_message);
1016 return [ 'status' => 'error', 'result' => 'disconnected' ];
1017 } else {
1018 $result = json_decode($response['body']);
1019 if (isset($result->status)) {
1020 if (isset($result->expiration) && isset($result->access_token)) {
1021 $expiresInSeconds = intval($result->expiration) - intval(microtime(true));
1022 $accessToken = $result->access_token;
1023 set_transient($this->dropboxAccessToken, $accessToken, $expiresInSeconds);
1024 }
1025 if ($result->status == 'disconnected' && BMI_DEBUG) {
1026 Logger::error('[BMI PRO] Dropbox connection is disconnected in order to this response: ' . json_encode($result));
1027 }
1028 if ($result->status == 'disconnected') $res = 'disconnected';
1029 if ($result->status == 'connected') $res = 'connected';
1030 if ($result->status == 'error') $res = 'disconnected';
1031 }
1032
1033 if ($res == 'disconnected' && $issue == 'auth_error') set_transient('bmip_dropbox_issue', 'auth_error_disconnected');
1034 else if ($res == 'connected' && in_array($issue, ['auth_error', 'auth_error_disconnected'])) delete_transient('bmip_dropbox_issue');
1035 return [ 'status' => 'success', 'result' => $res ];
1036 }
1037
1038 }
1039
1040
1041 /**
1042 * disconnect - Removes the Dropbox connection
1043 *
1044 * @return array explain the status of the connection in format
1045 * [
1046 * 'status' => 'success' | 'error'
1047 * ]
1048 */
1049 public function disconnect() {
1050 $baseurl = home_url();
1051 if (substr($baseurl, 0, 4) != 'http') {
1052 if (is_ssl()) $baseurl = 'https://' . home_url();
1053 else $baseurl = 'http://' . home_url();
1054 }
1055
1056 $dropboxAuthCode = get_option($this->dropboxAuthCodeOption, '');
1057 $dropboxId = get_option($this->dropboxId, '');
1058
1059 $url = 'https://authentication.backupbliss.com/v1/dropbox/disconnect';
1060 $response = wp_remote_post($url, array(
1061 'method' => 'POST',
1062 'timeout' => 15,
1063 'redirection' => 2,
1064 'httpversion' => '1.0',
1065 'blocking' => true,
1066 'body' => array(
1067 'client_id' => $dropboxAuthCode,
1068 'site_token' => $dropboxId,
1069 'redirect_uri' => $baseurl
1070 )
1071 ));
1072
1073 if (is_wp_error($response)) {
1074 $error_message = $response->get_error_message();
1075 Logger::error('[BMI PRO] Something went wrong during Dropbox removal process:' . $error_message);
1076 return [ 'status' => 'error' ];
1077 }
1078
1079 return [ 'status' => 'success' ];
1080
1081 }
1082
1083 }
1084