PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
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.16.5, at methods/dropbox.php

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