PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.22.19
UpdraftPlus: WP Backup & Migration Plugin v1.22.19
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / methods / dropbox.php

dropbox.php in UpdraftPlus: WP Backup & Migration Plugin 1.22.19, at methods/dropbox.php

1,018 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * https://www.dropbox.com/developers/apply?cont=/developers/apps
4 */
5
6 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.');
7
8 // Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes
9
10 if (!class_exists('UpdraftPlus_BackupModule')) require_once(UPDRAFTPLUS_DIR.'/methods/backup-module.php');
11
12 // Fix a potential problem for users who had the short-lived 1.12.35-1.12.38 free versions (see: https://wordpress.org/support/topic/1-12-37-dropbox-auth-broken/page/2/#post-8981457)
13 // Can be removed after a few months
14 $potential_options = UpdraftPlus_Options::get_updraft_option('updraft_dropbox');
15 if (is_array($potential_options) && isset($potential_options['version']) && isset($potential_options['settings']) && array() === $potential_options['settings']) {
16 // Wipe it, which will force its re-creation in proper format
17 UpdraftPlus_Options::delete_updraft_option('updraft_dropbox');
18 }
19
20 class UpdraftPlus_BackupModule_dropbox extends UpdraftPlus_BackupModule {
21
22 private $current_file_hash;
23
24 private $current_file_size;
25
26 private $uploaded_offset;
27
28 private $upload_tick;
29
30 /**
31 * This callback is called as upload progress is made
32 *
33 * @param Integer $offset - the byte offset
34 * @param String $uploadid - identifier for the upload in progress
35 * @param Boolean|String $fullpath - optional full path to the file being uploaded
36 */
37 public function chunked_callback($offset, $uploadid, $fullpath = false) {
38
39 global $updraftplus;
40
41 $storage = $this->get_storage();
42
43 // Update upload ID
44 $this->jobdata_set('upload_id_'.$this->current_file_hash, $uploadid);
45 $this->jobdata_set('upload_offset_'.$this->current_file_hash, $offset);
46
47 $time_now = microtime(true);
48
49 $time_since_last_tick = $time_now - $this->upload_tick;
50 $data_since_last_tick = $offset - $this->uploaded_offset;
51
52 $this->upload_tick = $time_now;
53 $this->uploaded_offset = $offset;
54
55 // Here we use job-wide data, because we don't expect wildly different performance for different Dropbox accounts
56 $chunk_size = $updraftplus->jobdata_get('dropbox_chunk_size', 1048576);
57 // Don't go beyond 10MB, or change the chunk size after the last segment
58 if ($chunk_size < 10485760 && $this->current_file_size > 0 && $offset < $this->current_file_size) {
59 $job_run_time = $time_now - $updraftplus->job_time_ms;
60 if ($time_since_last_tick < 10) {
61 $upload_rate = $data_since_last_tick / max($time_since_last_tick, 1);
62 $upload_secs = min(floor($job_run_time), 10);
63 if ($job_run_time < 15) $upload_secs = max(6, $job_run_time*0.6);
64 $new_chunk = max(min($upload_secs * $upload_rate * 0.9, 10485760), 1048576);
65 $new_chunk = $new_chunk - ($new_chunk % 524288);
66 $chunk_size = (int) $new_chunk;
67 $storage->setChunkSize($chunk_size);
68 $updraftplus->jobdata_set('dropbox_chunk_size', $chunk_size);
69 }
70 }
71
72 if ($this->current_file_size > 0) {
73 $percent = round(100*($offset/$this->current_file_size), 1);
74 $updraftplus->record_uploaded_chunk($percent, "$uploadid, $offset, ".round($chunk_size/1024, 1)." KB", $fullpath);
75 } else {
76 $this->log("Chunked Upload: $offset bytes uploaded");
77 // This act is done by record_uploaded_chunk, and helps prevent overlapping runs
78 if ($fullpath) touch($fullpath);
79 }
80 }
81
82 /**
83 * Supported features
84 *
85 * @return Array
86 */
87 public function get_supported_features() {
88 // This options format is handled via only accessing options via $this->get_options()
89 return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic', 'manual_authentication');
90 }
91
92 /**
93 * Default options
94 *
95 * @return Array
96 */
97 public function get_default_options() {
98 return array(
99 'appkey' => '',
100 'secret' => '',
101 'folder' => '',
102 'tk_access_token' => '',
103 );
104 }
105
106 /**
107 * Check whether options have been set up by the user, or not
108 *
109 * @param Array $opts - the potential options
110 *
111 * @return Boolean
112 */
113 public function options_exist($opts) {
114 if (is_array($opts) && !empty($opts['tk_access_token'])) return true;
115 return false;
116 }
117
118 /**
119 * Acts as a WordPress options filter
120 *
121 * @param Array $dropbox - An array of Dropbox options
122 * @return Array - the returned array can either be the set of updated Dropbox settings or a WordPress error array
123 */
124 public function options_filter($dropbox) {
125
126 // Get the current options (and possibly update them to the new format)
127 $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('dropbox');
128
129 if (is_wp_error($opts)) {
130 if ('recursion' !== $opts->get_error_code()) {
131 $msg = "(".$opts->get_error_code()."): ".$opts->get_error_message();
132 $this->log($msg);
133 error_log("UpdraftPlus: $msg");
134 }
135 // The saved options had a problem; so, return the new ones
136 return $dropbox;
137 }
138
139 // If the input is not as expected, then return the current options
140 if (!is_array($dropbox)) return $opts;
141
142 // Remove instances that no longer exist
143 foreach ($opts['settings'] as $instance_id => $storage_options) {
144 if (!isset($dropbox['settings'][$instance_id])) unset($opts['settings'][$instance_id]);
145 }
146
147 // Dropbox has a special case where the settings could be empty so we should check for this before
148 if (!empty($dropbox['settings'])) {
149
150 foreach ($dropbox['settings'] as $instance_id => $storage_options) {
151 if (!empty($opts['settings'][$instance_id]['tk_access_token'])) {
152
153 $current_app_key = empty($opts['settings'][$instance_id]['appkey']) ? false : $opts['settings'][$instance_id]['appkey'];
154 $new_app_key = empty($storage_options['appkey']) ? false : $storage_options['appkey'];
155
156 // If a different app key is being used, then wipe the stored token as it cannot belong to the new app
157 if ($current_app_key !== $new_app_key) {
158 unset($opts['settings'][$instance_id]['tk_access_token']);
159 unset($opts['settings'][$instance_id]['ownername']);
160 unset($opts['settings'][$instance_id]['CSRF']);
161 }
162
163 }
164
165 // Now loop over the new options, and replace old options with them
166 foreach ($storage_options as $key => $value) {
167 if (null === $value) {
168 unset($opts['settings'][$instance_id][$key]);
169 } else {
170 if (!isset($opts['settings'][$instance_id])) $opts['settings'][$instance_id] = array();
171 $opts['settings'][$instance_id][$key] = $value;
172 }
173 }
174
175 if (!empty($opts['settings'][$instance_id]['folder']) && preg_match('#^https?://(www.)dropbox\.com/home/Apps/UpdraftPlus(.Com)?([^/]*)/(.*)$#i', $opts['settings'][$instance_id]['folder'], $matches)) $opts['settings'][$instance_id]['folder'] = $matches[3];
176
177 // check if we have the dummy nosave option and remove it so that it doesn't get saved
178 if (isset($opts['settings'][$instance_id]['dummy-nosave'])) unset($opts['settings'][$instance_id]['dummy-nosave']);
179 }
180
181 }
182
183 return $opts;
184 }
185
186 public function backup($backup_array) {
187
188 global $updraftplus;
189
190 $opts = $this->get_options();
191
192 if (empty($opts['tk_access_token'])) {
193 $this->log('You do not appear to be authenticated with Dropbox (1)');
194 $this->log(__('You do not appear to be authenticated with Dropbox', 'updraftplus'), 'error');
195 return false;
196 }
197
198 // 28 September 2017: APIv1 is gone. We'll keep the variable to make life easier if there's ever an APIv3.
199 $use_api_ver = 2;
200
201 if (empty($opts['tk_request_token'])) {
202 $this->log("begin cloud upload (using API version $use_api_ver with OAuth v2 token)");
203 } else {
204 $this->log("begin cloud upload (using API version $use_api_ver with OAuth v1 token)");
205 }
206
207 $chunk_size = $updraftplus->jobdata_get('dropbox_chunk_size', 1048576);
208
209 try {
210 $dropbox = $this->bootstrap();
211 if (false === $dropbox) throw new Exception(__('You do not appear to be authenticated with Dropbox', 'updraftplus'));
212 $this->log("access gained; setting chunk size to: ".round($chunk_size/1024, 1)." KB");
213 $dropbox->setChunkSize($chunk_size);
214 } catch (Exception $e) {
215 $this->log('error when trying to gain access: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
216 $this->log(sprintf(__('error: %s (see log file for more)', 'updraftplus'), $e->getMessage()), 'error');
217 return false;
218 }
219
220 $updraft_dir = $updraftplus->backups_dir_location();
221
222 foreach ($backup_array as $file) {
223
224 $available_quota = -1;
225
226 // If we experience any failures collecting account info, then carry on anyway
227 try {
228
229 /*
230 Quota information is no longer provided with account information a new call to quotaInfo must be made to get this information.
231 */
232 $quota_info = $dropbox->quotaInfo();
233
234 // Access token expired try to refresh and then call quota info again
235 if ("401" == $quota_info['code']) {
236 $this->log('HTTP code 401 (unauthorized) code returned from Dropbox; attempting to refresh access token');
237 $dropbox->refreshAccessToken();
238 $quota_info = $dropbox->quotaInfo();
239 }
240
241 if ("200" != $quota_info['code']) {
242 $message = "account/info did not return HTTP 200; returned: ". $quota_info['code'];
243 } elseif (!isset($quota_info['body'])) {
244 $message = "account/info did not return the expected data";
245 } else {
246 $body = $quota_info['body'];
247 if (isset($body->quota_info)) {
248 $quota_info = $body->quota_info;
249 $total_quota = $quota_info->quota;
250 $normal_quota = $quota_info->normal;
251 $shared_quota = $quota_info->shared;
252 $available_quota = $total_quota - ($normal_quota + $shared_quota);
253 $message = "quota usage: normal=".round($normal_quota/1048576, 1)." MB, shared=".round($shared_quota/1048576, 1)." MB, total=".round($total_quota/1048576, 1)." MB, available=".round($available_quota/1048576, 1)." MB";
254 } else {
255 $total_quota = max($body->allocation->allocated, 1);
256 $used = $body->used;
257 /* check here to see if the account is a team account and if so use the other used value
258 This will give us their total usage including their individual account and team account */
259 if (isset($body->allocation->used)) $used = $body->allocation->used;
260 $available_quota = $total_quota - $used;
261 $message = "quota usage: used=".round($used/1048576, 1)." MB, total=".round($total_quota/1048576, 1)." MB, available=".round($available_quota/1048576, 1)." MB";
262 }
263 }
264 $this->log($message);
265 } catch (Exception $e) {
266 $this->log("exception (".get_class($e).") occurred whilst getting account info: ".$e->getMessage());
267 // $this->log(sprintf(__("%s error: %s", 'updraftplus'), 'Dropbox', $e->getMessage()).' ('.$e->getCode().')', 'warning', md5($e->getMessage()));
268 }
269
270 $file_success = 1;
271
272 $hash = md5($file);
273 $this->current_file_hash = $hash;
274
275 $filesize = filesize($updraft_dir.'/'.$file);
276 $this->current_file_size = $filesize;
277
278 // Into KB
279 $filesize = $filesize/1024;
280 $microtime = microtime(true);
281
282 if ('None' != ($upload_id = $this->jobdata_get('upload_id_'.$hash, 'None', 'updraf_dbid_'.$hash))) {
283 // Resume
284 $offset = $this->jobdata_get('upload_offset_'.$hash, 0, 'updraf_dbof_'.$hash);
285 if ($offset) $this->log("This is a resumption: $offset bytes had already been uploaded");
286 } else {
287 $offset = 0;
288 $upload_id = 'None';
289 }
290
291 // We don't actually abort now - there's no harm in letting it try and then fail
292 if (-1 != $available_quota && $available_quota < ($filesize-$offset)) {
293 $this->log("File upload expected to fail: file data remaining to upload ($file) size is ".($filesize-$offset)." b (overall file size; .".($filesize*1024)." b), whereas available quota is only $available_quota b");
294 // $this->log(sprintf(__("Account full: your %s account has only %d bytes left, but the file to be uploaded has %d bytes remaining (total size: %d bytes)",'updraftplus'),'Dropbox', $available_quota, $filesize-$offset, $filesize), 'warning');
295 }
296
297 $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this);
298
299 $this->log("Attempt to upload: $file to: $ufile");
300
301 $this->upload_tick = microtime(true);
302 $this->uploaded_offset = $offset;
303
304 try {
305 $response = $dropbox->chunkedUpload($updraft_dir.'/'.$file, '', $ufile, true, $offset, $upload_id, array($this, 'chunked_callback'));
306 if (empty($response['code']) || "200" != $response['code']) {
307 $this->log('Unexpected HTTP code returned from Dropbox: '.$response['code']." (".serialize($response).")");
308 if ($response['code'] >= 400) {
309 if (401 == $response['code']) {
310 $this->log('HTTP code 401 returned from Dropbox, refreshing access token');
311 $dropbox->refreshAccessToken();
312 }
313 $this->log(sprintf(__('error: failed to upload file to %s (see log file for more)', 'updraftplus'), $file), 'error');
314 $file_success = 0;
315 } else {
316 $this->log(__('did not return the expected response - check your log file for more details', 'updraftplus'), 'warning');
317 }
318 }
319 } catch (Exception $e) {
320 $this->log("chunked upload exception (".get_class($e)."): ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
321 if (preg_match("/Submitted input out of alignment: got \[(\d+)\] expected \[(\d+)\]/i", $e->getMessage(), $matches)) {
322 // Try the indicated offset
323 $we_tried = $matches[1];
324 $dropbox_wanted = (int) $matches[2];
325 $this->log("not yet aligned: tried=$we_tried, wanted=$dropbox_wanted; will attempt recovery");
326 $this->uploaded_offset = $dropbox_wanted;
327 $upload_id = $this->jobdata_get('upload_id_'.$hash, 'None', 'updraf_dbid_'.$hash);
328 try {
329 $dropbox->chunkedUpload($updraft_dir.'/'.$file, '', $ufile, true, $dropbox_wanted, $upload_id, array($this, 'chunked_callback'));
330 } catch (Exception $e) {
331 $msg = $e->getMessage();
332 if (preg_match('/Upload with upload_id .* already completed/', $msg)) {
333 $this->log('returned an error, but apparently indicating previous success: '.$msg);
334 } else {
335 $this->log($msg.' (line: '.$e->getLine().', file: '.$e->getFile().')');
336 $this->log(sprintf(__('failed to upload file to %s (see log file for more)', 'updraftplus'), $ufile), 'error');
337 $file_success = 0;
338 if (strpos($msg, 'select/poll returned error') !== false && $this->upload_tick > 0 && time() - $this->upload_tick > 800) {
339 UpdraftPlus_Job_Scheduler::reschedule(60);
340 $this->log("Select/poll returned after a long time: scheduling a resumption and terminating for now");
341 UpdraftPlus_Job_Scheduler::record_still_alive();
342 die;
343 }
344 }
345 }
346 } else {
347 $msg = $e->getMessage();
348 if (preg_match('/Upload with upload_id .* already completed/', $msg)) {
349 $this->log('returned an error, but apparently indicating previous success: '.$msg);
350 } else {
351 $this->log(sprintf(__('failed to upload file to %s (see log file for more)', 'updraftplus'), $ufile), 'error');
352 $file_success = 0;
353 if (strpos($msg, 'select/poll returned error') !== false && $this->upload_tick > 0 && time() - $this->upload_tick > 800) {
354 UpdraftPlus_Job_Scheduler::reschedule(60);
355 $this->log("Select/poll returned after a long time: scheduling a resumption and terminating for now");
356 UpdraftPlus_Job_Scheduler::record_still_alive();
357 die;
358 }
359 }
360 }
361 }
362 if ($file_success) {
363 $updraftplus->uploaded_file($file);
364 $microtime_elapsed = microtime(true)-$microtime;
365 $speedps = ($microtime_elapsed > 0) ? $filesize/$microtime_elapsed : 0;
366 $speed = sprintf("%.2d", $filesize)." KB in ".sprintf("%.2d", $microtime_elapsed)."s (".sprintf("%.2d", $speedps)." KB/s)";
367 $this->log("File upload success (".$file."): $speed");
368 $this->jobdata_delete('upload_id_'.$hash, 'updraf_dbid_'.$hash);
369 $this->jobdata_delete('upload_offset_'.$hash, 'updraf_dbof_'.$hash);
370 }
371
372 }
373
374 return null;
375
376 }
377
378 /**
379 * This method gets a list of files from the remote stoage that match the string passed in and returns an array of backups
380 *
381 * @param String $match a substring to require (tested via strpos() !== false)
382 * @return Array
383 */
384 public function listfiles($match = 'backup_') {
385
386 $opts = $this->get_options();
387
388 if (empty($opts['tk_access_token'])) return new WP_Error('no_settings', __('No settings were found', 'updraftplus').' (dropbox)');
389
390 try {
391 $dropbox = $this->bootstrap();
392 } catch (Exception $e) {
393 $this->log('access error: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
394 return new WP_Error('access_error', $e->getMessage());
395 }
396
397 $searchpath = '/'.untrailingslashit(apply_filters('updraftplus_dropbox_modpath', '', $this));
398
399 try {
400 /* Some users could have a large amount of backups, the max search is 1000 entries we should continue to search until there are no more entries to bring back. */
401 $cursor = '';
402 $matches = array();
403
404 while (true) {
405 $search = $dropbox->search($match, $searchpath, 1000, $cursor);
406 if (empty($search['code']) || 200 != $search['code']) return new WP_Error('response_error', sprintf(__('%s returned an unexpected HTTP response: %s', 'updraftplus'), 'Dropbox', $search['code']), $search['body']);
407
408 if (empty($search['body'])) return array();
409
410 if (isset($search['body']->matches) && is_array($search['body']->matches)) {
411 $matches = array_merge($matches, $search['body']->matches);
412 } elseif (is_array($search['body'])) {
413 $matches = $search['body'];
414 } else {
415 break;
416 }
417
418 if (isset($search['body']->has_more) && true == $search['body']->has_more && isset($search['body']->cursor)) {
419 $cursor = $search['body']->cursor;
420 } else {
421 break;
422 }
423 }
424
425 } catch (Exception $e) {
426 $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
427 // The most likely cause of a search_error is specifying a non-existent path, which should just result in an empty result set.
428 // return new WP_Error('search_error', $e->getMessage());
429 return array();
430 }
431
432 $results = array();
433
434 foreach ($matches as $item) {
435 $item = $item->metadata;
436 if (!is_object($item)) continue;
437 if (isset($item->metadata)) $item = $item->metadata; // 2/files/search_v2 has a slightly different output structure compared to 2/files/search model
438
439 if ((!isset($item->size) || $item->size > 0) && 'folder' != $item->{'.tag'} && !empty($item->path_display) && 0 === strpos($item->path_display, $searchpath)) {
440
441 $path = substr($item->path_display, strlen($searchpath));
442 if ('/' == substr($path, 0, 1)) $path = substr($path, 1);
443
444 // Ones in subfolders are not wanted
445 if (false !== strpos($path, '/')) continue;
446
447 $result = array('name' => $path);
448 if (!empty($item->size)) $result['size'] = $item->size;
449
450 $results[] = $result;
451 }
452 }
453
454 return $results;
455 }
456
457 /**
458 * Identification of Dropbox app
459 *
460 * @return Array
461 */
462 private function defaults() {
463 return apply_filters('updraftplus_dropbox_defaults', array('Z3Q3ZmkwbnplNHA0Zzlx', 'bTY0bm9iNmY4eWhjODRt'));
464 }
465
466 /**
467 * Delete files from the service using the Dropbox API
468 *
469 * @param Array $files - array of filenames to delete
470 * @param Array $data - unused here
471 * @param Array $sizeinfo - unused here
472 * @return Boolean|String - either a boolean true or an error code string
473 */
474 public function delete($files, $data = null, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $data and $sizeinfo unused
475
476 if (is_string($files)) $files = array($files);
477
478 $opts = $this->get_options();
479
480 if (empty($opts['tk_access_token'])) {
481 $this->log('You do not appear to be authenticated with Dropbox (3)');
482 $this->log(sprintf(__('You do not appear to be authenticated with %s (whilst deleting)', 'updraftplus'), 'Dropbox'), 'warning');
483 return 'authentication_fail';
484 }
485
486 try {
487 $dropbox = $this->bootstrap();
488 } catch (Exception $e) {
489 $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
490 $this->log(sprintf(__('Failed to access %s when deleting (see log file for more)', 'updraftplus'), 'Dropbox'), 'warning');
491 return 'service_unavailable';
492 }
493 if (false === $dropbox) return false;
494
495 $any_failures = false;
496
497 foreach ($files as $file) {
498 $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this);
499 $this->log("request deletion: $ufile");
500
501 try {
502 $dropbox->delete($ufile);
503 $file_success = 1;
504 } catch (Exception $e) {
505 $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
506 }
507
508 if (isset($file_success)) {
509 $this->log('deletion succeeded');
510 } else {
511 $this->log('deletion failed');
512 $any_failures = true;
513 }
514 }
515
516 return $any_failures ? 'file_delete_error' : true;
517
518 }
519
520 public function download($file) {
521
522 global $updraftplus;
523
524 $opts = $this->get_options();
525
526 if (empty($opts['tk_access_token'])) {
527 $this->log('You do not appear to be authenticated with Dropbox (4)');
528 $this->log(sprintf(__('You do not appear to be authenticated with %s', 'updraftplus'), 'Dropbox'), 'error');
529 return false;
530 }
531
532 try {
533 $dropbox = $this->bootstrap();
534 } catch (Exception $e) {
535 $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
536 $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')', 'error');
537 return false;
538 }
539 if (false === $dropbox) return false;
540
541 $remote_files = $this->listfiles($file);
542
543 foreach ($remote_files as $file_info) {
544 if ($file_info['name'] == $file) {
545 return $updraftplus->chunked_download($file, $this, $file_info['size'], apply_filters('updraftplus_dropbox_downloads_manually_break_up', false), null, 2*1048576);
546 }
547 }
548
549 $this->log("$file: file not found in listing of remote directory");
550
551 return false;
552 }
553
554 /**
555 * Callback used by by chunked downloading API
556 *
557 * @param String $file - the file (basename) to be downloaded
558 * @param Array $headers - supplied headers
559 * @param Mixed $data - pass-back from our call to the API (which we don't use)
560 * @param resource $fh - the local file handle
561 *
562 * @return String - the data downloaded
563 */
564 public function chunked_download($file, $headers, $data, $fh) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
565
566 $opts = $this->get_options();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- filter use
567 $storage = $this->get_storage();
568
569 $try_the_other_one = false;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- filter use
570
571 $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this);
572
573 $options = array();
574
575 if (!empty($headers)) $options['headers'] = $headers;
576
577 try {
578 $get = $storage->download($ufile, $fh, $options);
579 } catch (Exception $e) {
580 $this->log($e);
581 $this->log($e->getMessage(), 'error');
582 $get = false;
583 }
584
585 return $get;
586 }
587
588 /**
589 * Get the pre configuration template
590 *
591 * @return String - the template
592 */
593 public function get_pre_configuration_template() {
594
595 global $updraftplus_admin;
596
597 $classes = $this->get_css_classes(false);
598
599 ?>
600 <tr class="<?php echo $classes . ' ' . 'dropbox_pre_config_container';?>">
601 <td colspan="2">
602 <img alt="<?php _e(sprintf(__('%s logo', 'updraftplus'), 'Dropbox')); ?>" src="<?php echo UPDRAFTPLUS_URL.'/images/dropbox-logo.png'; ?>">
603 <br>
604 <p>
605 <?php
606 global $updraftplus_admin;
607 $updraftplus_admin->curl_check('Dropbox', false, 'dropbox');
608 ?>
609 </p>
610 <p>
611 <?php echo sprintf(__('Please read %s for use of our %s authorization app (none of your backup data is sent to us).', 'updraftplus'), '<a target="_blank" href="https://updraftplus.com/faqs/what-is-your-privacy-policy-for-the-use-of-your-dropbox-app/">'.__('this privacy policy', 'updraftplus').'</a>', 'Dropbox');?>
612 </p>
613 </td>
614 </tr>
615
616 <?php
617 }
618
619 /**
620 * Get the configuration template
621 *
622 * @return String - the template, ready for substitutions to be carried out
623 */
624 public function get_configuration_template() {
625 ob_start();
626 $classes = $this->get_css_classes();
627
628 $defmsg = '<tr class="'.$classes.'"><td></td><td><strong>'.__('Need to use sub-folders?', 'updraftplus').'</strong> '.sprintf(__('Backups are saved in %s.', 'updraftplus'), 'apps/UpdraftPlus').' '.sprintf(__('If you backup several sites into the same Dropbox and want to organize with sub-folders, then %scheck out Premium%s', 'updraftplus'), '<a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").'" target="_blank">', '</a>').'</td></tr>';
629
630 $extra_config = apply_filters('updraftplus_dropbox_extra_config_template', $defmsg, $this);
631 echo $extra_config;
632 ?>
633 <tr class="<?php echo $classes;?>">
634 <th><?php echo sprintf(__('Authenticate with %s', 'updraftplus'), __('Dropbox', 'updraftplus'));?>:</th>
635 <td>
636 {{#if is_authenticated}}
637 <?php
638 echo "<p><strong>".__('(You appear to be already authenticated).', 'updraftplus')."</strong>";
639 $this->get_deauthentication_link();
640 echo '</p>';
641 ?>
642 {{/if}}
643 {{#if ownername_sentence}}
644 <br/>
645 {{ownername_sentence}}
646 {{/if}}
647 <?php
648 echo '<p>';
649 $this->get_authentication_link();
650 echo '</p>';
651 ?>
652 </td>
653 </tr>
654 {{!-- Legacy: only show this next setting to old users who had a setting stored --}}
655 {{#if old_user_settings}}
656 <tr class="<?php echo $classes;?>">
657 <th></th>
658 <td>
659 <?php echo '<p>'.htmlspecialchars(__('You must add the following as the authorised redirect URI in your Dropbox console (under "API Settings") when asked', 'updraftplus')).': <kbd>'.UpdraftPlus_Options::admin_page_url().'?page=updraftplus&action=updraftmethod-dropbox-auth</kbd></p>'; ?>
660 </td>
661 </tr>
662 <tr class="<?php echo $classes;?>">
663 <th>Your Dropbox App Key:</th>
664 <td><input type="text" autocomplete="off" style="width:332px" <?php $this->output_settings_field_name_and_id('appkey');?> value="{{appkey}}" /></td>
665 </tr>
666 <tr class="<?php echo $classes;?>">
667 <th>Your Dropbox App Secret:</th>
668 <td><input type="text" style="width:332px" <?php $this->output_settings_field_name_and_id('secret');?> value="{{secret}}" /></td>
669 </tr>
670 {{else}}
671 <?php if (false === strpos($extra_config, '<input')) {
672 // We need to make sure that it is not the case that the module has no settings whatsoever - this can result in the module being effectively invisible.
673 ?>
674 <input type="hidden" <?php $this->output_settings_field_name_and_id('dummy-nosave');?> value="0">
675 <?php } ?>
676 {{/if}}
677 <?php
678 return ob_get_clean();
679 }
680
681 /**
682 * Modifies handerbar template options
683 *
684 * @param array $opts
685 * @return Array - Modified handerbar template options
686 */
687 public function transform_options_for_template($opts) {
688 if (!empty($opts['tk_access_token'])) {
689 $opts['ownername'] = empty($opts['ownername']) ? '' : $opts['ownername'];
690 if ($opts['ownername']) {
691 $opts['ownername_sentence'] = sprintf(__("Account holder's name: %s.", 'updraftplus'), $opts['ownername']).' ';
692 }
693 $opts['is_authenticated'] = true;
694 }
695 $opts['old_user_settings'] = (!empty($opts['appkey']) || (defined('UPDRAFTPLUS_CUSTOM_DROPBOX_APP') && UPDRAFTPLUS_CUSTOM_DROPBOX_APP));
696 if ($opts['old_user_settings']) {
697 $opts['appkey'] = empty($opts['appkey']) ? '' : $opts['appkey'];
698 $opts['secret'] = empty($opts['secret']) ? '' : $opts['secret'];
699 }
700 $opts = apply_filters("updraftplus_options_dropbox_options", $opts);
701 return $opts;
702 }
703
704 /**
705 * Gives settings keys which values should not passed to handlebarsjs context.
706 * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker)
707 *
708 * @return Array - Settings array keys which should be filtered
709 */
710 public function filter_frontend_settings_keys() {
711 return array(
712 'CSRF',
713 'code',
714 'ownername',
715 'tk_access_token',
716 );
717 }
718
719 /**
720 * Over-rides the parent to allow this method to output extra information about using the correct account for OAuth authentication
721 *
722 * @return [boolean] - return false so that no extra information is output
723 */
724 public function output_account_warning() {
725 return true;
726 }
727
728 /**
729 * Handles various URL actions, as indicated by the updraftplus_dropboxauth URL parameter
730 *
731 * @return null
732 */
733 public function action_auth() {
734 if (isset($_GET['updraftplus_dropboxauth'])) {
735 if ('doit' == $_GET['updraftplus_dropboxauth']) {
736 $this->action_authenticate_storage();
737 return;
738 } elseif ('deauth' == $_GET['updraftplus_dropboxauth']) {
739 $this->action_deauthenticate_storage();
740 return;
741 }
742 } elseif (isset($_REQUEST['state'])) {
743
744 if ('POST' == $_SERVER['REQUEST_METHOD']) {
745 $raw_state = urldecode($_POST['state']);
746 if (isset($_POST['code'])) $raw_code = urldecode($_POST['code']);
747 } else {
748 $raw_state = $_GET['state'];
749 if (isset($_GET['code'])) $raw_code = $_GET['code'];
750 }
751
752 $this->do_complete_authentication($raw_state, $raw_code);
753 }
754 try {
755 $this->auth_request();
756 } catch (Exception $e) {
757 $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error');
758 }
759 }
760
761 /**
762 * This function will complete the oAuth flow, if return_instead_of_echo is true then add the action to display the authed admin notice, otherwise echo this notice to page.
763 *
764 * @param string $raw_state - the state
765 * @param string $raw_code - the oauth code
766 * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it
767 *
768 * @return void|string - returns the authentication message if return_instead_of_echo is true
769 */
770 public function do_complete_authentication($raw_state, $raw_code, $return_instead_of_echo = false) {
771 // Get the CSRF from setting and check it matches the one returned if it does no CSRF attack has happened
772 $opts = $this->get_options();
773 $csrf = $opts['CSRF'];
774 $state = stripslashes($raw_state);
775 // Check the state to see if an instance_id has been attached and if it has then extract the state
776 $parts = explode(':', $state);
777 $state = $parts[0];
778
779 if (strcmp($csrf, $state) == 0) {
780 $opts['CSRF'] = '';
781 if (isset($raw_code)) {
782 // set code so it can be accessed in the next authentication step
783 $opts['code'] = stripslashes($raw_code);
784 // remove our flag so we know this authentication is complete
785 if (isset($opts['auth_in_progress'])) unset($opts['auth_in_progress']);
786 $this->set_options($opts, true);
787 $auth_result = $this->auth_token($return_instead_of_echo);
788 if ($return_instead_of_echo) return $auth_result;
789 }
790 } else {
791 error_log("UpdraftPlus: CSRF comparison failure: $csrf != $state");
792 }
793 }
794
795 /**
796 * This method will reset any saved options and start the bootstrap process for an authentication
797 *
798 * @param String $instance_id - the instance id of the settings we want to authenticate
799 */
800 public function do_authenticate_storage($instance_id) {
801 try {
802 // Clear out the existing credentials
803 $opts = $this->get_options();
804 $opts['tk_access_token'] = '';
805 unset($opts['tk_request_token']);
806 $opts['ownername'] = '';
807 // Set a flag so we know this authentication is in progress
808 $opts['auth_in_progress'] = true;
809 $this->set_options($opts, true);
810
811 $this->set_instance_id($instance_id);
812 $this->bootstrap(false);
813 } catch (Exception $e) {
814 $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error');
815 }
816 }
817
818 /**
819 * This method will start the bootstrap process for a de-authentication
820 *
821 * @param String $instance_id - the instance id of the settings we want to de-authenticate
822 */
823 public function do_deauthenticate_storage($instance_id) {
824 try {
825 $this->set_instance_id($instance_id);
826 $this->bootstrap(true);
827 } catch (Exception $e) {
828 $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s de-authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error');
829 }
830 }
831
832 /**
833 * This method will setup the authenticated admin warning, it can either return this or echo it
834 *
835 * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it
836 *
837 * @return void|string - returns the authentication message if return_instead_of_echo is true
838 */
839 public function show_authed_admin_warning($return_instead_of_echo) {
840 global $updraftplus_admin;
841
842 $dropbox = $this->bootstrap();
843 if (false === $dropbox) return false;
844
845 try {
846 $account_info = $dropbox->accountInfo();
847 } catch (Exception $e) {
848 $accountinfo_err = sprintf(__("%s error: %s", 'updraftplus'), 'Dropbox', $e->getMessage()).' ('.$e->getCode().')';
849 }
850
851 $message = "<strong>".__('Success:', 'updraftplus').'</strong> '.sprintf(__('you have authenticated your %s account', 'updraftplus'), 'Dropbox');
852 // We log, because otherwise people get confused by the most recent log message of 'Parameter not found: oauth_token' and raise support requests
853 $this->log(__('Success:', 'updraftplus').' '.sprintf(__('you have authenticated your %s account', 'updraftplus'), 'Dropbox'));
854
855 if (empty($account_info['code']) || "200" != $account_info['code']) {
856 $message .= " (".__('though part of the returned information was not as expected - your mileage may vary', 'updraftplus').") ". $account_info['code'];
857 if (!empty($accountinfo_err)) $message .= "<br>".htmlspecialchars($accountinfo_err);
858 } else {
859 $body = $account_info['body'];
860 $name = '';
861 if (isset($body->display_name)) {
862 $name = $body->display_name;
863 } else {
864 $name = $body->name->display_name;
865 }
866 $message .= ". <br>".sprintf(__('Your %s account name: %s', 'updraftplus'), 'Dropbox', htmlspecialchars($name));
867 $opts = $this->get_options();
868 $opts['ownername'] = $name;
869 $this->set_options($opts, true);
870
871 try {
872 /**
873 * Quota information is no longer provided with account information a new call to qoutaInfo must be made to get this information. The timeout is because we've seen cases where it returned after 180 seconds (apparently a faulty outgoing proxy), and we may as well wait as cause an error leading to user confusion.
874 */
875 $quota_info = $dropbox->quotaInfo(array('timeout' => 190));
876
877 if (empty($quota_info['code']) || "200" != $quota_info['code']) {
878 $message .= " (".__('though part of the returned information was not as expected - your mileage may vary', 'updraftplus').")". $quota_info['code'];
879 if (!empty($accountinfo_err)) $message .= "<br>".htmlspecialchars($accountinfo_err);
880 } else {
881 $body = $quota_info['body'];
882 if (isset($body->quota_info)) {
883 $quota_info = $body->quota_info;
884 $total_quota = max($quota_info->quota, 1);
885 $normal_quota = $quota_info->normal;
886 $shared_quota = $quota_info->shared;
887 $available_quota =$total_quota - ($normal_quota + $shared_quota);
888 $used_perc = round(($normal_quota + $shared_quota)*100/$total_quota, 1);
889 $message .= ' <br>'.sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'Dropbox', $used_perc, round($available_quota/1048576, 1).' MB');
890 } else {
891 $total_quota = max($body->allocation->allocated, 1);
892 $used = $body->used;
893 /* check here to see if the account is a team account and if so use the other used value
894 This will give us their total usage including their individual account and team account */
895 if (isset($body->allocation->used)) $used = $body->allocation->used;
896 $available_quota =$total_quota - $used;
897 $used_perc = round($used*100/$total_quota, 1);
898 $message .= ' <br>'.sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'Dropbox', $used_perc, round($available_quota/1048576, 1).' MB');
899 }
900 }
901 } catch (Exception $e) {
902 // Catch
903 }
904
905 }
906 if ($return_instead_of_echo) {
907 return "<div class='updraftmessage updated'><p>{$message}</p></div>";
908 } else {
909 $updraftplus_admin->show_admin_warning($message);
910 }
911
912 }
913
914 /**
915 * Bootstrap and check token, can also return the authentication method if return_instead_of_echo is true
916 *
917 * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it
918 *
919 * @return void|string - returns the authentication message if return_instead_of_echo is true
920 */
921 public function auth_token($return_instead_of_echo) {
922 $this->bootstrap();
923 $opts = $this->get_options();
924 if (!empty($opts['tk_access_token'])) {
925 if ($return_instead_of_echo) {
926 return $this->show_authed_admin_warning($return_instead_of_echo);
927 } else {
928 add_action('all_admin_notices', array($this, 'show_authed_admin_warning'));
929 }
930 }
931 }
932
933 /**
934 * Acquire single-use authorization code
935 */
936 public function auth_request() {
937 $this->bootstrap();
938 }
939
940 /**
941 * This basically reproduces the relevant bits of bootstrap.php from the SDK
942 *
943 * @param Boolean $deauthenticate indicates if we should bootstrap for a deauth or auth request
944 * @return object
945 */
946 public function bootstrap($deauthenticate = false) {
947
948 $storage = $this->get_storage();
949
950 if (!empty($storage) && !is_wp_error($storage)) return $storage;
951
952 // Dropbox APIv1 is dead, but we'll keep the variable in case v3 is ever announced
953 $dropbox_api = 'Dropbox2';
954
955 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/API.php');
956 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/Exception.php');
957 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Consumer/ConsumerAbstract.php');
958 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Storage/StorageInterface.php');
959 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Storage/Encrypter.php');
960 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Storage/WordPress.php');
961 include_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Consumer/Curl.php');
962 // require_once(UPDRAFTPLUS_DIR.'/includes/'.$dropbox_api.'/OAuth/Consumer/WordPress.php');
963
964 $opts = $this->get_options();
965
966 $key = empty($opts['secret']) ? '' : $opts['secret'];
967 $sec = empty($opts['appkey']) ? '' : $opts['appkey'];
968
969 $oauth2_id = defined('UPDRAFTPLUS_DROPBOX_CLIENT_ID') ? UPDRAFTPLUS_DROPBOX_CLIENT_ID : base64_decode('dzQxM3o0cWhqejY1Nm5l');
970
971 // Set the callback URL
972 $callbackhome = UpdraftPlus_Options::admin_page_url().'?page=updraftplus&action=updraftmethod-dropbox-auth';
973 $callback = defined('UPDRAFTPLUS_DROPBOX_AUTH_RETURN_URL') ? UPDRAFTPLUS_DROPBOX_AUTH_RETURN_URL : 'https://auth.updraftplus.com/auth/dropbox/';
974
975 $instance_id = $this->get_instance_id();
976 // Instantiate the Encrypter and storage objects
977 $encrypter = new Dropbox_Encrypter('ThisOneDoesNotMatterBeyondLength');
978
979 // Instantiate the storage
980 $dropbox_storage = new Dropbox_WordPress($encrypter, "tk_", 'updraft_dropbox', $this);
981
982 // WordPress consumer does not yet work
983 // $oauth = new Dropbox_ConsumerWordPress($sec, $key, $dropbox_storage, $callback);
984
985 // Get the DropBox API access details
986 list($d2, $d1) = $this->defaults();
987 if (empty($sec)) {
988 $sec = base64_decode($d1);
989 }
990
991 if (empty($key)) {
992 $key = base64_decode($d2);
993 }
994
995 $root = 'sandbox';
996 if ('dropbox:' == substr($sec, 0, 8)) {
997 $sec = substr($sec, 8);
998 $root = 'dropbox';
999 }
1000
1001 try {
1002 $oauth = new Dropbox_Curl($sec, $oauth2_id, $key, $dropbox_storage, $callback, $callbackhome, $deauthenticate, $instance_id);
1003 } catch (Exception $e) {
1004 $this->log("Curl error: ".$e->getMessage());
1005 $this->log(sprintf(__("%s error: %s", 'updraftplus'), "Dropbox/Curl", $e->getMessage().' ('.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile()).')', 'error');
1006 return false;
1007 }
1008
1009 if ($deauthenticate) return true;
1010
1011 $storage = new UpdraftPlus_Dropbox_API($oauth, $root);
1012
1013 $this->set_storage($storage);
1014
1015 return $storage;
1016 }
1017 }
1018