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