UpdraftPlus WordPress Backup Plugin - Version 1.11.27

Version Description

  • 17/Feb/2016 =

  • FEATURE: Automatic backups can take place before updates commissioned via WordPress.Com/JetPack remote site management (requires a not-yet-released version of JetPack - all current releases are insufficient, so please don't file reports about this yet)

  • FIX: Fixed a further logic error in the advanced backup retention options, potentially relevant if you had more than one extra rule, affecting the oldest backups

  • TWEAK: Resolve issue on some sites with in-dashboard downloads being interfered with by other site components

  • TWEAK: Auto-backups now hook to a newly-added more suitable action, on WP 4.4+ (https://core.trac.wordpress.org/ticket/30441)

  • TWEAK: Make WebDAV library not use a language construct that's not supported by HHVM

  • TWEAK: Change options in the "Backup Now" dialog as main settings are changed

  • TWEAK: Show the file options in the "Backup Now" dialog if/when alerting the user that they've chosen inconsistent options

  • TWEAK: When pruning old backups, save the history to the database at least every 10 seconds, to help with sites with slow network communications and short PHP timeouts

Download this release

Release Info

Developer DavidAnderson
Plugin Icon 128x128 UpdraftPlus WordPress Backup Plugin
Version 1.11.27
Comparing to
See all releases

Code changes from version 1.11.26 to 1.11.27

admin.php CHANGED
@@ -476,6 +476,7 @@ class UpdraftPlus_Admin {
476
  'servererrorcode' => __('The web server returned an error code (try again, or check your web server logs)', 'updraftplus'),
477
  'newuserpass' => __("The new user's RackSpace console password is (this will not be shown again):", 'updraftplus'),
478
  'trying' => __('Trying...', 'updraftplus'),
 
479
  'calculating' => __('calculating...','updraftplus'),
480
  'begunlooking' => __('Begun looking for this entity','updraftplus'),
481
  'stilldownloading' => __('Some files are still downloading or being processed - please wait.', 'updraftplus'),
@@ -536,13 +537,14 @@ class UpdraftPlus_Admin {
536
  'key' => __('Key', 'updraftplus'),
537
  'nokeynamegiven' => sprintf(__("Failure: No %s was given.",'updraftplus'), __('key name','updraftplus')),
538
  'deleting' => __('Deleting...', 'updraftplus'),
 
539
  'delete_response_not_understood' => __("We requested to delete the file, but could not understand the server's response", 'updraftplus'),
540
  'testingconnection' => __('Testing connection...', 'updraftplus'),
541
  'send' => __('Send', 'updraftplus'),
542
  'migratemodalheight' => class_exists('UpdraftPlus_Addons_Migrator') ? 555 : 300,
543
  'migratemodalwidth' => class_exists('UpdraftPlus_Addons_Migrator') ? 770 : 500,
544
  'download' => _x('Download', '(verb)', 'updraftplus'),
545
- 'unsavedsettingsbackup' => __('You have made changes to your settings, and not saved.', 'updraftplus')."\n".__('Your backup will use your old settings until you save your changes.','updraftplus'),
546
  'dayselector' => $day_selector,
547
  'mdayselector' => $mday_selector,
548
  'ud_url' => UPDRAFTPLUS_URL,
@@ -864,7 +866,7 @@ class UpdraftPlus_Admin {
864
  die();
865
  }
866
 
867
- // This function may die(), depending on the request being made
868
  public function do_updraft_download_backup($findex, $type, $timestamp, $stage, $close_connection_callable = false) {
869
 
870
  @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
@@ -910,7 +912,7 @@ class UpdraftPlus_Admin {
910
  $fullpath = $updraftplus->backups_dir_location().'/'.$file;
911
 
912
  if (2 == $stage) {
913
- $updraftplus->spool_file($type, $fullpath);
914
  // Do not return - we do not want the caller to add any output
915
  die;
916
  }
@@ -1078,25 +1080,31 @@ class UpdraftPlus_Admin {
1078
 
1079
  echo json_encode($this->get_activejobs_list($_GET));
1080
 
1081
- } elseif (isset($_REQUEST['subaction']) && 'remotecontrol_createkey' == $_REQUEST['subaction']) {
1082
- // Use the site URL - this means that if the site URL changes, communication ends; which is the case anyway
1083
- $user = wp_get_current_user();
1084
- $name_hash = $user->ID;
1085
- // Sending the key over https means it doesn't have to travel potentially over insecure http to the user's browser for copy-paste
1086
- $send_it_where = defined('UPDRAFTPLUS_REMOTE_SENDKEY_WHERE') ? UPDRAFTPLUS_REMOTE_SENDKEY_WHERE : false;
1087
-
1088
- $extra_info = array(
1089
- 'user_id' => $user->ID,
1090
- 'user_login' => $user->user_login,
1091
- );
1092
 
1093
- if ($send_it_where) {
1094
- $extra_info['mothership'] = UPDRAFTPLUS_REMOTE_SENDKEY_WHERE;
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
  }
1096
 
1097
- $created = $updraftplus->create_remote_control_key($name_hash, $extra_info, $send_it_where);
1098
- echo json_encode($created);
 
 
1099
  die;
 
1100
  } elseif (isset($_REQUEST['subaction']) && 'callwpaction' == $_REQUEST['subaction'] && !empty($_REQUEST['wpaction'])) {
1101
 
1102
  ob_start();
@@ -4056,7 +4064,7 @@ class UpdraftPlus_Admin {
4056
  $included = (UpdraftPlus_Options::get_updraft_option("updraft_include_$key", apply_filters("updraftplus_defaultoption_include_".$key, true))) ? 'checked="checked"' : "";
4057
  if ('others' == $key || 'uploads' == $key) {
4058
 
4059
- $ret .= '<input id="'.$prefix.'updraft_include_'.$key.'" type="checkbox" name="updraft_include_'.$key.'" value="1" '.$included.'> <label '.(('others' == $key) ? 'title="'.sprintf(__('Your wp-content directory server path: %s', 'updraftplus'), WP_CONTENT_DIR).'" ' : '').' for="'.$prefix.'updraft_include_'.$key.'">'.(('others' == $key) ? __('Any other directories found inside wp-content', 'updraftplus') : htmlspecialchars($info['description'])).'</label><br>';
4060
 
4061
  if ($show_exclusion_options) {
4062
  $include_exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_'.$key.'_exclude', ('others' == $key) ? UPDRAFT_DEFAULT_OTHERS_EXCLUDE : UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
@@ -4075,7 +4083,7 @@ class UpdraftPlus_Admin {
4075
  } else {
4076
 
4077
  if ($key != 'more' || true === $include_more || ('sometimes' === $include_more && !empty($include_more_paths))) {
4078
- $ret .= "<input id=\"".$prefix."updraft_include_$key\" type=\"checkbox\" name=\"updraft_include_$key\" value=\"1\" $included /><label for=\"".$prefix."updraft_include_$key\"".((isset($info['htmltitle'])) ? ' title="'.htmlspecialchars($info['htmltitle']).'"' : '')."> ".htmlspecialchars($info['description']);
4079
 
4080
  $ret .= "</label><br>";
4081
  $ret .= apply_filters("updraftplus_config_option_include_$key", '', $prefix);
476
  'servererrorcode' => __('The web server returned an error code (try again, or check your web server logs)', 'updraftplus'),
477
  'newuserpass' => __("The new user's RackSpace console password is (this will not be shown again):", 'updraftplus'),
478
  'trying' => __('Trying...', 'updraftplus'),
479
+ 'fetching' => __('Fetching...', 'updraftplus'),
480
  'calculating' => __('calculating...','updraftplus'),
481
  'begunlooking' => __('Begun looking for this entity','updraftplus'),
482
  'stilldownloading' => __('Some files are still downloading or being processed - please wait.', 'updraftplus'),
537
  'key' => __('Key', 'updraftplus'),
538
  'nokeynamegiven' => sprintf(__("Failure: No %s was given.",'updraftplus'), __('key name','updraftplus')),
539
  'deleting' => __('Deleting...', 'updraftplus'),
540
+ 'enter_mothership_url' => __('Please enter a valid URL', 'updraftplus'),
541
  'delete_response_not_understood' => __("We requested to delete the file, but could not understand the server's response", 'updraftplus'),
542
  'testingconnection' => __('Testing connection...', 'updraftplus'),
543
  'send' => __('Send', 'updraftplus'),
544
  'migratemodalheight' => class_exists('UpdraftPlus_Addons_Migrator') ? 555 : 300,
545
  'migratemodalwidth' => class_exists('UpdraftPlus_Addons_Migrator') ? 770 : 500,
546
  'download' => _x('Download', '(verb)', 'updraftplus'),
547
+ 'unsavedsettingsbackup' => __('You have made changes to your settings, and not saved.', 'updraftplus')."\n".__('You should save your changes to ensure that they are used for making your backup.','updraftplus'),
548
  'dayselector' => $day_selector,
549
  'mdayselector' => $mday_selector,
550
  'ud_url' => UPDRAFTPLUS_URL,
866
  die();
867
  }
868
 
869
+ // This function may die(), depending on the request being made in $stage
870
  public function do_updraft_download_backup($findex, $type, $timestamp, $stage, $close_connection_callable = false) {
871
 
872
  @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
912
  $fullpath = $updraftplus->backups_dir_location().'/'.$file;
913
 
914
  if (2 == $stage) {
915
+ $updraftplus->spool_file($fullpath);
916
  // Do not return - we do not want the caller to add any output
917
  die;
918
  }
1080
 
1081
  echo json_encode($this->get_activejobs_list($_GET));
1082
 
1083
+ } elseif (isset($_REQUEST['subaction']) && 'updraftcentral_delete_key' == $_REQUEST['subaction'] && isset($_REQUEST['key_id'])) {
 
 
 
 
 
 
 
 
 
 
1084
 
1085
+ global $updraftplus_updraftcentral_main;
1086
+ if (!is_a($updraftplus_updraftcentral_main, 'UpdraftPlus_UpdraftCentral_Main')) {
1087
+ echo json_encode(array('error' => 'UpdraftPlus_UpdraftCentral_Main object not found'));
1088
+ die;
1089
+ }
1090
+
1091
+ echo json_encode($updraftplus_updraftcentral_main->delete_key($_REQUEST['key_id']));
1092
+ die;
1093
+
1094
+ } elseif (isset($_REQUEST['subaction']) && ('updraftcentral_create_key' == $_REQUEST['subaction'] || 'updraftcentral_get_log' == $_REQUEST['subaction'])) {
1095
+
1096
+ global $updraftplus_updraftcentral_main;
1097
+ if (!is_a($updraftplus_updraftcentral_main, 'UpdraftPlus_UpdraftCentral_Main')) {
1098
+ echo json_encode(array('error' => 'UpdraftPlus_UpdraftCentral_Main object not found'));
1099
+ die;
1100
  }
1101
 
1102
+ $call_method = substr($_REQUEST['subaction'], 15);
1103
+
1104
+ echo json_encode(call_user_func(array($updraftplus_updraftcentral_main, $call_method), $_REQUEST));
1105
+
1106
  die;
1107
+
1108
  } elseif (isset($_REQUEST['subaction']) && 'callwpaction' == $_REQUEST['subaction'] && !empty($_REQUEST['wpaction'])) {
1109
 
1110
  ob_start();
4064
  $included = (UpdraftPlus_Options::get_updraft_option("updraft_include_$key", apply_filters("updraftplus_defaultoption_include_".$key, true))) ? 'checked="checked"' : "";
4065
  if ('others' == $key || 'uploads' == $key) {
4066
 
4067
+ $ret .= '<input class="updraft_include_entity" id="'.$prefix.'updraft_include_'.$key.'" type="checkbox" name="updraft_include_'.$key.'" value="1" '.$included.'> <label '.(('others' == $key) ? 'title="'.sprintf(__('Your wp-content directory server path: %s', 'updraftplus'), WP_CONTENT_DIR).'" ' : '').' for="'.$prefix.'updraft_include_'.$key.'">'.(('others' == $key) ? __('Any other directories found inside wp-content', 'updraftplus') : htmlspecialchars($info['description'])).'</label><br>';
4068
 
4069
  if ($show_exclusion_options) {
4070
  $include_exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_'.$key.'_exclude', ('others' == $key) ? UPDRAFT_DEFAULT_OTHERS_EXCLUDE : UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
4083
  } else {
4084
 
4085
  if ($key != 'more' || true === $include_more || ('sometimes' === $include_more && !empty($include_more_paths))) {
4086
+ $ret .= "<input class=\"updraft_include_entity\" id=\"".$prefix."updraft_include_$key\" type=\"checkbox\" name=\"updraft_include_$key\" value=\"1\" $included /><label for=\"".$prefix."updraft_include_$key\"".((isset($info['htmltitle'])) ? ' title="'.htmlspecialchars($info['htmltitle']).'"' : '')."> ".htmlspecialchars($info['description']);
4087
 
4088
  $ret .= "</label><br>";
4089
  $ret .= apply_filters("updraftplus_config_option_include_$key", '', $prefix);
backup.php CHANGED
@@ -579,6 +579,7 @@ class UpdraftPlus_Backup {
579
  if (empty($backup_to_examine)) {
580
  unset($functional_backup_history[$backup_datestamp]);
581
  unset($backup_history[$backup_datestamp]);
 
582
  } else {
583
  $functional_backup_history[$backup_datestamp] = $backup_to_examine;
584
  $backup_history[$backup_datestamp] = $backup_to_examine;
@@ -699,7 +700,7 @@ class UpdraftPlus_Backup {
699
 
700
  // Sending an empty array is not itself a problem - except that the remote storage method may not check that before setting up a connection, which can waste time: especially if this is done every time around the loop.
701
  if (!empty($files_to_prune)) {
702
- # Actually delete the files
703
  foreach ($services as $service => $sd) {
704
  $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], $file_sizes);
705
  $updraftplus->record_still_alive();
@@ -710,6 +711,7 @@ class UpdraftPlus_Backup {
710
  if (empty($backup_to_examine)) {
711
  // unset($functional_backup_history[$backup_datestamp]);
712
  unset($backup_history[$backup_datestamp]);
 
713
  } else {
714
  // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
715
  $backup_history[$backup_datestamp] = $backup_to_examine;
@@ -728,6 +730,19 @@ class UpdraftPlus_Backup {
728
 
729
  }
730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  private function remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history) {
732
 
733
  global $updraftplus;
579
  if (empty($backup_to_examine)) {
580
  unset($functional_backup_history[$backup_datestamp]);
581
  unset($backup_history[$backup_datestamp]);
582
+ $this->maybe_save_backup_history_and_reschedule($backup_history);
583
  } else {
584
  $functional_backup_history[$backup_datestamp] = $backup_to_examine;
585
  $backup_history[$backup_datestamp] = $backup_to_examine;
700
 
701
  // Sending an empty array is not itself a problem - except that the remote storage method may not check that before setting up a connection, which can waste time: especially if this is done every time around the loop.
702
  if (!empty($files_to_prune)) {
703
+ // Actually delete the files
704
  foreach ($services as $service => $sd) {
705
  $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], $file_sizes);
706
  $updraftplus->record_still_alive();
711
  if (empty($backup_to_examine)) {
712
  // unset($functional_backup_history[$backup_datestamp]);
713
  unset($backup_history[$backup_datestamp]);
714
+ $this->maybe_save_backup_history_and_reschedule($backup_history);
715
  } else {
716
  // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
717
  $backup_history[$backup_datestamp] = $backup_to_examine;
730
 
731
  }
732
 
733
+ // The purpose of this is to save the backup history periodically - for the benefit of setups where the pruning takes longer than the total allow run time (e.g. if the network communications to the remote storage have delays in, and there are a lot of sets to scan)
734
+ private function maybe_save_backup_history_and_reschedule($backup_history) {
735
+ static $last_saved_at = 0;
736
+ if (!$last_saved_at) $last_saved_at = time();
737
+ if (time() - $last_saved_at >= 10) {
738
+ global $updraftplus;
739
+ $updraftplus->log("Retain: saving new backup history, because at least 10 seconds have passed since the last save (sets now: ".count($backup_history).")");
740
+ UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history, false);
741
+ $updraftplus->something_useful_happened();
742
+ $last_saved_at = time();
743
+ }
744
+ }
745
+
746
  private function remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history) {
747
 
748
  global $updraftplus;
class-updraftplus.php CHANGED
@@ -18,7 +18,6 @@ class UpdraftPlus {
18
  'onedrive' => 'Microsoft OneDrive',
19
  'ftp' => 'FTP',
20
  'azure' => 'Microsoft Azure',
21
- 'copycom' => 'Copy.Com',
22
  'sftp' => 'SFTP / SCP',
23
  'googlecloud' => 'Google Cloud',
24
  'webdav' => 'WebDAV',
@@ -53,8 +52,11 @@ class UpdraftPlus {
53
 
54
  public function __construct() {
55
 
56
- # Bitcasa support is deprecated
57
  if (is_file(UPDRAFTPLUS_DIR.'/addons/bitcasa.php')) $this->backup_methods['bitcasa'] = 'Bitcasa';
 
 
 
58
 
59
  // Initialisation actions - takes place on plugin load
60
 
@@ -135,81 +137,6 @@ class UpdraftPlus {
135
  return $ud_rpc;
136
  }
137
 
138
- public function create_remote_control_key($name_hash, $extra_info = array(), $post_it = false) {
139
-
140
- $indicator_name = $name_hash.'.central.updraftplus.com';
141
-
142
- $our_keys = UpdraftPlus_Options::get_updraft_option('updraft_central_localkeys');
143
- if (!is_array($our_keys)) $our_keys = array();
144
-
145
- if (isset($our_keys[$name_hash])) {
146
- unset($our_keys[$name_hash]);
147
- }
148
-
149
- $ud_rpc = $this->get_udrpc($indicator_name);
150
-
151
- if (is_object($ud_rpc) && $ud_rpc->generate_new_keypair()) {
152
-
153
- if ($post_it) {
154
- // This option allows the key to be sent to the other side via a known-secure channel (e.g. http over SSL), rather than potentially allowing it to travel over an unencrypted channel (e.g. http back to the user's browser). As such, if specified, it is compulsory for it to work.
155
- $sent_key = wp_remote_post(
156
- $post_it,
157
- array(
158
- 'timeout' => 45,
159
- 'body' => array(
160
- 'key' => $ud_rpc->get_key_remote()
161
- )
162
- )
163
- );
164
- if (is_wp_error($sent_key) || empty($sent_key)) {
165
- $err_msg = sprintf(__('A key was created, but the attempt to register it with %s was unsuccessful - please try again later.', 'updraftplus'), (string)$post_it);
166
- if (is_wp_error($sent_key)) $err_msg .= ' '.$sent_key->get_error_message().' ('.$sent_key->get_error_code().')';
167
- return array(
168
- 'r' => $err_msg
169
- );
170
- }
171
-
172
- $response = json_decode($sent_key['body'], true);
173
-
174
- if (!is_array($response) || !isset($response['key_id']) || !isset($response['key_public'])) {
175
- return array(
176
- 'r' => sprintf(__('A key was created, but the attempt to register it with %s was unsuccessful - please try again later.', 'updraftplus'), (string)$post_it),
177
- 'raw' => $sent_key['body']
178
- );
179
- }
180
-
181
- $key_hash = hash('sha256', $ud_rpc->get_key_remote());
182
-
183
- $local_bundle = $ud_rpc->get_portable_bundle('base64_with_count', $extra_info, array('key' => array('key_hash' => $key_hash, 'key_id' => $response['key_id'])));
184
-
185
- } else {
186
- $local_bundle = $ud_rpc->get_portable_bundle('base64_with_count', $extra_info, array('key' => $ud_rpc->get_key_remote()));
187
- }
188
-
189
-
190
- $our_keys[$name_hash] = array(
191
- 'name' => 'Updraft Remote Control',
192
- 'key' => $ud_rpc->get_key_local(),
193
- 'extra_info' => $extra_info
194
- );
195
- // Store the other side's public key
196
- if (!empty($response) && is_array($response) && !empty($response['key_public'])) {
197
- $our_keys[$name_hash]['publickey_remote'] = $response['key_public'];
198
- }
199
- UpdraftPlus_Options::update_updraft_option('updraft_central_localkeys', $our_keys);
200
-
201
- return array(
202
- 'bundle' => $local_bundle,
203
- 'r' => __('Key created successfully.', 'updraftplus').' '.__('You must copy and paste this key now - it cannot be shown again.', 'updraftplus'),
204
- // 'selector' => $this->get_remotesites_selector(array()),
205
- // 'ourkeys' => $this->list_our_keys($our_keys),
206
- );
207
- }
208
-
209
- return false;
210
-
211
- }
212
-
213
  public function ensure_phpseclib($classes = false, $class_paths = false) {
214
 
215
  if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
@@ -389,15 +316,51 @@ class UpdraftPlus {
389
  add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
390
  }
391
  } elseif (isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadfile' && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-db([0-9]+)?+\.(gz\.crypt)$/i', $_GET['updraftplus_file']) && UpdraftPlus_Options::user_can_manage()) {
 
392
  $updraft_dir = $this->backups_dir_location();
393
- $spool_file = $updraft_dir.'/'.basename($_GET['updraftplus_file']);
 
394
  if (is_readable($spool_file)) {
395
  $dkey = isset($_GET['decrypt_key']) ? $_GET['decrypt_key'] : "";
396
- $this->spool_file('db', $spool_file, $dkey);
397
  exit;
398
  } else {
399
  add_action('all_admin_notices', array($this,'show_admin_warning_unreadablefile') );
400
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  }
402
  }
403
  }
@@ -434,7 +397,7 @@ class UpdraftPlus {
434
 
435
  public function show_admin_warning_unreadablefile() {
436
  global $updraftplus_admin;
437
- $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
438
  }
439
 
440
  public function plugins_loaded() {
@@ -445,22 +408,11 @@ class UpdraftPlus {
445
  // The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
446
  if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && $_GET['page'] == 'updraftplus')) {
447
  remove_action('init', 'ganalyticator_stats_init');
448
- # Appointments+ does the same; but provides a cleaner way to disable it
449
- define('APP_GCAL_DISABLE', true);
450
- return;
451
  }
452
 
453
- if (!class_exists('UpdraftPlus_RemoteControl')) {
454
- if (!file_exists(UPDRAFTPLUS_DIR.'/remote.php')) return;
455
- require_once(UPDRAFTPLUS_DIR.'/remote.php');
456
- }
457
-
458
- // Remote control keys
459
- // These are different from the remote send keys, which are set up in the Migrator add-on
460
- $our_keys = UpdraftPlus_Options::get_updraft_option('updraft_central_localkeys');
461
- if (is_array($our_keys) && !empty($our_keys)) {
462
- $remote_control = new UpdraftPlus_RemoteControl($our_keys);
463
- }
464
 
465
  }
466
 
@@ -591,7 +543,7 @@ class UpdraftPlus {
591
  @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
592
  $max_execution_time = (int)@ini_get("max_execution_time");
593
 
594
- $logline = "UpdraftPlus WordPress backup plugin (https://updraftplus.com): ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".@php_uname().") MySQL: $mysql_version Server: ".$_SERVER["SERVER_SOFTWARE"]." safe_mode: $safe_mode max_execution_time: $max_execution_time memory_limit: $memory_limit (used: ${memory_usage}M | ${memory_usage2}M) multisite: ".((is_multisite()) ? 'Y' : 'N')." mcrypt: ".((function_exists('mcrypt_encrypt')) ? 'Y' : 'N')." LANG: ".getenv('LANG')." ZipArchive::addFile: ";
595
 
596
  // method_exists causes some faulty PHP installations to segfault, leading to support requests
597
  if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
@@ -1997,8 +1949,8 @@ class UpdraftPlus {
1997
  if (function_exists('doing_action') && doing_action('init') && (doing_action('updraft_backup_database') || doing_action('updraft_backup'))) {
1998
  $last_scheduled_action_called_at = get_option("updraft_last_scheduled_$semaphore");
1999
  // 11 minutes - so, we're assuming that they haven't custom-modified their schedules to run scheduled backups more often than that. If they have, they need also to use the filter to over-ride this check.
2000
- if ($last_scheduled_action_called_at && time() - $last_scheduled_action_called_at < 660 && apply_filters('updraft_check_repeated_scheduled_backups', true)) {
2001
- $seconds_ago = time() - $last_scheduled_action_called_at;
2002
  $this->log(sprintf('Scheduled backup aborted - another backup of this type was apparently invoked by the WordPress scheduler only %d seconds ago - the WordPress scheduler invoking events multiple times usually indicates a very overloaded server (or other plugins that mis-use the scheduler)', $seconds_ago));
2003
  return;
2004
  }
@@ -2008,7 +1960,15 @@ class UpdraftPlus {
2008
  require_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
2009
  $this->semaphore = UpdraftPlus_Semaphore::factory();
2010
  $this->semaphore->lock_name = $semaphore;
2011
- $this->log('Requesting semaphore lock ('.$semaphore.')');
 
 
 
 
 
 
 
 
2012
  if (!$this->semaphore->lock()) {
2013
  $this->log('Failed to gain semaphore lock ('.$semaphore.') - another backup of this type is apparently already active - aborting (if this is wrong - i.e. if the other backup crashed without removing the lock, then another can be started after 3 minutes)');
2014
  return;
@@ -3219,7 +3179,7 @@ class UpdraftPlus {
3219
  }
3220
  }
3221
 
3222
- public function spool_file($type, $fullpath, $encryption = '') {
3223
  @set_time_limit(900);
3224
 
3225
  if (file_exists($fullpath)) {
@@ -3230,6 +3190,7 @@ class UpdraftPlus {
3230
 
3231
  $spooled = false;
3232
  if ('.crypt' == substr($fullpath, -6, 6)) {
 
3233
  header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
3234
  header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
3235
  $this->spool_crypted_file($fullpath, (string)$encryption);
@@ -3241,7 +3202,7 @@ class UpdraftPlus {
3241
  require_once(UPDRAFTPLUS_DIR.'/includes/class-partialfileservlet.php');
3242
 
3243
  //Prevent the file being read into memory
3244
- @ob_end_flush();
3245
 
3246
  if (isset($_SERVER['HTTP_RANGE'])) {
3247
  $range_header = trim($_SERVER['HTTP_RANGE']);
18
  'onedrive' => 'Microsoft OneDrive',
19
  'ftp' => 'FTP',
20
  'azure' => 'Microsoft Azure',
 
21
  'sftp' => 'SFTP / SCP',
22
  'googlecloud' => 'Google Cloud',
23
  'webdav' => 'WebDAV',
52
 
53
  public function __construct() {
54
 
55
+ // Bitcasa support is deprecated
56
  if (is_file(UPDRAFTPLUS_DIR.'/addons/bitcasa.php')) $this->backup_methods['bitcasa'] = 'Bitcasa';
57
+
58
+ // Copy.Com will be closed on 1st May 2016
59
+ if (is_file(UPDRAFTPLUS_DIR.'/addons/copycom.php')) $this->backup_methods['copycom'] = 'Copy.Com';
60
 
61
  // Initialisation actions - takes place on plugin load
62
 
137
  return $ud_rpc;
138
  }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  public function ensure_phpseclib($classes = false, $class_paths = false) {
141
 
142
  if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
316
  add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
317
  }
318
  } elseif (isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadfile' && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-db([0-9]+)?+\.(gz\.crypt)$/i', $_GET['updraftplus_file']) && UpdraftPlus_Options::user_can_manage()) {
319
+ // Though this (venerable) code uses the action 'downloadfile', in fact, it's not that general: it's just for downloading a decrypted copy of encrypted databases, and nothing else
320
  $updraft_dir = $this->backups_dir_location();
321
+ $file = $_GET['updraftplus_file'];
322
+ $spool_file = $updraft_dir.'/'.basename($file);
323
  if (is_readable($spool_file)) {
324
  $dkey = isset($_GET['decrypt_key']) ? $_GET['decrypt_key'] : "";
325
+ $this->spool_file($spool_file, $dkey);
326
  exit;
327
  } else {
328
  add_action('all_admin_notices', array($this,'show_admin_warning_unreadablefile') );
329
  }
330
+ } elseif ($_GET['action'] == 'updraftplus_spool_file' && !empty($_GET['what']) && !empty($_GET['backup_timestamp']) && is_numeric($_GET['backup_timestamp']) && UpdraftPlus_Options::user_can_manage()) {
331
+ // At some point, it may be worth merging this with the previous section
332
+ $updraft_dir = $this->backups_dir_location();
333
+
334
+ $findex = isset($_GET['findex']) ? (int)$_GET['findex'] : 0;
335
+ $backup_timestamp = $_GET['backup_timestamp'];
336
+ $what = $_GET['what'];
337
+
338
+ $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
339
+
340
+ $filename = null;
341
+ if (isset($backup_history[$backup_timestamp])) {
342
+ if ('db' != substr($what, 0, 2)) {
343
+ $backupable_entities = $this->get_backupable_file_entities();
344
+ if (!isset($backupable_entities[$what])) $filename = false;
345
+ }
346
+ if (false !== $filename && isset($backup_history[$backup_timestamp][$what])) {
347
+ if (is_string($backup_history[$backup_timestamp][$what]) && 0 == $findex) {
348
+ $filename = $backup_history[$backup_timestamp][$what];
349
+ } elseif (isset($backup_history[$backup_timestamp][$what][$findex])) {
350
+ $filename = $backup_history[$backup_timestamp][$what][$findex];
351
+ }
352
+ }
353
+ }
354
+ if (empty($filename) || !is_readable($updraft_dir.'/'.basename($filename))) {
355
+ echo json_encode(array('result' => __('UpdraftPlus notice:','updraftplus').' '.__('The given file was not found, or could not be read.','updraftplus')));
356
+ exit;
357
+ }
358
+
359
+ $dkey = isset($_GET['decrypt_key']) ? (string)$_GET['decrypt_key'] : "";
360
+
361
+ $this->spool_file($updraft_dir.'/'.basename($filename), $dkey);
362
+ exit;
363
+
364
  }
365
  }
366
  }
397
 
398
  public function show_admin_warning_unreadablefile() {
399
  global $updraftplus_admin;
400
+ $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file was not found, or could not be read.','updraftplus'));
401
  }
402
 
403
  public function plugins_loaded() {
408
  // The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
409
  if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && $_GET['page'] == 'updraftplus')) {
410
  remove_action('init', 'ganalyticator_stats_init');
411
+ // Appointments+ does the same; but provides a cleaner way to disable it
412
+ @define('APP_GCAL_DISABLE', true);
 
413
  }
414
 
415
+ if (file_exists(UPDRAFTPLUS_DIR.'/central/bootstrap.php')) require_once(UPDRAFTPLUS_DIR.'/central/bootstrap.php');
 
 
 
 
 
 
 
 
 
 
416
 
417
  }
418
 
543
  @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
544
  $max_execution_time = (int)@ini_get("max_execution_time");
545
 
546
+ $logline = "UpdraftPlus WordPress backup plugin (https://updraftplus.com): ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".@php_uname().") MySQL: $mysql_version Server: ".$_SERVER["SERVER_SOFTWARE"]." safe_mode: $safe_mode max_execution_time: $max_execution_time memory_limit: $memory_limit (used: ${memory_usage}M | ${memory_usage2}M) multisite: ".(is_multisite() ? 'Y' : 'N')." mcrypt: ".(function_exists('mcrypt_encrypt') ? 'Y' : 'N')." LANG: ".getenv('LANG')." ZipArchive::addFile: ";
547
 
548
  // method_exists causes some faulty PHP installations to segfault, leading to support requests
549
  if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
1949
  if (function_exists('doing_action') && doing_action('init') && (doing_action('updraft_backup_database') || doing_action('updraft_backup'))) {
1950
  $last_scheduled_action_called_at = get_option("updraft_last_scheduled_$semaphore");
1951
  // 11 minutes - so, we're assuming that they haven't custom-modified their schedules to run scheduled backups more often than that. If they have, they need also to use the filter to over-ride this check.
1952
+ $seconds_ago = time() - $last_scheduled_action_called_at;
1953
+ if ($last_scheduled_action_called_at && $seconds_ago < 660 && apply_filters('updraft_check_repeated_scheduled_backups', true)) {
1954
  $this->log(sprintf('Scheduled backup aborted - another backup of this type was apparently invoked by the WordPress scheduler only %d seconds ago - the WordPress scheduler invoking events multiple times usually indicates a very overloaded server (or other plugins that mis-use the scheduler)', $seconds_ago));
1955
  return;
1956
  }
1960
  require_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
1961
  $this->semaphore = UpdraftPlus_Semaphore::factory();
1962
  $this->semaphore->lock_name = $semaphore;
1963
+
1964
+ $semaphore_log_message = 'Requesting semaphore lock ('.$semaphore.')';
1965
+ if (!empty($last_scheduled_action_called_at)) {
1966
+ $semaphore_log_message .= " (apparently via scheduler: last_scheduled_action_called_at=$last_scheduled_action_called_at, seconds_ago=$seconds_ago)";
1967
+ } else {
1968
+ $semaphore_log_message .= " (apparently not via scheduler)";
1969
+ }
1970
+
1971
+ $this->log($semaphore_log_message);
1972
  if (!$this->semaphore->lock()) {
1973
  $this->log('Failed to gain semaphore lock ('.$semaphore.') - another backup of this type is apparently already active - aborting (if this is wrong - i.e. if the other backup crashed without removing the lock, then another can be started after 3 minutes)');
1974
  return;
3179
  }
3180
  }
3181
 
3182
+ public function spool_file($fullpath, $encryption = '') {
3183
  @set_time_limit(900);
3184
 
3185
  if (file_exists($fullpath)) {
3190
 
3191
  $spooled = false;
3192
  if ('.crypt' == substr($fullpath, -6, 6)) {
3193
+ if (ob_get_level()) @ob_end_clean();
3194
  header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
3195
  header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
3196
  $this->spool_crypted_file($fullpath, (string)$encryption);
3202
  require_once(UPDRAFTPLUS_DIR.'/includes/class-partialfileservlet.php');
3203
 
3204
  //Prevent the file being read into memory
3205
+ if (ob_get_level()) @ob_end_clean();
3206
 
3207
  if (isset($_SERVER['HTTP_RANGE'])) {
3208
  $range_header = trim($_SERVER['HTTP_RANGE']);
css/admin.css CHANGED
@@ -50,10 +50,6 @@
50
  list-style: disc inside;
51
  }
52
 
53
- .updraft-hidden {
54
- display:none;
55
- }
56
-
57
  .dashicons-log-fix {
58
  display: inherit;
59
  }
50
  list-style: disc inside;
51
  }
52
 
 
 
 
 
53
  .dashicons-log-fix {
54
  display: inherit;
55
  }
includes/class-udrpc.php ADDED
@@ -0,0 +1,817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ This class provides methods for encrypting, sending, receiving and decrypting messages of arbitrary length, using standard encryption methods and including protection against replay attacks.
5
+
6
+ Example:
7
+
8
+ // Set a key and encrypt with it
9
+ $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
10
+ $ud_rpc->set_key_local($our_private_key);
11
+ $ud_rpc->set_key_remote($their_public_key);
12
+ $encrypted = $ud_rpc->encrypt_message('blah blah');
13
+
14
+ // Use the saved WP site option
15
+ $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
16
+ $ud_rpc->set_option_name('udrpc_remotekey');
17
+ if (!$ud_rpc->get_key_remote()) throw new Exception('...');
18
+ $encrypted = $ud_rpc->encrypt_message('blah blah');
19
+
20
+ // Generate a new key
21
+ $ud_rpc = new UpdraftPlus_Remote_Communications('myindicator.example.com');
22
+ $ud_rpc->set_option_name('udrpc_localkey'); // Save as a WP site option
23
+ $new_pair = $ud_rpc->generate_new_keypair();
24
+ if ($new_pair) {
25
+ $local_private_key = $ud_rpc->get_key_local();
26
+ $remote_public_key = $ud_rpc->get_key_remote();
27
+ // ...
28
+ } else {
29
+ throw new Exception('...');
30
+ }
31
+
32
+ // Send a message
33
+ $ud_rpc->activate_replay_protection();
34
+ $ud_rpc->set_destination_url('https://example.com/path/to/wp');
35
+ $ud_rpc->send_message('ping');
36
+ $ud_rpc->send_message('somecommand', array('param1' => 'data', 'param2' => 'moredata'));
37
+
38
+ // N.B. The data sent needs to be something that will pass json_encode(). So, it may be desirable to base64-encode it first.
39
+
40
+ // Create a listener for incoming messages
41
+
42
+ add_filter('udrpc_command_somecommand', 'my_function', 10, 3);
43
+ // function my_function($response, $data, $name_indicator) { ... ; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
44
+ // Or:
45
+ // add_filter('udrpc_action', 'some_function', 10, 4); // Function must return something other than false to indicate that it handled the specific command. Any returned value will be sent as the reply.
46
+ // function some_function($response, $command, $data, $name_indicator) { ...; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
47
+ $ud_rpc->set_option_name('udrpc_local_private_key');
48
+ $ud_rpc->activate_replay_protection();
49
+ if ($ud_rpc->get_key_local()) {
50
+ // Make sure you call this before the wp_loaded action is fired (e.g. at init)
51
+ $ud_rpc->create_listener();
52
+ }
53
+
54
+ // Instead of using activate_replay_protection(), you can use activate_sequence_protection() (receiving side) and set_next_send_sequence_id(). They are very similar; but, the sequence number code isn't tested, and is problematic if you may have multiple clients that don't share storage (you can use the current time as a sequence number, but if two clients send at the same millisecond (or whatever granularity you use), you may have problems); whereas the replay protection code relies on database storage on the sending side (not just the receiving).
55
+
56
+ */
57
+
58
+ if (!class_exists('UpdraftPlus_Remote_Communications')):
59
+ class UpdraftPlus_Remote_Communications {
60
+
61
+ // Version numbers relate to versions of this PHP library only (i.e. it's not a protocol support number, and version numbers of other compatible libraries (e.g. JavaScript) are not comparable)
62
+ public $version = '1.2';
63
+
64
+ private $key_name_indicator;
65
+
66
+ private $key_option_name = false;
67
+ private $key_remote = false;
68
+ private $key_local = false;
69
+
70
+ private $can_generate = false;
71
+
72
+ private $destination_url = false;
73
+
74
+ private $maximum_replay_time_difference = 300;
75
+ private $extra_replay_protection = false;
76
+
77
+ private $sequence_protection_tolerance;
78
+ private $sequence_protection_table;
79
+ private $sequence_protection_column;
80
+ private $sequence_protection_where_sql;
81
+
82
+ // Debug may log confidential data using $this->log() - so only use when you are in a secure environment
83
+ private $debug = false;
84
+
85
+ private $next_send_sequence_id;
86
+
87
+ private $allow_cors_from = array();
88
+
89
+ // Default protocol version - this can be over-ridden with set_message_format
90
+ // Protocol version 1 (which uses only one RSA key-pair, instead of two) is legacy/deprecated
91
+ private $format = 2;
92
+
93
+ public function __construct($key_name_indicator = 'default', $can_generate = false) {
94
+ $this->set_key_name_indicator($key_name_indicator);
95
+ }
96
+
97
+ public function set_key_name_indicator($key_name_indicator) {
98
+ $this->key_name_indicator = $key_name_indicator;
99
+ }
100
+
101
+ public function set_can_generate($can_generate = true) {
102
+ $this->can_generate = $can_generate;
103
+ }
104
+
105
+ // Which sites to allow CORS requests from
106
+ public function set_allow_cors_from($allow_cors_from) {
107
+ $this->allow_cors_from = $allow_cors_from;
108
+ }
109
+
110
+ public function set_maximum_replay_time_difference($replay_time_difference) {
111
+ $this->maximum_replay_time_difference = (int)$replay_time_difference;
112
+ }
113
+
114
+ // This will cause more things to be sent to $this->log()
115
+ public function set_debug($debug = true) {
116
+ $this->debug = (bool)$debug;
117
+ }
118
+
119
+ // Sequence protection and replay protection perform similar functions, and using both is often over-kill; the distinction is that sequence protection can be used without needing to do database writes on the sending side (e.g. use the value of time() as the sequence number).
120
+ // The only rule of sequences is that the receiving side will reject any sequence number that is less than the last previously seen one, within the bounds of the tolerance (but it may also reject those if they are repeats).
121
+ // The given table/column will record a comma-separated list of recently seen sequences numbers within the tolerance threshold.
122
+ public function activate_sequence_protection($table, $column, $where_sql, $tolerance = 5) {
123
+ $this->sequence_protection_tolerance = (int)$tolerance;
124
+ $this->sequence_protection_table = (string)$table;
125
+ $this->sequence_protection_column = (string)$column;
126
+ $this->sequence_protection_where_sql = (string)$where_sql;
127
+ }
128
+
129
+ private function ensure_crypto_loaded() {
130
+ if (!class_exists('Crypt_Rijndael') || !class_exists('Crypt_RSA') || !class_exists('Crypt_Hash')) {
131
+ global $updraftplus;
132
+ // phpseclib 1.x uses deprecated PHP4-style constructors
133
+ $this->no_deprecation_warnings_on_php7();
134
+ if (is_a($updraftplus, 'UpdraftPlus')) {
135
+ $updraftplus->ensure_phpseclib(array('Crypt_Rijndael', 'Crypt_RSA', 'Crypt_Hash'), array('Crypt/Rijndael', 'Crypt/RSA', 'Crypt/Hash'));
136
+ } elseif (defined('UPDRAFTPLUS_DIR') && file_exists(UPDRAFTPLUS_DIR.'/includes/phpseclib')) {
137
+ if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(UPDRAFTPLUS_DIR.'/includes/phpseclib'.PATH_SEPARATOR.get_include_path());
138
+ if (!class_exists('Crypt_Rijndael')) require_once('Crypt/Rijndael.php');
139
+ if (!class_exists('Crypt_RSA')) require_once('Crypt/RSA.php');
140
+ if (!class_exists('Crypt_Hash')) require_once('Crypt/Hash.php');
141
+ } elseif (file_exists(dirname(__DIR__).'/vendor/phpseclib')) {
142
+ $pdir = dirname(__DIR__).'/vendor/phpseclib';
143
+ if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
144
+ if (!class_exists('Crypt_Rijndael')) require_once('Crypt/Rijndael.php');
145
+ if (!class_exists('Crypt_RSA')) require_once('Crypt/RSA.php');
146
+ if (!class_exists('Crypt_Hash')) require_once('Crypt/Hash.php');
147
+ } elseif (file_exists(dirname(__DIR__).'/composer/vendor/phpseclib/phpseclib/phpseclib')) {
148
+ $pdir = dirname(__DIR__).'/composer/vendor/phpseclib/phpseclib/phpseclib';
149
+ if (false === strpos(get_include_path(), $pdir)) set_include_path($pdir.PATH_SEPARATOR.get_include_path());
150
+ if (!class_exists('Crypt_Rijndael')) require_once('Crypt/Rijndael.php');
151
+ if (!class_exists('Crypt_RSA')) require_once('Crypt/RSA.php');
152
+ if (!class_exists('Crypt_Hash')) require_once('Crypt/Hash.php');
153
+ }
154
+ }
155
+ }
156
+
157
+ // Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
158
+ private function no_deprecation_warnings_on_php7() {
159
+ // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
160
+ // We don't test for PHP > 7 because the specific deprecated element will be removed in PHP 8 - and so no warning should come anyway (and we shouldn't suppress other stuff until we know we need to).
161
+ if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
162
+ $old_level = error_reporting();
163
+ $new_level = $old_level & ~E_DEPRECATED;
164
+ if ($old_level != $new_level) error_reporting($new_level);
165
+ }
166
+ }
167
+
168
+ public function set_destination_url($destination_url) {
169
+ $this->destination_url = $destination_url;
170
+ }
171
+
172
+ public function set_option_name($key_option_name) {
173
+ $this->key_option_name = $key_option_name;
174
+ }
175
+
176
+ // Method to get the remote key
177
+ public function get_key_remote() {
178
+ if (empty($this->key_remote) && $this->can_generate) {
179
+ $this->generate_new_keypair();
180
+ }
181
+ return empty($this->key_remote) ? false : $this->key_remote;
182
+ }
183
+
184
+ // Set the remote key
185
+ public function set_key_remote($key_remote) {
186
+ $this->key_remote = $key_remote;
187
+ }
188
+
189
+ // Used for sending - when receiving, the format is part of the message
190
+ public function set_message_format($format = 2) {
191
+ $this->format = $format;
192
+ }
193
+
194
+ // Method to get the local key
195
+ public function get_key_local() {
196
+ if (empty($this->key_local)) {
197
+ if ($this->key_option_name) {
198
+ $key_local = get_site_option($this->key_option_name);
199
+ if ($key_local) {
200
+ $this->key_local = $key_local;
201
+ }
202
+ }
203
+ }
204
+ if (empty($this->key_local) && $this->can_generate) {
205
+ $this->generate_new_keypair();
206
+ }
207
+ return empty($this->key_local) ? false : $this->key_local;
208
+ }
209
+
210
+ // Tests whether a supplied string (after trimming) is a valid portable bundle
211
+ // Valid formats: same as get_portable_bundle()
212
+ // Returns: (array)an array (which the consumer is free to use - e.g. convert into internationalised string), with keys 'code' and (perhaps) 'data'
213
+ // Error codes: 'invalid_wrong_length'|'invalid_corrupt'
214
+ // Success codes: 'success' - then has further keys 'key', 'name_indicator' and 'url' (and anything else that was in the bundle)
215
+ public function decode_portable_bundle($bundle, $format = 'raw') {
216
+ $bundle = trim($bundle);
217
+ if ('base64_with_count' == $format) {
218
+ if (strlen($bundle) < 5) return array('code' => 'invalid_wrong_length', 'data' => 'too_short');
219
+ $len = substr($bundle, 0, 4);
220
+ $bundle = substr($bundle, 4);
221
+ $len = hexdec($len);
222
+ if (strlen($bundle) != $len) return array('code' => 'invalid_wrong_length', 'data' => "1,$len,".strlen($bundle));
223
+ if (false === ($bundle = base64_decode($bundle))) return array('code' => 'invalid_corrupt', 'data' => 'not_base64');
224
+ if (null === ($bundle = json_decode($bundle, true))) return array('code' => 'invalid_corrupt', 'data' => 'not_json');
225
+ }
226
+ if (empty($bundle['key'])) return array('code' => 'invalid_corrupt', 'data' => 'no_key');
227
+ if (empty($bundle['url'])) return array('code' => 'invalid_corrupt', 'data' => 'no_url');
228
+ if (empty($bundle['name_indicator'])) return array('code' => 'invalid_corrupt', 'data' => 'no_name_indicator');
229
+ return $bundle;
230
+ }
231
+
232
+ // Method to get a portable bundle sufficient to contact this site (i.e. remote site - so you need to have generated a key-pair, or stored the remote key somewhere and restored it)
233
+ // Supported formats: base64_with_count | (default)raw
234
+ // $extra_info needs to be JSON-serialisable, so be careful about what you put into it.
235
+ public function get_portable_bundle($format = 'raw', $extra_info = array(), $options = array()) {
236
+ $site_url = trailingslashit(network_site_url());
237
+ $bundle = array_merge($extra_info, array(
238
+ 'key' => empty($options['key']) ? $this->get_key_remote() : $options['key'],
239
+ 'name_indicator' => $this->key_name_indicator,
240
+ 'url' => $site_url,
241
+ ));
242
+
243
+ if ('base64_with_count' == $format) {
244
+ $bundle = base64_encode(json_encode($bundle));
245
+
246
+ $len = strlen($bundle); // Get the length
247
+ $len = dechex($len); // The first bytes of the message are the bundle length
248
+ $len = str_pad($len, 4, '0', STR_PAD_LEFT); // Zero pad
249
+
250
+ return $len.$bundle;
251
+
252
+ } else {
253
+ return $bundle;
254
+ }
255
+
256
+ }
257
+
258
+ public function set_key_local($key_local) {
259
+ $this->key_local = $key_local;
260
+ if ($this->key_option_name) update_site_option($this->key_option_name, $this->key_local);
261
+ }
262
+
263
+ public function generate_new_keypair() {
264
+
265
+ $this->ensure_crypto_loaded();
266
+
267
+ $rsa = new Crypt_RSA();
268
+ $keys = $rsa->createKey(2048);
269
+
270
+ if (empty($keys['privatekey'])) {
271
+ $this->set_key_local(false);
272
+ } else {
273
+ $this->set_key_local($keys['privatekey']);
274
+ }
275
+
276
+ if (empty($keys['publickey'])) {
277
+ $this->set_key_remote(false);
278
+ } else {
279
+ $this->set_key_remote($keys['publickey']);
280
+ }
281
+
282
+ return empty($keys['publickey']) ? false : true;
283
+ }
284
+
285
+ // A base-64 encoded RSA hash (PKCS_1) of the message digest
286
+ public function signature_for_message($message, $use_key = false) {
287
+
288
+ $hash_algorithm = 'sha256';
289
+
290
+ // Sign with the private (local) key
291
+ if (!$use_key) {
292
+ if (!$this->key_local) throw new Exception('No signing key has been set');
293
+ $use_key = $this->key_local;
294
+ }
295
+
296
+ $this->ensure_crypto_loaded();
297
+
298
+ $rsa = new Crypt_RSA();
299
+ $rsa->loadKey($use_key);
300
+ // This is the older signature mode; phpseclib's default is the preferred CRYPT_RSA_SIGNATURE_PSS; however, Forge JS doesn't yet support this. More info: https://en.wikipedia.org/wiki/PKCS_1
301
+ $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
302
+
303
+ // Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
304
+ // $hash = new Crypt_Hash($hash_algorithm);
305
+ // $hashed = $hash->hash($message);
306
+
307
+ // if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
308
+
309
+ // phpseclib defaults to SHA1
310
+ $rsa->setHash($hash_algorithm);
311
+ $encrypted = $rsa->sign($message);
312
+
313
+ if ($this->debug) $this->log("Signed hash (mode=".CRYPT_RSA_SIGNATURE_PKCS1.") (hex): ".bin2hex($encrypted));
314
+
315
+ $signature = base64_encode($encrypted);
316
+
317
+ if ($this->debug) $this->log("Message signature (base64): $signature");
318
+
319
+ return $signature;
320
+ }
321
+
322
+ // $level is not yet used much
323
+ private function log($message, $level = 'notice') {
324
+ // Allow other plugins to do something with the message
325
+ do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
326
+ if ($level != 'info') error_log("UDRPC (".$this->key_name_indicator.", $level): $message");
327
+ }
328
+
329
+ // Encrypt the message, using the local key (which needs to exist)
330
+ public function encrypt_message($plaintext, $use_key = false, $key_length=32) {
331
+
332
+ if (!$use_key) {
333
+ if ($this->format == 1) {
334
+ if (!$this->key_local) throw new Exception('No encryption key has been set');
335
+ $use_key = $this->key_local;
336
+ } else {
337
+ if (!$this->key_remote) throw new Exception('No encryption key has been set');
338
+ $use_key = $this->key_remote;
339
+ }
340
+ }
341
+
342
+ $this->ensure_crypto_loaded();
343
+
344
+ $rsa = new Crypt_RSA();
345
+
346
+ if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
347
+
348
+ $rij = new Crypt_Rijndael();
349
+
350
+ // Generate Random Symmetric Key
351
+ $sym_key = crypt_random_string($key_length);
352
+
353
+ if ($this->debug) $this->log("Unencrypted symmetric key (hex): ".bin2hex($sym_key));
354
+
355
+ // Encrypt Message with new Symmetric Key
356
+ $rij->setKey($sym_key);
357
+ $ciphertext = $rij->encrypt($plaintext);
358
+
359
+ if ($this->debug) $this->log("Encrypted ciphertext (hex): ".bin2hex($ciphertext));
360
+
361
+ $ciphertext = base64_encode($ciphertext);
362
+
363
+ // Encrypt the Symmetric Key with the Asymmetric Key
364
+ $rsa->loadKey($use_key);
365
+ $sym_key = $rsa->encrypt($sym_key);
366
+
367
+ if ($this->debug) $this->log("Encrypted symmetric key (hex): ".bin2hex($sym_key));
368
+
369
+ // Base 64 encode the symmetric key for transport
370
+ $sym_key = base64_encode($sym_key);
371
+
372
+ if ($this->debug) $this->log("Encrypted symmetric key (b64): ".$sym_key);
373
+
374
+ $len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
375
+
376
+ // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
377
+ $cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
378
+
379
+ // Concatenate the length, the encrypted symmetric key, and the message
380
+ return $len.$sym_key.$cipherlen.$ciphertext;
381
+
382
+ }
383
+
384
+ // Decrypt the message, using the local key (which needs to exist)
385
+ public function decrypt_message($message) {
386
+
387
+ if (!$this->key_local) throw new Exception('No decryption key has been set');
388
+
389
+ $this->ensure_crypto_loaded();
390
+
391
+ $rsa = new Crypt_RSA();
392
+ if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
393
+ // Defaults to CRYPT_AES_MODE_CBC
394
+ $rij = new Crypt_Rijndael();
395
+
396
+ // Extract the Symmetric Key
397
+ $len = substr($message, 0, 3);
398
+ $len = hexdec($len);
399
+ $sym_key = substr($message, 3, $len);
400
+
401
+ // Extract the encrypted message
402
+ $cipherlen = substr($message, $len+3, 16);
403
+ $cipherlen = hexdec($cipherlen);
404
+
405
+ $ciphertext = substr($message, $len+19, $cipherlen);
406
+ $ciphertext = base64_decode($ciphertext);
407
+
408
+ // Decrypt the encrypted symmetric key
409
+ $rsa->loadKey($this->key_local);
410
+ $sym_key = base64_decode($sym_key);
411
+ $sym_key = $rsa->decrypt($sym_key);
412
+
413
+ // Decrypt the message
414
+ $rij->setKey($sym_key);
415
+ return $rij->decrypt($ciphertext);
416
+
417
+ }
418
+
419
+ // Returns an array - which the caller will then format as required (e.g. use as body in post, or JSON-encode, etc.)
420
+ public function create_message($command, $data = null, $is_response = false, $use_key_remote = false, $use_key_local = false) {
421
+
422
+ if ($is_response) {
423
+ $send_array = array('response' => $command);
424
+ } else {
425
+ $send_array = array('command' => $command);
426
+ }
427
+
428
+ $send_array['time'] = time();
429
+ // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
430
+ $send_array['key_name'] = $this->key_name_indicator;
431
+
432
+ // This random element means that if the site needs to send two identical commands or responses in the same second, then it can, and still use replay protection
433
+ $send_array['rand'] = rand(0, PHP_INT_MAX);
434
+
435
+ if ($this->next_send_sequence_id) {
436
+ $send_array['sequence_id'] = $this->next_send_sequence_id;
437
+ $this->next_send_sequence_id++;
438
+ }
439
+
440
+ if (null !== $data) $send_array['data'] = $data;
441
+ $send_data = $this->encrypt_message(json_encode($send_array), $use_key_remote);
442
+
443
+ $message = array(
444
+ 'format' => $this->format,
445
+ 'key_name' => $this->key_name_indicator,
446
+ 'udrpc_message' => $send_data
447
+ );
448
+
449
+ if ($this->format >= 2) {
450
+ $signature = $this->signature_for_message($send_data, $use_key_local);
451
+ $message['signature'] = $signature;
452
+ }
453
+
454
+ return $message;
455
+
456
+ }
457
+
458
+ // N.B. There's already some time-based replay protection. This can be turned on to beef it up.
459
+ // This is only for listeners. Replays can only be detection if transients are working on the WP site (which by default only means that the option table is working).
460
+ public function activate_replay_protection($activate = true) {
461
+ $this->extra_replay_protection = (bool)$activate;
462
+ }
463
+
464
+ public function set_next_send_sequence_id($id) {
465
+ $this->next_send_sequence_id = $id;
466
+ }
467
+
468
+ public function send_message($command, $data = null, $timeout = 20) {
469
+
470
+ if (empty($this->destination_url)) return new WP_Error('not_initialised', 'RPC error: URL not initialised');
471
+
472
+ $message = $this->create_message($command, $data);
473
+
474
+ $post = wp_remote_post(
475
+ $this->destination_url,
476
+ array(
477
+ 'timeout' => $timeout,
478
+ 'body' => $message
479
+ )
480
+ );
481
+
482
+ if (is_wp_error($post)) return $post;
483
+
484
+ if (empty($post['response']) || empty($post['response']['code'])) return new WP_Error('empty_http_code', 'Unexpected HTTP response code');
485
+
486
+ if ($post['response']['code'] < 200 || $post['response']['code'] >= 300) return new WP_Error('unexpected_http_code', 'Unexpected HTTP response code ('.$post['response']['code'].')', $post['response']['code']);
487
+
488
+ if (empty($post['body'])) return new WP_Error('empty_response', 'Empty response from remote site');
489
+
490
+ $decoded = json_decode((string)$post['body'], true);
491
+
492
+ if (empty($decoded)) {
493
+ $this->log("response from remote site could not be understood: ".substr($post['body'], 0, 100).' ... ');
494
+ return new WP_Error('response_not_understood', 'Response from remote site could not be understood', $post['body']);
495
+ }
496
+
497
+ if (!is_array($decoded) || empty($decoded['udrpc_message'])) return new WP_Error('response_not_understood', 'Response from remote site was not in the expected format ('.$post['body'].')', $decoded);
498
+
499
+ if ($this->format >= 2) {
500
+ if (empty($decoded['signature'])) {
501
+ $this->log("No message signature found");
502
+ die;
503
+ }
504
+ if (!$this->key_remote) {
505
+ $this->log('No signature verification key has been set');
506
+ die;
507
+ }
508
+ if (!$this->verify_signature($decoded['udrpc_message'], $decoded['signature'], $this->key_remote)) {
509
+ $this->log('Signature verification failed; discarding');
510
+ die;
511
+ }
512
+ }
513
+
514
+
515
+ $decoded = $this->decrypt_message($decoded['udrpc_message']);
516
+
517
+ if (!is_string($decoded)) return new WP_Error('not_decrypted', 'Response from remote site was not successfully decrypted', $decoded['udrpc_message']);
518
+
519
+ $json_decoded = json_decode($decoded, true);
520
+
521
+ if (!is_array($json_decoded) || empty($json_decoded['response']) || empty($json_decoded['time']) || !is_numeric($json_decoded['time'])) return new WP_Error('response_corrupt', 'Response from remote site was not in the expected format', $decoded);
522
+
523
+ // Don't do the reply detection until now, because $post['body'] may not be a message that originated from the remote component at all (e.g. an HTTP error)
524
+ if ($this->extra_replay_protection) {
525
+ $message_hash = $this->calculate_message_hash((string)$post['body']);
526
+ if ($this->message_hash_seen($message_hash)) {
527
+ return new WP_Error('replay_detected', 'Message refused: replay detected', $message_hash);
528
+ }
529
+ }
530
+
531
+ $time_difference = absint(time() - $json_decoded['time']);
532
+ if ($time_difference > $this->maximum_replay_time_difference) return array(
533
+ 'response' => 'rpcerror',
534
+ 'data' => array(
535
+ 'code' => 'window_error',
536
+ 'difference' => $time_difference,
537
+ 'maximum_difference' => $this->maximum_replay_time_difference
538
+ )
539
+ );
540
+
541
+ // Should be an array with keys including 'response' and (if relevant) 'data'
542
+ return $json_decoded;
543
+
544
+ }
545
+
546
+ // Returns a boolean indicating whether a listener was created - which depends on whether one was needed (so, false does not necessarily indicate an error condition)
547
+ public function create_listener() {
548
+
549
+ $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
550
+
551
+ // Create the WP actions to handle incoming commands, handle built-in commands (e.g. ping, create_keys (authenticate with admin creds)), dispatch them to the right place, and die
552
+ if ( (!empty($_POST) && !empty($_POST['udrpc_message']) && !empty($_POST['format'])) || (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) ) {
553
+ add_action('wp_loaded', array($this, 'wp_loaded'));
554
+ add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
555
+ return true;
556
+ }
557
+ return false;
558
+ }
559
+
560
+ public function wp_loaded_final() {
561
+ $message_for = empty($_POST['key_name']) ? '' : (string)$_POST['key_name'];
562
+ $this->log("Message was received, but not understood by local site (for: $message_for)");
563
+ die;
564
+ }
565
+
566
+ public function wp_loaded() {
567
+ // CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
568
+ // get_http_origin() : since WP 3.4
569
+ $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
570
+ if (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) {
571
+ if (in_array($http_origin, $this->allow_cors_from)) {
572
+ header("Access-Control-Allow-Origin: $http_origin");
573
+ header("Access-Control-Allow-Credentials: true");
574
+ if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header('Access-Control-Allow-Methods: POST, OPTIONS');
575
+ if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
576
+ die;
577
+ } elseif ($this->debug) {
578
+ $this->log("Non-allowed CORS from: ".$http_origin);
579
+ }
580
+ // Having detected that this is a CORS request, there's nothing more to do. We return, because a different listener might pick it up, even though we didn't.
581
+ return;
582
+ }
583
+
584
+ // Silently return, rather than dying, in case another instance is able to handle this
585
+ if (empty($_POST['format']) || (1 != $_POST['format'] && 2 != $_POST['format'])) return;
586
+
587
+ $format = $_POST['format'];
588
+
589
+ /*
590
+
591
+ In format 1 (legacy/obsolete), the one encrypts (the shared AES key) using one half of the key-pair, and decrypts with the other; whereas the other side of the conversation does the reverse when replying (and uses a different shared AES key). Though this is possible in RSA, this is the wrong thing to do - see https://crypto.stackexchange.com/questions/2123/rsa-encryption-with-private-key-and-decryption-with-a-public-key
592
+
593
+ In format 2, both sides have their own private and public key. The sender encrypts using the other side's public key, and decrypts using its own private key. Messages are signed (the message digest is SHA-256).
594
+
595
+ */
596
+
597
+ // Is this for us?
598
+ if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
599
+ return;
600
+ }
601
+
602
+ // wp_unslash() does not exist until after WP 3.5
603
+ // $udrpc_message = function_exists('wp_unslash') ? wp_unslash($_POST['udrpc_message']) : stripslashes_deep($_POST['udrpc_message']);
604
+
605
+ // Data should not have any slashes - it is base64-encoded
606
+ $udrpc_message = (string)$_POST['udrpc_message'];
607
+
608
+ // Check this now, rather than allow the decrypt method to thrown an Exception
609
+
610
+ if (empty($this->key_local)) {
611
+ $this->log("no local key (format 1): cannot decrypt");
612
+ die;
613
+ }
614
+
615
+ if ($format >= 2) {
616
+ if (empty($_POST['signature'])) {
617
+ $this->log("No message signature found");
618
+ die;
619
+ }
620
+ if (!$this->key_remote) {
621
+ $this->log('No signature verification key has been set');
622
+ die;
623
+ }
624
+ if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
625
+ $this->log('Signature verification failed; discarding');
626
+ }
627
+ }
628
+
629
+ try {
630
+ $udrpc_message = $this->decrypt_message($udrpc_message);
631
+ } catch (Exception $e) {
632
+ $this->log("Exception (".get_class($e)."): ".$e->getMessage());
633
+ die;
634
+ }
635
+
636
+ $udrpc_message = json_decode($udrpc_message, true);
637
+
638
+ if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
639
+ $this->log("Could not decode JSON on incoming message");
640
+ die;
641
+ }
642
+
643
+ if (empty($udrpc_message['time'])) {
644
+ $this->log("No time set in incoming message");
645
+ die;
646
+ }
647
+
648
+ // Mismatch indicating a replay of the message with a different key name in the unencrypted portion?
649
+ if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
650
+ $this->log("key_name mismatch between encrypted and unencrypted portions");
651
+ die;
652
+ }
653
+
654
+ if ($this->extra_replay_protection) {
655
+ $message_hash = $this->calculate_message_hash((string)$_POST['udrpc_message']);
656
+ if ($this->message_hash_seen($message_hash)) {
657
+ $this->log("Message dropped: apparently a replay (hash: $message_hash)");
658
+ die;
659
+ }
660
+ }
661
+
662
+ // Do this after the extra replay protection, as that checks hashes within the maximum time window - so don't check the maximum time window until afterwards, to avoid a tiny window (race) in between.
663
+ $time_difference = absint($udrpc_message['time'] - time());
664
+ if ($time_difference > $this->maximum_replay_time_difference) {
665
+ $this->log("Time in incoming message is outside of allowed window ($time_difference > ".$this->maximum_replay_time_difference.")");
666
+ die;
667
+ }
668
+
669
+ // The sequence number should always be larger than any previously-sent sequence number
670
+ if ($this->sequence_protection_tolerance) {
671
+
672
+ if ($this->debug) $this->log("Sequence protection is active; tolerance: ".$this->sequence_protection_tolerance);
673
+
674
+ global $wpdb;
675
+
676
+ if (!isset($udrpc_message['sequence_id']) || !is_numeric($udrpc_message['sequence_id'])) {
677
+ $this->log("a numerical sequence number is required, but none was included in the message - dropping");
678
+ die;
679
+ }
680
+
681
+ $message_sequence_id = (int)$udrpc_message['sequence_id'];
682
+ $recently_seen_sequences_ids = $wpdb->get_var($wpdb->prepare("SELECT %s FROM %s LIMIT 1 WHERE ".$this->sequence_protection_where_sql, $this->sequence_protection_column, $this->sequence_protection_table));
683
+
684
+ if ('' === $recently_seen_sequences_ids) $recently_seen_sequences_ids = '0';
685
+
686
+ $recently_seen_sequences_ids_as_array = explode($recently_seen_sequences_ids, ',');
687
+ sort($recently_seen_sequences_ids_as_array);
688
+
689
+ // Seen before?
690
+ if (in_array($message_sequence_id, $recently_seen_sequences_ids_as_array)) {
691
+ $this->log("message with duplicate sequence number received - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids)");
692
+ die;
693
+ }
694
+
695
+ // Within the tolerance threshold? That means: a) either bigger than the max, or b) no more than <tolerance> lower than the least
696
+ if ($message_sequence_id > max($recently_seen_sequences_ids)) {
697
+ if ($this->debug) $this->log("Sequence id ($message_sequence_id) is greater than any previous (".max($recently_seen_sequences_ids).") - message is thus OK");
698
+ // All is well
699
+ $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
700
+ } elseif (max($recently_seen_sequences_ids) - $message_sequence_id <= $this->sequence_protection_tolerance) {
701
+ // All is well - was one of those 'missing' in the sequence
702
+ if ($this->debug) $this->log("Sequence id ($message_sequence_id) is within tolerance range of previous maximum (".max($recently_seen_sequences_ids).") - message is thus OK");
703
+ $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
704
+ } else {
705
+ $this->log("message received outside of allowed sequence window - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids, tolerance=".$this->sequence_protection_tolerance.")");
706
+ die;
707
+ }
708
+
709
+ // Remove out-of-bounds seen IDs
710
+ $max_sequence_id_seen = max($recently_seen_sequences_ids_as_array);
711
+ foreach ($recently_seen_sequences_ids_as_array as $k => $id) {
712
+ if ($max_sequence_id_seen - $id > $this->sequence_protection_tolerance) {
713
+ if ($this->debug) $this->log("Removing no-longer-relevant sequence from list of those recently seen: $id");
714
+ unset($recently_seen_sequences_ids_as_array[$k]);
715
+ }
716
+ }
717
+
718
+ // Allow reset
719
+ if ($current_sequence_id > PHP_INT_MAX - 10) {
720
+ $recently_seen_sequences_ids_as_array = array(0);
721
+ }
722
+
723
+ // Write them back to the database
724
+ $sql = $wpdb->prepare("UPDATE %s SET %s=%s WHERE ".$this->sequence_protection_where_sql, $this->sequence_protection_table, $this->sequence_protection_column, implode(',', $recently_seen_sequences_ids_as_array));
725
+ if ($this->debug) $this->log("SQL to send recent sequence IDs back to the database: $sql");
726
+ $wpdb->query($sql);
727
+
728
+ }
729
+
730
+ $command = (string)$udrpc_message['command'];
731
+ $data = empty($udrpc_message['data']) ? null : $udrpc_message['data'];
732
+
733
+ if ($http_origin) {
734
+ header("Access-Control-Allow-Origin: $http_origin");
735
+ header("Access-Control-Allow-Credentials: true");
736
+ }
737
+
738
+ $this->log("Command received: ".$command, 'info');
739
+
740
+ if ('ping' == $command) {
741
+ echo json_encode($this->create_message('pong', null, true));
742
+ } else {
743
+ if (has_filter('udrpc_command_'.$command)) {
744
+ $command_action_hooked = true;
745
+ $response = apply_filters('udrpc_command_'.$command, null, $data, $this->key_name_indicator);
746
+ } else {
747
+ $response = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'command' => $command));
748
+ }
749
+
750
+ $response = apply_filters('udrpc_action', $response, $command, $data, $this->key_name_indicator, $this);
751
+
752
+ if (is_array($response)) {
753
+
754
+ if ($this->debug) {
755
+ $this->log("UDRPC response (pre-encoding/encryption): ".serialize($response));
756
+ }
757
+
758
+ $data = isset($response['data']) ? $response['data'] : null;
759
+ echo json_encode($this->create_message($response['response'], $data, true));
760
+ }
761
+
762
+ }
763
+
764
+ die;
765
+
766
+ }
767
+
768
+ // The hash needs to be in a format that phpseclib likes. phpseclib uses lower case.
769
+ // Pass in a base64-encoded signature (i.e. just as signature_for_message creates)
770
+ // Returns a boolean
771
+ public function verify_signature($message, $signature, $key, $hash_algorithm = 'sha256') {
772
+ $this->ensure_crypto_loaded();
773
+ $rsa = new Crypt_RSA();
774
+ $rsa->setHash(strtolower($hash_algorithm));
775
+ // This is not the default, but is what we use
776
+ $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
777
+ $rsa->loadKey($key);
778
+
779
+ // Don't hash it - Crypt_RSA::verify() already does that
780
+ // $hash = new Crypt_Hash($hash_algorithm);
781
+ // $hashed = $hash->hash($message);
782
+
783
+ $verified = $rsa->verify($message, base64_decode($signature));
784
+
785
+ if ($this->debug) $this->log("Signature verification result: ".serialize($verified));
786
+
787
+ return $verified;
788
+ }
789
+
790
+ private function calculate_message_hash($message) {
791
+ return hash('sha256', $message);
792
+ }
793
+
794
+ private function message_hash_seen($message_hash) {
795
+ // 39 characters - less than the WP site transient name limit (40). Though, we use a normal transient, as these don't auto-load at all times.
796
+ $transient_name = 'udrpch_'.md5($this->key_name_indicator);
797
+ $seen_hashes = get_transient($transient_name);
798
+ if (!is_array($seen_hashes)) $seen_hashes = array();
799
+ $time_now = time();
800
+ // $any_changes = false;
801
+ // Prune the old hashes
802
+ foreach ($seen_hashes as $hash => $last_seen) {
803
+ if ($last_seen < $time_now - $this->maximum_replay_time_difference) {
804
+ // $any_changes = true;
805
+ unset($seen_hashes[$hash]);
806
+ }
807
+ }
808
+ if (isset($seen_hashes[$message_hash])) {
809
+ return true;
810
+ }
811
+ $seen_hashes[$message_hash] = $time_now;
812
+ set_transient($transient_name, $seen_hashes, $this->maximum_replay_time_difference);
813
+ return false;
814
+ }
815
+
816
+ }
817
+ endif;
includes/updraft-admin-ui.js CHANGED
@@ -16,6 +16,19 @@ function updraft_delete(key, nonce, showremote) {
16
  jQuery('#updraft-delete-modal').dialog('open');
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  function updraft_deleteallselected() {
20
  var howmany = 0;
21
  var remote_exists = 0;
@@ -112,6 +125,12 @@ function updraft_backup_dialog_open() {
112
  }
113
  }
114
 
 
 
 
 
 
 
115
 
116
  function updraft_migrate_dialog_open() {
117
  jQuery('#updraft_migrate_modal_alt').hide();
@@ -914,7 +933,6 @@ function updraft_backupnow_go(backupnow_nodb, backupnow_nofiles, backupnow_noclo
914
  extradata: extradata
915
  };
916
 
917
-
918
  if ('' != onlythesefileentities) {
919
  params.onlythisfileentity = onlythesefileentities;
920
  }
@@ -981,22 +999,92 @@ jQuery(document).ready(function($){
981
  e.preventDefault();
982
  updraft_updatehistory(1, 1);
983
  });
 
 
 
 
 
 
 
 
 
 
984
 
985
- // Remote Control
986
- jQuery('#updraft_central_keycreate_go').click(function(e) {
 
 
 
 
 
987
  e.preventDefault();
988
- jQuery('#updraft_central_key').html(updraftlion.creating);
989
  try {
990
  jQuery.post(ajaxurl, {
991
  action: 'updraft_ajax',
992
- subaction: 'remotecontrol_createkey',
993
  nonce: updraft_credentialtest_nonce
994
  }, function(response) {
 
995
  try {
996
  resp = jQuery.parseJSON(response);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
  alert(resp.r);
 
 
 
998
  if (resp.hasOwnProperty('bundle')) {
999
- jQuery('#updraft_central_key').html('<textarea onclick="this.select();" style="width:620px; height:165px; word-wrap:break-word; border: 1px solid #aaa; border-radius: 3px; padding:4px;">'+resp.bundle+'</textarea>');
1000
  } else {
1001
  console.log(resp);
1002
  }
@@ -1011,6 +1099,36 @@ jQuery(document).ready(function($){
1011
  }
1012
  });
1013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1014
  jQuery('#updraft_reset_sid').click(function(e) {
1015
  e.preventDefault();
1016
  jQuery.post(ajaxurl, {
@@ -1380,15 +1498,7 @@ jQuery(document).ready(function($){
1380
  var backupnow_nofiles = jQuery('#backupnow_includefiles').is(':checked') ? 0 : 1;
1381
  var backupnow_nocloud = jQuery('#backupnow_includecloud').is(':checked') ? 0 : 1;
1382
 
1383
- var onlythesefileentities = '';
1384
- jQuery('#backupnow_includefiles_moreoptions input[type="checkbox"]').each(function(index) {
1385
- if (!jQuery(this).is(':checked')) { return; }
1386
- var name = jQuery(this).attr('name');
1387
- if (name.substring(0, 16) != 'updraft_include_') { return; }
1388
- var entity = name.substring(16);
1389
- if (onlythesefileentities != '') { onlythesefileentities += ','; }
1390
- onlythesefileentities += entity;
1391
- });
1392
 
1393
  if ('' == onlythesefileentities && 0 == backupnow_nofiles) {
1394
  alert(updraftlion.nofileschosen);
@@ -1919,6 +2029,14 @@ jQuery(document).ready(function($){
1919
  var my_image = new Image();
1920
  my_image.src = updraftlion.ud_url+'/images/udlogo-rotating.gif';
1921
 
 
 
 
 
 
 
 
 
1922
  $('#updraftplus-settings-save').click(function(e) {
1923
  e.preventDefault();
1924
  $.blockUI({ message: '<div style="margin: 8px; font-size:150%;"><img src="'+updraftlion.ud_url+'/images/udlogo-rotating.gif" height="80" width="80" style="padding-bottom:10px;"><br>'+updraftlion.saving+'</div>'});
16
  jQuery('#updraft-delete-modal').dialog('open');
17
  }
18
 
19
+ function backupnow_whichfiles_checked(onlythesefileentities){
20
+ jQuery('#backupnow_includefiles_moreoptions input[type="checkbox"]').each(function(index) {
21
+ if (!jQuery(this).is(':checked')) { return; }
22
+ var name = jQuery(this).attr('name');
23
+ if (name.substring(0, 16) != 'updraft_include_') { return; }
24
+ var entity = name.substring(16);
25
+ if (onlythesefileentities != '') { onlythesefileentities += ','; }
26
+ onlythesefileentities += entity;
27
+ });
28
+ // console.log(onlythesefileentities);
29
+ return onlythesefileentities;
30
+ }
31
+
32
  function updraft_deleteallselected() {
33
  var howmany = 0;
34
  var remote_exists = 0;
125
  }
126
  }
127
 
128
+ var onlythesefileentities = backupnow_whichfiles_checked('');
129
+ if ('' == onlythesefileentities) {
130
+ jQuery("#backupnow_includefiles_moreoptions").show();
131
+ } else {
132
+ jQuery("#backupnow_includefiles_moreoptions").hide();
133
+ }
134
 
135
  function updraft_migrate_dialog_open() {
136
  jQuery('#updraft_migrate_modal_alt').hide();
933
  extradata: extradata
934
  };
935
 
 
936
  if ('' != onlythesefileentities) {
937
  params.onlythisfileentity = onlythesefileentities;
938
  }
999
  e.preventDefault();
1000
  updraft_updatehistory(1, 1);
1001
  });
1002
+
1003
+ function updraftcentral_keys_setupform(on_page_load) {
1004
+ var is_other = jQuery('#updraftcentral_mothership_other').is(':checked') ? true : false;
1005
+ if (is_other) {
1006
+ jQuery('#updraftcentral_keycreate_mothership').prop('disabled', false);
1007
+ if (!on_page_load) { jQuery('#updraftcentral_keycreate_mothership').focus(); }
1008
+ } else {
1009
+ jQuery('#updraftcentral_keycreate_mothership').prop('disabled', true);
1010
+ }
1011
+ }
1012
 
1013
+ jQuery('#updraftcentral_keys').on('change', 'input[type="radio"]', function() {
1014
+ updraftcentral_keys_setupform(false);
1015
+ });
1016
+ // Initial setup (for browsers, e.g. Firefox, that remember form selection state but not DOM state, which can leave an inconsistent state)
1017
+ updraftcentral_keys_setupform(true);
1018
+
1019
+ jQuery('#updraftcentral_keys').on('click', '#updraftcentral_view_log', function(e) {
1020
  e.preventDefault();
1021
+ jQuery('#updraftcentral_view_log_container').block({ message: '<div style="margin: 8px; font-size:150%;"><img src="'+updraftlion.ud_url+'/images/udlogo-rotating.gif" height="80" width="80" style="padding-bottom:10px;"><br>'+updraftlion.fetching+'</div>'});
1022
  try {
1023
  jQuery.post(ajaxurl, {
1024
  action: 'updraft_ajax',
1025
+ subaction: 'updraftcentral_get_log',
1026
  nonce: updraft_credentialtest_nonce
1027
  }, function(response) {
1028
+ jQuery('#updraftcentral_view_log_container').unblock();
1029
  try {
1030
  resp = jQuery.parseJSON(response);
1031
+ if (resp.hasOwnProperty('log_contents')) {
1032
+ jQuery('#updraftcentral_view_log_contents').html('<div style="border:1px solid;padding: 2px;max-height: 400px; overflow-y:scroll;">'+resp.log_contents+'</div>');
1033
+ } else {
1034
+ console.log(resp);
1035
+ }
1036
+ } catch (err) {
1037
+ alert(updraftlion.unexpectedresponse+' '+