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 / restorer.php

restorer.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at restorer.php

3,407 lines 153.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
4
5 if (!class_exists('Updraft_Restorer_Skin')) require_once(UPDRAFTPLUS_DIR.'/includes/updraft-restorer-skin.php');
6
7 class Updraft_Restorer {
8
9 // This just stores the result of is_multisite()
10 private $is_multisite;
11
12 // This is just used so far for detecting whether we're on the second run for an entity or not.
13 public $been_restored = array();
14
15 private $tables_been_dropped = array();
16
17 // Public: it is manipulated by the caller after the caller gets the object
18 public $delete = false;
19
20 private $created_by_version = false;
21
22 // This one can be set externally, if the information is available
23 public $ud_backup_is_multisite = -1;
24
25 private $ud_backup_set;
26
27 public $ud_foreign;
28
29 // store restored table names
30 public $restored_table_names = array();
31
32 public $is_dummy_db_restore = false;
33
34 // The default of false means "use the global $wpdb"
35 private $wpdb_obj = false;
36
37 private $line_last_logged = 0;
38
39 private $our_siteurl;
40
41 private $configuration_bundle;
42
43 private $ajax_restore_auth_code;
44
45 private $restore_options;
46
47 private $restore_this_site = array();
48
49 private $restore_this_table = array();
50
51 private $line = 0;
52
53 private $statements_run = 0;
54
55 private $use_wpdb = null;
56
57 private $import_table_prefix = null;
58
59 private $continuation_data;
60
61 // Constants for use with the move_backup_in method
62 // These can't be arbitrarily changed; there is legacy code doing bitwise operations and numerical comparisons, and possibly legacy code still using the values directly.
63 const MOVEIN_OVERWRITE_NO_BACKUP = 0;
64 const MOVEIN_MAKE_BACKUP_OF_EXISTING = 1;
65 const MOVEIN_DO_NOTHING_IF_EXISTING = 2;
66 const MOVEIN_COPY_IN_CONTENTS = 3;
67
68 private $wp_upgrader;
69
70 public $skin = null;
71
72 public $strings = array();
73
74 /**
75 * Constructor
76 *
77 * @param WP_Upgrader_Skin|Null $skin - an upgrader skin
78 * @param Array|Null $backup_set - the backup set to restore
79 * @param Boolean $short_init - whether just to do a minimal initialisation
80 * @param Array $restore_options - options to guide the restoration
81 * @param Array|Null $continuation_data - continuation data; the jobdata of the job thus far (but only a few properties are used - including second_loop_entities; $restore_options will have come from there too if relevant, but that is passed in here separately); the 'last_index_*' entries also indicate unzipping progress
82 */
83 public function __construct($skin = null, $backup_set = null, $short_init = false, $restore_options = array(), $continuation_data = null) {
84
85 global $wpdb, $updraftplus;
86
87 $this->our_siteurl = untrailingslashit(site_url());
88
89 $this->continuation_data = $continuation_data;
90
91 // Line up a wpdb-like object
92 if (!$this->use_wpdb()) {
93 // We have our own extension which drops lots of the overhead on the query
94 $wpdb_obj = new UpdraftPlus_WPDB(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
95 // Was that successful?
96 if (!$wpdb_obj->is_mysql || !$wpdb_obj->ready) {
97 $this->use_wpdb = true;
98 } else {
99 $this->wpdb_obj = $wpdb_obj;
100 $this->mysql_dbh = $wpdb_obj->updraftplus_get_database_handle();
101 $this->use_mysqli = $wpdb_obj->updraftplus_use_mysqli();
102 }
103 }
104
105 if ($short_init) return;
106
107 // If updraft_incremental_restore_point is equal to -1 then this is either not a incremental restore or we are going to restore up to the latest increment, so there is no need to prune the backup set of any unwanted backup archives.
108 if (isset($restore_options['updraft_incremental_restore_point']) && $restore_options['updraft_incremental_restore_point'] > 0) {
109 $restore_point = $restore_options['updraft_incremental_restore_point'];
110 foreach ($backup_set['incremental_sets'] as $increment_timestamp => $entities) {
111 if ($increment_timestamp > $restore_point) {
112 foreach ($entities as $entity => $backups) {
113 foreach ($backups as $key => $value) {
114 unset($backup_set[$entity][$key]);
115 }
116 }
117 }
118 }
119 }
120
121 // Restore in the most helpful order
122 uksort($backup_set, array('UpdraftPlus_Manipulation_Functions', 'sort_restoration_entities'));
123
124 $this->ud_backup_set = $backup_set;
125
126 add_filter('updraftplus_logline', array($this, 'updraftplus_logline'), 10, 5);
127
128 do_action('updraftplus_restorer_restore_options', $restore_options);
129 $this->ud_multisite_selective_restore = (is_array($restore_options) && !empty($restore_options['updraft_restore_ms_whichsites']) && $restore_options['updraft_restore_ms_whichsites'] > 0) ? $restore_options['updraft_restore_ms_whichsites'] : false;
130 $this->restore_options = $restore_options;
131
132 $this->ud_foreign = empty($backup_set['meta_foreign']) ? false : $backup_set['meta_foreign'];
133 if (isset($backup_set['is_multisite'])) $this->ud_backup_is_multisite = $backup_set['is_multisite'];
134 if (isset($backup_set['created_by_version'])) $this->created_by_version = $backup_set['created_by_version'];
135
136 $this->backup_strings();
137
138 $this->is_multisite = is_multisite();
139
140 if (!class_exists('WP_Upgrader')) include_once(ABSPATH.'wp-admin/includes/class-wp-upgrader.php');
141 $this->skin = $skin;
142 $this->wp_upgrader = new WP_Upgrader($skin);
143 $this->wp_upgrader->init();
144 }
145
146 /**
147 * Get the wpdb-like object that we are using, if we are using one
148 *
149 * @return UpdraftPlus_WPDB|Boolean
150 */
151 public function get_db_object() {
152 return $this->wpdb_obj;
153 }
154
155 /**
156 * Restore has been completed - clean some things up
157 *
158 * @param Boolean|WP_Error $successful - if the restore was successful (true) or not (false or WP_Error). If not, then only a minimum of necessary clean-up things is done.
159 * @param Boolean $browser_context - if true, then extra messages will be echo-ed
160 *
161 * @uses UpdraftPlus::log()
162 */
163 public function post_restore_clean_up($successful = true, $browser_context = true) {
164
165 global $updraftplus, $updraftplus_admin;
166
167 if (is_wp_error($successful)) {
168 foreach ($successful->get_error_codes() as $code) {
169 if ('already_exists' == $code) {
170 if ($browser_context) {
171 global $updraftplus_admin;
172 $updraftplus_admin->print_delete_old_dirs_form(false);
173 } else {
174 $updraftplus->log(__('Your WordPress install has old directories from its state before you restored/migrated (technical information: these are suffixed with -old).', 'updraftplus'));
175 }
176 }
177 $data = $successful->get_error_data($code);
178 if (!empty($data)) {
179 $pdata = is_string($data) ? $data : serialize($data);
180 $updraftplus->log(__('Error data:', 'updraftplus').' '.$pdata, 'warning-restore');
181 if (false !== strpos($pdata, 'PCLZIP_ERR_BAD_FORMAT (-10)')) {
182 if ($browser_context) {
183 echo '<a href="'.apply_filters('updraftplus_com_link', 'https://updraftplus.com/faqs/error-message-pclzip_err_bad_format-10-invalid-archive-structure-mean/').'" target="_blank"><strong>'.__('Follow this link for more information', 'updraftplus').'</strong></a><br>';
184 } else {
185 $updraftplus->log(__('Follow this link for more information', 'updraftplus').': '.$url);
186 }
187 }
188 }
189
190 }
191 $successful = false;
192 }
193
194 // From this point on, $successful is a boolean
195 if ($successful) {
196 // All done - remove the intermediate marker
197 delete_site_option('updraft_restore_in_progress');
198
199 foreach (array('template', 'stylesheet', 'template_root', 'stylesheet_root') as $opt) {
200 add_filter('pre_option_'.$opt, array($this, 'option_filter_'.$opt));
201 }
202
203 // Clear any cached pages after the restore
204 $this->clear_cache();
205
206 // Have seen a case where the current theme in the DB began with a capital, but not on disk - and this breaks migrating from Windows to a case-sensitive system
207 $template = get_option('template');
208 if (!empty($template) && WP_DEFAULT_THEME != $template && strtolower($template) != $template) {
209
210 $theme_root = get_theme_root($template);
211 $theme_root2 = get_theme_root(strtolower($template));
212
213 if (!file_exists("$theme_root/$template/style.css") && file_exists("$theme_root/".strtolower($template)."/style.css")) {
214 $updraftplus->log_e("Theme directory (%s) not found, but lower-case version exists; updating database option accordingly", $template);
215 update_option('template', strtolower($template));
216 }
217
218 }
219
220 if (!function_exists('validate_current_theme')) include_once(ABSPATH.WPINC.'/themes');
221
222 if (!validate_current_theme()) {
223 if ($browser_context) echo '<strong>';
224 $updraftplus->log_e("The current theme was not found; to prevent this stopping the site from loading, your theme has been reverted to the default theme");
225 if ($browser_context) echo '</strong>';
226 }
227
228 do_action('updraftplus_restore_completed');
229 }
230
231 if ($browser_context) echo '</div>'; // Close the updraft_restore_progress div
232
233 restore_error_handler();
234
235 }
236
237 /**
238 * Whether or not we must use the global $wpdb object for database queries.
239 * That is to say: we *can* always use it. But we prefer to avoid the overhead since we are potentially doing very many queries.
240 *
241 * This is the getter. We have no use-case for a setter outside of this class, so we just set it directly.
242 *
243 * @return Boolean
244 */
245 public function use_wpdb() {
246 if (!is_bool($this->use_wpdb)) {
247 global $wpdb;
248 if (defined('UPDRAFTPLUS_USE_WPDB')) {
249 $this->use_wpdb = (bool) UPDRAFTPLUS_USE_WPDB;
250 } else {
251 $this->use_wpdb = ((!function_exists('mysql_query') && !function_exists('mysqli_query')) || !$wpdb->is_mysql || !$wpdb->ready) ? true : false;
252 }
253 }
254 return $this->use_wpdb;
255 }
256
257 /**
258 * Get the skin
259 *
260 * @return WP_Upgrader_Skin
261 */
262 public function ud_get_skin() {
263 return $this->skin;
264 }
265
266 /**
267 * Ensure that needed files are present locally, and return data for the next step (plus do some internal configuration)
268 *
269 * @param Array $entities_to_restore - as returned by self::get_entities_to_restore()
270 * @param Array $backupable_entities - list of entities that can be backed u
271 * @param Array $services - list of services that the backup can be found at
272 *
273 * @uses self::pre_restore_backup() (and some other internal properties)
274 * @uses UpdraftPlus::log()
275 *
276 * @return Boolean|Array|WP_Error - a sorted array (of entity types and files for each entity type) or false or a WP_Error if there was an error
277 */
278 private function ensure_restore_files_present($entities_to_restore, $backupable_entities, $services) {
279
280 global $updraftplus;
281
282 $entities_to_download = $this->get_entities_to_download($entities_to_restore);
283
284 $backup_set = $this->ud_backup_set;
285 $timestamp = $backup_set['timestamp'];
286
287 $updraft_dir = $updraftplus->backups_dir_location();
288 $foreign_known = apply_filters('updraftplus_accept_archivename', array());
289
290 // First loop: make sure that files are present + readable; and populate array for second loop
291 foreach ($backup_set as $type => $files) {
292
293 // All restorable entities must be given explicitly, as we can store other arbitrary data in the history array
294 if (!isset($backupable_entities[$type]) && 'db' != $type) continue;
295
296 if (isset($backupable_entities[$type]['restorable']) && false == $backupable_entities[$type]['restorable']) continue;
297
298 if (!isset($entities_to_download[$type])) continue;
299
300 if ('wpcore' == $type && is_multisite() && 0 === $this->ud_backup_is_multisite) {
301 $updraftplus->log('wpcore: '.__('Skipping restoration of WordPress core when importing a single site into a multisite installation. If you had anything necessary in your WordPress directory then you will need to re-add it manually from the zip file.', 'updraftplus'), 'notice-restore');
302 // TODO
303 // $updraftplus->log_e('Skipping restoration of WordPress core when importing a single site into a multisite installation. If you had anything necessary in your WordPress directory then you will need to re-add it manually from the zip file.');
304 continue;
305 }
306
307 if (is_string($files)) $files = array($files);
308
309 foreach ($files as $ind => $file) {
310
311 $fullpath = $updraft_dir.'/'.$file;
312 $updraftplus->log(sprintf(__("Looking for %s archive: file name: %s", 'updraftplus'), $type, $file), 'notice-restore');
313
314 if (is_array($this->continuation_data) && isset($this->continuation_data['second_loop_entities'][$type]) && !in_array($file, $this->continuation_data['second_loop_entities'][$type])) {
315 $updraftplus->log(__('Skipping: this archive was already restored.', 'updraftplus'), 'notice-restore');
316 // Set the marker so that the existing directory isn't moved out of the way
317 $this->been_restored[$type] = true;
318 continue;
319 }
320
321 if (!is_readable($fullpath) || 0 == filesize($fullpath)) UpdraftPlus_Storage_Methods_Interface::get_remote_file($services, $file, $timestamp, true);
322
323 $index = (0 == $ind) ? '' : $ind;
324 // If a file size is stored in the backup data, then verify correctness of the local file
325 if (isset($backup_set[$type.$index.'-size'])) {
326 $fs = $backup_set[$type.$index.'-size'];
327 $print_message = __("Archive is expected to be size:", 'updraftplus')." ".round($fs/1024, 1)." KB: ";
328 $as = @filesize($fullpath);
329 if ($as == $fs) {
330 $updraftplus->log($print_message.__('OK', 'updraftplus'), 'notice-restore');
331 } else {
332 $updraftplus->log($print_message.__('Error:', 'updraftplus')." ".__('file is size:', 'updraftplus')." ".round($as/1024)." ($fs, $as)", 'warning-restore');
333 }
334 } else {
335 $updraftplus->log(__("The backup records do not contain information about the proper size of this file.", 'updraftplus'), 'notice-restore');
336 }
337 if (!is_readable($fullpath)) {
338 $updraftplus->log(__('Could not find one of the files for restoration', 'updraftplus')." ($file)", 'warning-restore');
339 $updraftplus->log("$file: ".__('Could not find one of the files for restoration', 'updraftplus'), 'error');
340 return false;
341 }
342 }
343
344 if (empty($this->ud_foreign)) {
345 $types = array($type);
346 } else {
347 if ('db' != $type || empty($foreign_known[$this->ud_foreign]['separatedb'])) {
348 $types = array('wpcore');
349 } else {
350 $types = array('db');
351 }
352 }
353
354 foreach ($types as $check_type) {
355 $info = isset($backupable_entities[$check_type]) ? $backupable_entities[$check_type] : array();
356 $val = $this->pre_restore_backup($files, $check_type, $info);
357 if (is_wp_error($val)) {
358 $updraftplus->log_wp_error($val);
359 foreach ($val->get_error_messages() as $msg) {
360 $updraftplus->log(__('Error:', 'updraftplus').' '.$msg, 'warning-restore');
361 }
362 return $val;
363 } elseif (false === $val) {
364 return false;
365 }
366 }
367
368 foreach ($entities_to_restore as $entity => $via) {
369 if ($via == $type) {
370 if ('wpcore' == $via && 'db' == $entity && count($files) > 1) {
371 $second_loop[$entity] = apply_filters('updraftplus_select_wpcore_file_with_db', $files, $this->ud_foreign);
372 } else {
373 $second_loop[$entity] = $files;
374 }
375 }
376 }
377
378 }
379
380 $this->delete = UpdraftPlus_Options::get_updraft_option('updraft_delete_local', 1) ? true : false;
381 if (empty($services) || array('email') === $services || !empty($this->ud_foreign)) {
382 if ($this->delete) $updraftplus->log_e('Will not delete any archives after unpacking them, because there was no cloud storage for this backup');
383 $this->delete = false;
384 }
385
386 if (!empty($this->ud_foreign)) $updraftplus->log("Foreign backup; created by: ".$this->ud_foreign);
387
388 // Second loop: now actually do the restoration
389 uksort($second_loop, array('UpdraftPlus_Manipulation_Functions', 'sort_restoration_entities'));
390
391 // If continuing, then prune those already done
392 if (is_array($this->continuation_data)) {
393 foreach ($second_loop as $type => $files) {
394 if (isset($this->continuation_data['second_loop_entities'][$type])) $second_loop[$type] = $this->continuation_data['second_loop_entities'][$type];
395 }
396 }
397
398 return $second_loop;
399 }
400
401 /**
402 * Perform the restoration. No code here (or called) should assume anything about the method used to call it (e.g. wp-admin or WP-CLI); it should be independent of how it is being called.
403 *
404 * The path through this class is perform_restore() -> restore_backup() -> unpack_package() -> unpack_package_(archive|database) and then (for standard UD archives) UpdraftPlus_Filesystem_Functions::unzip_file()
405 *
406 * @param Array $entities_to_restore - entities to restore
407 * @param Array $restore_options - restoration options
408 *
409 * @uses the WordPress action updraftplus_restoration_title, allowing the title to be printed
410 *
411 * @return Boolean
412 */
413 public function perform_restore($entities_to_restore, $restore_options) {
414
415 global $updraftplus;
416
417 // Now log. We first remove any encryption passphrase from the log data.
418 $copy_restore_options = $restore_options;
419 if (!empty($copy_restore_options['updraft_encryptionphrase'])) $copy_restore_options['updraft_encryptionphrase'] = '***';
420 $updraftplus->log("Restore job started. Entities to restore: ".implode(', ', array_flip($entities_to_restore)).'. Restore options: '.json_encode($copy_restore_options));
421
422 do_action('updraftplus_restoration_title', __('Final checks', 'updraftplus'));
423
424 $backup_set = $this->ud_backup_set;
425
426 $services = isset($backup_set['service']) ? $updraftplus->get_canonical_service_list($backup_set['service']) : array();
427
428 $foreign_known = apply_filters('updraftplus_accept_archivename', array());
429
430 $entities_to_download = $this->get_entities_to_download($entities_to_restore);
431
432 $backupable_entities = $updraftplus->get_backupable_file_entities(true, true);
433
434 $remove_zip = isset($restore_options['delete_during_restore']) ? $restore_options['delete_during_restore'] : false;
435
436 if (!empty($restore_options['dummy_db_restore'])) {
437 $this->is_dummy_db_restore = true;
438 add_filter('updraftplus_restore_table_prefix', array($this, 'updraftplus_restore_table_prefix_dummy'));
439 }
440
441 // Allow add-ons to adjust the restore directory (but only in the case of restore - otherwise, they could just use the filter built into UpdraftPlus::get_backupable_file_entities)
442 $backupable_entities = apply_filters('updraft_backupable_file_entities_on_restore', $backupable_entities, $restore_options, $backup_set);
443
444 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
445
446 // Get an ordered list of things to restore
447 // This requires the global $updraft_restorer to be set up
448 $second_loop = $this->ensure_restore_files_present($entities_to_download, $backupable_entities, $services);
449
450 if (!is_array($second_loop)) return $second_loop;
451
452 $timestamp = $backup_set['timestamp'];
453
454 $updraftplus->jobdata_set('second_loop_entities', $second_loop);
455 $updraftplus->jobdata_set('backup_timestamp', $timestamp);
456
457 // Use a site option, as otherwise on multisite when all the array of options is updated via UpdraftPlus_Options::update_site_option(), it will over-write any restored UD options from the backup
458 update_site_option('updraft_restore_in_progress', $updraftplus->nonce);
459
460 // Now process the actual restoration of the entities
461 foreach ($second_loop as $type => $files) {
462
463 // Types: uploads, themes, plugins, others, db
464 $info = isset($backupable_entities[$type]) ? $backupable_entities[$type] : array();
465
466 $restoration_title = ('db' == $type) ? __('Database', 'updraftplus') : $info['description'];
467
468 do_action('updraftplus_restoration_title', $restoration_title);
469
470 $updraftplus->log('Entity: '.$type);
471
472 if (is_string($files)) $files = array($files);
473
474 // Don't assume that the caller pre-sorted the array. We do need it sorted, so that incremental zips get restored in the right order
475 ksort($files);
476
477 foreach ($files as $fkey => $file) {
478 $last_one = (1 == count($second_loop) && 1 == count($files));
479 $last_entity = (1 == count($files));
480 try {
481 // Returns a boolean or WP_Error
482 $restore_result = $this->restore_backup($file, $type, $info, $last_one, $last_entity);
483 } catch (Exception $e) {
484 $log_message = 'Exception ('.get_class($e).') occurred during restore: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
485 $display_log_message = sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage());
486 error_log($log_message);
487 // @codingStandardsIgnoreLine
488 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
489 $updraftplus->log($log_message);
490 $updraftplus->log($display_log_message, 'notice-restore');
491 die();
492 // @codingStandardsIgnoreLine
493 } catch (Error $e) {
494 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
495 error_log($log_message);
496 // @codingStandardsIgnoreLine
497 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
498 $updraftplus->log($log_message);
499 $display_log_message = sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage());
500 $updraftplus->log($display_log_message, 'notice-restore');
501 die();
502 }
503
504 if (is_wp_error($restore_result)) {
505 $codes = $restore_result->get_error_codes();
506 if (is_array($codes) && in_array('not_found', $codes) && !empty($this->ud_foreign) && apply_filters('updraftplus_foreign_allow_missing_entity', false, $type, $this->ud_foreign)) {
507 $updraftplus->log('Entity to move not found in this zip - but this is possible with this foreign backup type');
508 } else {
509 $updraftplus->log_e($restore_result);
510 foreach ($restore_result->get_error_messages() as $msg) {
511 $updraftplus->log(__('Error message', 'updraftplus').': '.$msg, 'notice-restore');
512 }
513 return $restore_result;
514 }
515 } elseif (false === $restore_result) {
516 return false;
517 } elseif ($restore_result && $remove_zip) {
518 $deleted = unlink($updraftplus->backups_dir_location().'/'.$file);
519 $updraftplus->log("Delete zip during restore active; removing backup file: $file: ".($deleted ? 'OK' : 'Failed'));
520 }
521
522 unset($files[$fkey]);
523 $second_loop[$type] = $files;
524 $updraftplus->jobdata_set_multi(array('second_loop_entities' => $second_loop, 'backup_timestamp' => $timestamp));
525
526 do_action('updraft_restored_archive', $file, $type, $restore_result, $fkey, $timestamp);
527
528 }
529
530 // Update the job data each time we go round the loop, so that if it aborts, it can be resumed from the correct point
531 unset($second_loop[$type]);
532 update_site_option('updraft_restore_in_progress', $updraftplus->nonce);
533 $updraftplus->jobdata_set_multi(array('second_loop_entities' => $second_loop, 'backup_timestamp' => $timestamp));
534 }
535
536 // If the database was restored, then check active plugins and make sure they all exist; otherwise, the site may go down
537 if (null !== $this->import_table_prefix) $this->check_active_plugins($this->import_table_prefix);
538
539 return true;
540 }
541
542 /**
543 * Calculate the entities to download for a given backup set
544 *
545 * @param Array $entities_to_restore - entities to restore, in the format returned by UpdraftPlus_Admin::get_entities_to_restore
546 *
547 * @return Array - keys are entities, and values are 0|1
548 */
549 public function get_entities_to_download($entities_to_restore) {
550
551 $backup_set = $this->ud_backup_set;
552
553 $foreign_known = apply_filters('updraftplus_accept_archivename', array());
554
555 if (empty($backup_set['meta_foreign'])) return $entities_to_restore;
556
557 if (empty($foreign_known[$backup_set['meta_foreign']]['separatedb'])) return array('wpcore' => 1);
558
559 $entities_to_download = array();
560
561 if (in_array('db', $entities_to_restore)) $entities_to_download['db'] = 1;
562
563 if (count($entities_to_restore) > 1 || !in_array('db', $entities_to_restore)) {
564 $entities_to_download['wpcore'] = 1;
565 }
566
567 return $entities_to_download;
568 }
569
570 /**
571 * Logs a line from the restore process, being called from UpdraftPlus::log(). Currently, this means adding it to the browser output log file and either (depending on the constant WP_CLI) echoing it or passing it to a WPCLI method.
572 * Hooks the WordPress filter updraftplus_logline
573 * In future, this can get more sophisticated. For now, things are funnelled through here, giving the future possibility.
574 *
575 * @param String $line the line to be logged
576 * @param String $nonce the job ID of the restore job
577 * @param String $level the level of the log notice
578 * @param String|Boolean $uniq_id a unique ID for the log if it should only be logged once; or false otherwise
579 * @param String $destination the type of job ongoing. If it is not 'restore', then we will skip the logging.
580 * @return The filtered value. If set to false, then UpdraftPlus::log() will stop processing the log line.
581 */
582 public function updraftplus_logline($line, $nonce, $level, $uniq_id, $destination) {
583 if ('restore' != $destination) return $line;
584
585 global $updraftplus;
586 static $logfile_handle;
587 static $opened_log_time;
588
589 if (empty($logfile_handle)) {
590 $logfile_name = $updraftplus->backups_dir_location()."/log.$nonce-browser.txt";
591 $logfile_handle = fopen($logfile_name, 'a');
592 }
593
594 if (!empty($logfile_handle)) {
595 $rtime = microtime(true)-$updraftplus->job_time_ms;
596 fwrite($logfile_handle, sprintf("%08.03f", round($rtime, 3))." (R) ".'['.$level.'] '.$line."\n");
597 }
598 if (defined('WP_CLI') && WP_CLI) {
599 switch ($level) {
600 case 'error':
601 case 'warning':
602 // WP_CLI::error() displays message with the prefix "Error: ", We don't like message which are double prefixed like the "Error: Error: ".
603 if (0 === stripos($line, 'Error: ')) {
604 $log_line = substr($line, 7);
605 } else {
606 $log_line = $line;
607 }
608 WP_CLI::error($log_line, false);
609 break;
610 case 'notice':
611 default:
612 WP_CLI::log($line, false);
613 break;
614 }
615 } else {
616 if ('warning' == $destination || 'error' == $destination || $uniq_id) {
617 $line = '<strong>'.htmlspecialchars($line).'</strong>';
618 } else {
619 $line = htmlspecialchars($line);
620 }
621
622 echo $line.'<br>';
623 }
624 return false;
625 }
626
627 private function backup_strings() {
628 $this->strings['not_possible'] = __('UpdraftPlus is not able to directly restore this kind of entity. It must be restored manually.', 'updraftplus');
629 $this->strings['no_package'] = __('Backup file not available.', 'updraftplus');
630 $this->strings['copy_failed'] = __('Copying this entity failed.', 'updraftplus');
631 $this->strings['unpack_package'] = __('Unpacking backup...', 'updraftplus');
632 $this->strings['decrypt_database'] = __('Decrypting database (can take a while)...', 'updraftplus');
633 $this->strings['decrypted_database'] = __('Database successfully decrypted.', 'updraftplus');
634 $this->strings['moving_old'] = __('Moving old data out of the way...', 'updraftplus');
635 $this->strings['moving_backup'] = __('Moving unpacked backup into place...', 'updraftplus');
636 $this->strings['restore_database'] = __('Restoring the database (on a large site this can take a long time - if it times out (which can happen if your web hosting company has configured your hosting to limit resources) then you should use a different method, such as phpMyAdmin)...', 'updraftplus');
637 $this->strings['cleaning_up'] = __('Cleaning up rubbish...', 'updraftplus');
638 $this->strings['old_move_failed'] = __('Could not move old files out of the way.', 'updraftplus').' '.__('You should check the file ownerships and permissions in your WordPress installation', 'updraftplus');
639 $this->strings['old_delete_failed'] = __('Could not delete old directory.', 'updraftplus');
640 $this->strings['new_move_failed'] = __('Could not move new files into place. Check your wp-content/upgrade folder.', 'updraftplus');
641 $this->strings['move_failed'] = __('Could not move the files into place. Check your file permissions.', 'updraftplus');
642 $this->strings['delete_failed'] = __('Failed to delete working directory after restoring.', 'updraftplus');
643 $this->strings['multisite_error'] = __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus');
644 $this->strings['unpack_failed'] = __('Failed to unpack the archive', 'updraftplus');
645 $this->strings['read_manifest_failed'] = __('Failed to read the manifest file from backup.', 'updraftplus');
646 $this->strings['manifest_not_found'] = __('Failed to find a manifest file in the backup.', 'updraftplus');
647 $this->strings['read_working_dir_failed'] = __('Failed to read from the working directory.', 'updraftplus');
648 }
649
650 /**
651 * This function is copied from class WP_Upgrader (WP 3.8 - no significant changes since 3.2 at least); we only had to fork it because it hard-codes using the basename of the zip file as its unpack directory; which can be long; and then combining that with long pathnames in the zip being unpacked can overflow a 256-character path limit (yes, they apparently still exist - amazing!)
652 * Subsequently, we have also added the ability to unpack tarballs
653 *
654 * In the 'ordinary' case of unzipping a UD zip backup, this method basically does some preparation, and then calls UpdraftPlus_Filesystem_Functions::unzip_file() for the actual unzipping
655 *
656 * @used-by self::unpack_package()
657 *
658 * @param String $package specify package - full filepath
659 * @param Boolean $delete_package check to delete package
660 * @param String|Boolean $type type of archive e.g. db.
661 *
662 * @return String|WP_Error If successful, then this indicates the working directory that the archive was unpacked in; a WP_Filesystem path
663 */
664 private function unpack_package_archive($package, $delete_package = true, $type = false) {
665
666 global $wp_filesystem, $updraftplus;
667
668 // If it is a non-UD archive that is already unpacked, then don't re-run, but return the existing result
669 if (!empty($this->ud_foreign) && !empty($this->ud_foreign_working_dir) && $package == $this->ud_foreign_package) {
670 if (is_dir($this->ud_foreign_working_dir)) {
671 return $this->ud_foreign_working_dir;
672 } else {
673 $updraftplus->log('Previously unpacked directory seems to have disappeared; will unpack again');
674 }
675 }
676
677 $this->skin->feedback($this->strings['unpack_package'].' ('.basename($package).', '.round(filesize($package)/1048576, 1).' MB)');
678
679 $upgrade_folder = $wp_filesystem->wp_content_dir().'upgrade/';
680
681 $zip_starting_index = 0;
682
683 // We need a working directory. This has a change from the WP core version - minimise path length
684 // N.B. It is deterministic; the same package file will get the same working directory
685 // $working_dir = $upgrade_folder . basename($package, '.zip');
686 $working_dir = $upgrade_folder.substr(md5($package), 0, 8);
687
688 if ('.zip' == strtolower(substr($package, -4, 4))) {
689
690 $last_index_key = UpdraftPlus_Filesystem_Functions::get_jobdata_progress_key($package);
691
692 // Turn off the feature with define('UPDRAFTPLUS_UNZIP_RESUME_ENABLED', false);
693 if ((!defined('UPDRAFTPLUS_UNZIP_RESUME_ENABLED') || UPDRAFTPLUS_UNZIP_RESUME_ENABLED) && !empty($this->continuation_data[$last_index_key]) && !empty($this->continuation_data[$last_index_key]['info']['name'])) {
694
695 $reached = $this->continuation_data[$last_index_key];
696
697 $last_exists = $wp_filesystem->exists($working_dir.'/'.$reached['info']['name']);
698 $last_size = $last_exists ? $wp_filesystem->size($working_dir.'/'.$reached['info']['name']) : 'n/a';
699
700 if ($last_exists && $last_size == $reached['info']['size'] && isset($reached['index'])) $zip_starting_index = $reached['index'];
701
702 $updraftplus->log("Unpack resumption may be possible: zip_starting_index=$zip_starting_index, last_exists=$last_exists, last_size=$last_size, last_status=".serialize($this->continuation_data[$last_index_key]));
703 }
704
705 }
706
707 if (0 == $zip_starting_index) {
708 // Clean up contents of upgrade directory beforehand.
709 $upgrade_files = $wp_filesystem->dirlist($upgrade_folder);
710 if (!empty($upgrade_files)) {
711 foreach ($upgrade_files as $file) {
712 if (!$wp_filesystem->delete($upgrade_folder . $file['name'], true)) {
713 $this->restore_log_permission_failure_message($upgrade_folder, 'Delete '.$upgrade_folder.$file['name']);
714 }
715 }
716 }
717
718 // Clean up working directory - this is redundant, as we already cleared out the parent folder
719 if ($wp_filesystem->is_dir($working_dir)) {
720 if (!$wp_filesystem->delete($working_dir, true)) {
721 $this->restore_log_permission_failure_message(dirname($working_dir), 'Delete '.$working_dir);
722 }
723 }
724 }
725
726 // Unzip package to working directory
727 if ('.zip' == strtolower(substr($package, -4, 4))) {
728
729 $result = UpdraftPlus_Filesystem_Functions::unzip_file($package, $working_dir, $zip_starting_index);
730
731 } elseif ('.tar' == strtolower(substr($package, -4, 4)) || '.tar.gz' == strtolower(substr($package, -7, 7)) || '.tar.bz2' == strtolower(substr($package, -8, 8))) {
732 if (!class_exists('UpdraftPlus_Archive_Tar')) {
733 if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/PEAR')) set_include_path(UPDRAFTPLUS_DIR.'/includes/PEAR'.PATH_SEPARATOR.get_include_path());
734 include_once(UPDRAFTPLUS_DIR.'/includes/PEAR/Archive/Tar.php');
735 }
736
737 $p_compress = null;
738 if ('.tar.gz' == strtolower(substr($package, -7, 7))) {
739 $p_compress = 'gz';
740 } elseif ('.tar.bz2' == strtolower(substr($package, -8, 8))) {
741 $p_compress = 'bz2';
742 }
743
744 // It's not pretty, but it works.
745 if (is_a($wp_filesystem, 'WP_Filesystem_Direct')) {
746 $extract_dir = $working_dir;
747 } else {
748 $updraft_dir = $updraftplus->backups_dir_location();
749 if (!UpdraftPlus_Filesystem_Functions::really_is_writable($updraft_dir)) {
750 $updraftplus->log_e("Backup directory (%s) is not writable, or does not exist.", $updraft_dir);
751 $result = new WP_Error('unpack_failed', $this->strings['unpack_failed'], $tar->extract);
752 } else {
753 $extract_dir = $updraft_dir.'/'.basename($working_dir).'-old';
754 if (file_exists($extract_dir)) UpdraftPlus_Filesystem_Functions::remove_local_directory($extract_dir);
755 $updraftplus->log("Using a temporary folder to extract before moving over WPFS: $extract_dir");
756 }
757 }
758
759 // Slightly hackish - rather than re-write Archive_Tar to use wp_filesystem, we instead unpack into the location that we already require to be directly writable for other reasons, and then move from there.
760
761 if (empty($result)) {
762
763 $this->ud_extract_count = 0;
764 $this->ud_working_dir = trailingslashit($working_dir);
765 $this->ud_extract_dir = untrailingslashit($extract_dir);
766 $this->ud_made_dirs = array();
767 add_filter('updraftplus_tar_wrote', array($this, 'tar_wrote'), 10, 2);
768 $tar = new UpdraftPlus_Archive_Tar($package, $p_compress);
769 $result = $tar->extract($extract_dir, false);
770 if (!is_a($wp_filesystem, 'WP_Filesystem_Direct')) UpdraftPlus_Filesystem_Functions::remove_local_directory($extract_dir);
771 if (true != $result) {
772 $result = new WP_Error('unpack_failed', $this->strings['unpack_failed'], $result);
773 } else {
774 if (!is_a($wp_filesystem, 'WP_Filesystem_Direct')) {
775 $updraftplus->log('Moved unpacked tarball contents');
776 }
777 }
778 remove_filter('updraftplus_tar_wrote', array($this, 'tar_wrote'), 10, 2);
779 }
780 }
781
782 // Once extracted, delete the package if required.
783 if ($delete_package) unlink($package);
784
785 if (is_wp_error($result)) {
786 $wp_filesystem->delete($working_dir, true);
787 if ('incompatible_archive' == $result->get_error_code()) {
788 return new WP_Error('incompatible_archive', $this->wp_upgrader->strings['incompatible_archive'], $result->get_error_data());
789 }
790 return $result;
791 }
792
793 if (!empty($this->ud_foreign)) {
794 $this->ud_foreign_working_dir = $working_dir;
795 $this->ud_foreign_package = $package;
796 // Zip containing an SQL file. We try a default pattern.
797 if ('db' === $type) {
798 $basepack = basename($package, '.zip');
799 if ($wp_filesystem->exists($working_dir.'/'.$basepack.'.sql')) {
800 if (!$wp_filesystem->move($working_dir.'/'.$basepack.'.sql', $working_dir . "/backup.db", true)) {
801 $this->restore_log_permission_failure_message($working_dir, 'Move '. $working_dir.'/'.$basepack.'.sql'.' -> '.$working_dir . "/backup.db", 'Destination');
802 }
803 $updraftplus->log("Moving database file $basepack.sql to backup.db");
804 }
805 }
806 }
807
808 return $working_dir;
809 }
810
811 public function tar_wrote($result, $file) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
812 if (0 !== strpos($file, $this->ud_extract_dir)) return false;
813 global $wp_filesystem, $updraftplus;
814 if (!is_a($wp_filesystem, 'WP_Filesystem_Direct')) {
815 $modint = 100;
816 $leaf = substr($file, strlen($this->ud_extract_dir));
817 $dirname = dirname($leaf);
818 $need_dirs = explode('/', $dirname);
819 if (empty($this->ud_made_dirs[$dirname])) {
820 $cdir = '';
821 foreach ($need_dirs as $ndir) {
822 $cdir .= ($cdir) ? '/'.$ndir : $ndir;
823 if (empty($this->ud_made_dirs[$cdir])) {
824 if (!$wp_filesystem->mkdir($this->ud_working_dir.$cdir, FS_CHMOD_DIR) && !$wp_filesystem->is_dir($this->ud_working_dir.$cdir)) {
825 $updraftplus->log("Failed to create WPFS directory: ".$this->ud_working_dir.$cdir);
826 return false;
827 } else {
828 $this->ud_made_dirs[$cdir] = true;
829 }
830 }
831 }
832 }
833 $put = $wp_filesystem->put_contents($this->ud_working_dir.$leaf, file_get_contents($file));
834 if (is_wp_error($put)) $updraftplus->log_wp_error($put);
835 @unlink($file);
836 } else {
837 $modint = 500;
838 $put = true;
839 }
840 if ($put) {
841 $this->ud_extract_count++;
842 if (0 == $this->ud_extract_count % $modint) {
843 $updraftplus->log_e("%s files have been extracted", $this->ud_extract_count);
844 }
845 }
846 return (true == $put);
847 }
848
849 // This returns a wp_filesystem location (and we musn't change that, as we must retain compatibility with the class parent)
850
851 /**
852 * This returns a wp_filesystem location (and we musn't change that, as we must retain compatibility with the class parent)
853 * along with unpacking the encrypted db file and checking its contents before going off and restoring the Db
854 *
855 * @param string $package The file name of the encrypted File
856 * @param boolean $delete_package the file can be removed before going off to the restore stage (this is just incase the user dont want to proceed)
857 * @param boolean $type Check if the type is true or false
858 * @return string Returns success or Fail depending on errors and restors DB
859 */
860 public function unpack_package($package, $delete_package = true, $type = false) {
861
862 if (preg_match('/-db(\.gz(\.crypt)?)?$/i', $package) || preg_match('/\.sql(\.gz|\.bz2)?$/i', $package)) {
863 return $this->unpack_package_database($package, $delete_package);
864 } else {
865 global $updraftplus;
866 // If not database, then it is a zip - unpack in the usual way
867 return $this->unpack_package_archive($updraftplus->backups_dir_location().'/'.$package, $delete_package, $type);
868 }
869
870 }
871
872 /**
873 * Unpack a database backup file
874 *
875 * @used-by self::unpack_package()
876 *
877 * @param String $package - file to unpack; relative filepath
878 * @param Boolean $delete_package - check to delete package
879 *
880 * @return String|WP_Error If successful, then this indicates the working directory that the archive was unpacked in (a WP_Filesystem path)
881 */
882 private function unpack_package_database($package, $delete_package = true) {
883
884 global $wp_filesystem, $updraftplus;
885
886 $updraft_dir = $updraftplus->backups_dir_location();
887
888 // The general shape of the following comes from class-wp-upgrader.php
889
890 $backup_dir = $wp_filesystem->find_folder($updraft_dir);
891
892 @set_time_limit(1800);
893
894 $packsize = round(filesize($backup_dir.$package)/1048576, 1).' Mb';
895
896 $this->skin->feedback($this->strings['unpack_package'].' ('.basename($package).', '.$packsize.')');
897
898 $upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
899 @$wp_filesystem->mkdir($upgrade_folder, octdec($this->calculate_additive_chmod_oct(FS_CHMOD_DIR, 0775)));
900
901 // Clean up contents of upgrade directory beforehand.
902 $upgrade_files = $wp_filesystem->dirlist($upgrade_folder);
903 if (!empty($upgrade_files)) {
904 foreach ($upgrade_files as $file) {
905 if (!$wp_filesystem->delete($upgrade_folder.$file['name'], true)) {
906 $this->restore_log_permission_failure_message($upgrade_folder, 'Delete '.$upgrade_folder.$file['name']);
907 }
908 }
909 }
910
911 // We need a working directory
912 $working_dir = $upgrade_folder . basename($package, '.crypt');
913
914 // Clean up working directory
915 if ($wp_filesystem->is_dir($working_dir)) {
916 if (!$wp_filesystem->delete($working_dir, true)) {
917 $this->restore_log_permission_failure_message(dirname($working_dir), 'Delete '.$working_dir);
918 }
919 }
920
921 if (!$wp_filesystem->mkdir($working_dir, octdec($this->calculate_additive_chmod_oct(FS_CHMOD_DIR, 0775)))) return new WP_Error('mkdir_failed', __('Failed to create a temporary directory', 'updraftplus').' ('.$working_dir.')');
922
923 // Unpack package to working directory
924 if (UpdraftPlus_Encryption::is_file_encrypted($package)) {
925 $this->skin->feedback($this->strings['decrypt_database']);
926
927 $encryption = empty($this->restore_options['updraft_encryptionphrase']) ? UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase') : $this->restore_options['updraft_encryptionphrase'];
928
929 if (!$encryption) return new WP_Error('no_encryption_key', __('Decryption failed. The database file is encrypted, but you have no encryption key entered.', 'updraftplus'));
930
931 // function decrypt
932 $decrypted_file = UpdraftPlus_Encryption::decrypt($backup_dir.$package, $encryption);
933
934 if (is_array($decrypted_file)) {
935 $this->skin->feedback($this->strings['decrypted_database']);
936 if (!copy($decrypted_file['fullpath'], $working_dir.'/backup.db.gz')) {
937 return new WP_Error('write_failed', __('Failed to write out the decrypted database to the filesystem', 'updraftplus'));
938 } else {
939 unlink($decrypted_file['fullpath']);
940 }
941 } else {
942 return new WP_Error('decryption_failed', __('Decryption failed. The most likely cause is that you used the wrong key.', 'updraftplus'));
943 }
944 } else {
945 if (preg_match('/\.sql$/i', $package)) {
946 if (!$wp_filesystem->copy($backup_dir.$package, $working_dir.'/backup.db')) {
947 if ($wp_filesystem->errors->get_error_code()) {
948 foreach ($wp_filesystem->errors->get_error_messages() as $message) show_message($message);
949 }
950 return new WP_Error('copy_failed', $this->strings['copy_failed']);
951 }
952 } elseif (preg_match('/\.bz2$/i', $package)) {
953 if (!$wp_filesystem->copy($backup_dir.$package, $working_dir.'/backup.db.bz2')) {
954 if ($wp_filesystem->errors->get_error_code()) {
955 foreach ($wp_filesystem->errors->get_error_messages() as $message) show_message($message);
956 }
957 return new WP_Error('copy_failed', $this->strings['copy_failed']);
958 }
959 } elseif (!$wp_filesystem->copy($backup_dir.$package, $working_dir.'/backup.db.gz')) {
960 if ($wp_filesystem->errors->get_error_code()) {
961 foreach ($wp_filesystem->errors->get_error_messages() as $message) show_message($message);
962 }
963 return new WP_Error('copy_failed', $this->strings['copy_failed']);
964 }
965 }
966
967 // Once extracted, delete the package if required (non-recursive, is a file)
968 // if ($delete_package) $wp_filesystem->delete($decrypted_file['fullpath'], false, true);
969 if ($delete_package) {
970 if (!$wp_filesystem->delete($backup_dir.$package, false, true)) {
971 $this->restore_log_permission_failure_message($backup_dir, 'Delete '.$backup_dir.$package);
972 }
973 }
974
975 $updraftplus->log('Database successfully unpacked');
976
977 return $working_dir;
978 }
979
980 /**
981 * For moving files out of a directory into their new location
982 * The purposes of the $type parameter are 1) to detect 'others' and apply a historical bugfix 2) to detect wpcore, and apply the setting for what to do with wp-config.php 3) to work out whether to delete the directory itself
983 * Must use only wp_filesystem
984 * $dest_dir must already have a trailing slash
985 * $preserve_existing: this setting only applies at the top level: 0 = overwrite with no backup; 1 = make backup of existing; 2 = do nothing if there is existing, 3 = do nothing to the top level directory, but do copy-in contents (and over-write files). Thus, on a multi-archive set where you want a backup, you'd do this: first call with $preserve_existing === 1, then on subsequent zips call with 3
986 *
987 * @param string $working_dir specify working directory
988 * @param string $dest_dir specify destination directory
989 * @param integer $preserve_existing check to preserve exisitng file
990 * @param array $do_not_overwrite Specify files or directories not to overwrite
991 * @param string $type specify type
992 * @param boolean $send_actions send actions
993 * @param boolean $force_local force local
994 * @return boolean
995 */
996 public function move_backup_in($working_dir, $dest_dir, $preserve_existing = 1, $do_not_overwrite = array('plugins', 'themes', 'uploads', 'upgrade'), $type = 'not-others', $send_actions = false, $force_local = false) {
997
998 global $wp_filesystem, $updraftplus;
999 $updraft_dir = $updraftplus->backups_dir_location();
1000
1001 if (true == $force_local) {
1002 $wpfs = new UpdraftPlus_WP_Filesystem_Direct(true);
1003 } else {
1004 $wpfs = $wp_filesystem;
1005 }
1006
1007 // Get the content to be moved in. Include hidden files = true. Recursion is only required if we're likely to copy-in
1008 $recursive = (self::MOVEIN_COPY_IN_CONTENTS == $preserve_existing) ? true : false;
1009 $upgrade_files = $wpfs->dirlist($working_dir, true, $recursive);
1010
1011 if (empty($upgrade_files)) return true;
1012
1013 if (!$wpfs->is_dir($dest_dir)) {
1014 return new WP_Error('no_such_dir', __('The directory does not exist', 'updraftplus')." ($dest_dir)");
1015 }
1016
1017 $wpcore_config_moved = false;
1018
1019 if ('plugins' == $type || 'themes' == $type) $updraftplus->log("Top-level entities being moved: ".implode(', ', array_keys($upgrade_files)));
1020
1021 foreach ($upgrade_files as $file => $filestruc) {
1022
1023 if (empty($file)) continue;
1024
1025 if ($dest_dir.$file == $updraft_dir) {
1026 $updraftplus->log('Skipping attempt to replace updraft_dir whilst processing '.$type);
1027 continue;
1028 }
1029
1030 // Correctly restore files in 'others' in no directory that were wrongly backed up in versions 1.4.0 - 1.4.48
1031 if (('others' == $type || 'wpcore' == $type) && preg_match('/^([\-_A-Za-z0-9]+\.php)$/i', $file, $matches) && $wpfs->exists($working_dir . "/$file/$file")) {
1032 if ('others' == $type) {
1033 $updraftplus->log("Found file: $file/$file: presuming this is a backup with a known fault (backup made with versions 1.4.0 - 1.4.48, and sometimes up to 1.6.55 on some Windows servers); will rename to simply $file", 'notice-restore');
1034 } else {
1035 $updraftplus->log("Found file: $file/$file: presuming this is a backup with a known fault (backup made with versions before 1.6.55 in certain situations on Windows servers); will rename to simply $file", 'notice-restore');
1036 }
1037 $updraftplus->log("$file/$file: rename to $file");
1038 $file = $matches[1];
1039 $tmp_file = rand(0, 999999999).'.php';
1040 // Rename directory
1041 if (!$wpfs->move($working_dir . "/$file", $working_dir . "/".$tmp_file, true)) {
1042 $this->restore_log_permission_failure_message($working_dir, 'Move '. $working_dir . "/$file -> ".$working_dir . "/".$tmp_file, 'Destination');
1043 }
1044 if (!$wpfs->move($working_dir . "/$tmp_file/$file", $working_dir ."/".$file, true)) {
1045 $this->restore_log_permission_failure_message($working_dir, 'Move '.$working_dir . "/$tmp_file/$file -> ".$working_dir ."/".$file, 'Destination');
1046 }
1047 if (!$wpfs->rmdir($working_dir . "/$tmp_file", false)) {
1048 $this->restore_log_permission_failure_message($working_dir, 'Delete '.$working_dir . "/$tmp_file");
1049 }
1050 }
1051
1052 if ('wp-config.php' == $file && 'wpcore' == $type) {
1053 if (empty($this->restore_options['updraft_restorer_wpcore_includewpconfig'])) {
1054 $updraftplus->log_e('wp-config.php from backup: will restore as wp-config-backup.php', 'updraftplus');
1055 if (!$wpfs->move($working_dir . "/$file", $working_dir . "/wp-config-backup.php", true)) {
1056 $this->restore_log_permission_failure_message($working_dir, 'Move '.$working_dir . "/$file -> ".$working_dir . "/wp-config-backup.php", 'Destination');
1057 }
1058 $file = "wp-config-backup.php";
1059 $wpcore_config_moved = true;
1060 } else {
1061 $updraftplus->log_e("wp-config.php from backup: restoring (as per user's request)", 'updraftplus');
1062 }
1063 } elseif ('wpcore' == $type && 'wp-config-backup.php' == $file && $wpcore_config_moved) {
1064 // The file is already gone; nothing to do
1065 continue;
1066 }
1067
1068 // Sanity check (should not be possible as these were excluded at backup time)
1069 if (in_array($file, $do_not_overwrite)) continue;
1070
1071 if (('object-cache.php' == $file || 'advanced-cache.php' == $file) && 'others' == $type) {
1072 if (false == apply_filters('updraftplus_restorecachefiles', true, $file)) {
1073 $nfile = preg_replace('/\.php$/', '-backup.php', $file);
1074 if (!$wpfs->move($working_dir . "/$file", $working_dir . "/" .$nfile, true)) {
1075 $this->restore_log_permission_failure_message($working_dir, 'Move '. $working_dir . '/' . $file .' -> '.$working_dir . '/' . $nfile, 'Destination');
1076 }
1077 $file = $nfile;
1078 }
1079 } elseif (('object-cache-backup.php' == $file || 'advanced-cache-backup.php' == $file) && 'others' == $type) {
1080 if (!$wpfs->delete($working_dir."/".$file)) {
1081 $this->restore_log_permission_failure_message($working_dir, 'Delete '.$working_dir."/".$file);
1082 }
1083 continue;
1084 }
1085
1086 // First, move the existing one, if necessary (may not be present)
1087 if ($wpfs->exists($dest_dir.$file)) {
1088 if (self::MOVEIN_MAKE_BACKUP_OF_EXISTING == $preserve_existing) {
1089 if (!$wpfs->move($dest_dir.$file, $dest_dir.$file.'-old', true)) {
1090 $this->restore_log_permission_failure_message($dest_dir, 'Move '. $dest_dir.$file.' -> '.$dest_dir.$file.'-old', 'Destination');
1091 return new WP_Error('old_move_failed', $this->strings['old_move_failed']." ($dest_dir$file)");
1092 }
1093 } elseif (self::MOVEIN_OVERWRITE_NO_BACKUP == $preserve_existing) {
1094 if (!$wpfs->delete($dest_dir.$file, true)) {
1095 $this->restore_log_permission_failure_message($dest_dir, 'Delete '.$dest_dir.$file);
1096 return new WP_Error('old_delete_failed', $this->strings['old_delete_failed']." ($file)");
1097 }
1098 }
1099 }
1100
1101
1102 // Secondly, move in the new one
1103 $is_dir = $wpfs->is_dir($working_dir."/".$file);
1104
1105 if (self::MOVEIN_DO_NOTHING_IF_EXISTING == $preserve_existing && $wpfs->exists($dest_dir.$file)) {
1106 // Something exists - no move. Remove it from the temporary directory - so that it will be clean later
1107 @$wpfs->delete($working_dir.'/'.$file, true);
1108 // The $is_dir check was added in version 1.11.18; without this, files in the top-level that weren't in the first archive didn't get over-written
1109 } elseif (self::MOVEIN_COPY_IN_CONTENTS != $preserve_existing || !$wpfs->exists($dest_dir.$file) || !$is_dir) {
1110
1111 if ($wpfs->move($working_dir."/".$file, $dest_dir.$file, true)) {
1112 if ($send_actions) do_action('updraftplus_restored_'.$type.'_one', $file);
1113 // Make sure permissions are at least as great as those of the parent
1114 if ($is_dir) {
1115 // This method is broken due to https://core.trac.wordpress.org/ticket/26598
1116 if (empty($chmod)) $chmod = octdec(sprintf("%04d", $this->get_current_chmod($dest_dir, $wpfs)));
1117 if (!empty($chmod)) $this->chmod_if_needed($dest_dir.$file, $chmod, false, $wpfs);
1118 }
1119 } else {
1120 $this->restore_log_permission_failure_message($dest_dir, 'Move '. $working_dir."/".$file." -> ".$dest_dir.$file, 'Destination');
1121 return new WP_Error('move_failed', $this->strings['move_failed'], $working_dir."/".$file." -> ".$dest_dir.$file);
1122 }
1123 } elseif (self::MOVEIN_COPY_IN_CONTENTS == $preserve_existing && !empty($filestruc['files'])) {
1124 // The directory ($dest_dir) already exists, and we've been requested to copy-in. We need to perform the recursive copy-in
1125 // $filestruc['files'] is then a new structure like $upgrade_files
1126 // First pass: create directory structure
1127 // Get chmod value for the parent directory, and re-use it (instead of passing false)
1128
1129 // This method is broken due to https://core.trac.wordpress.org/ticket/26598
1130 if (empty($chmod)) $chmod = octdec(sprintf("%04d", $this->get_current_chmod($dest_dir, $wpfs)));
1131 // Copy in the files. This also needs to make sure the directories exist, in case the zip file lacks entries
1132 $delete_root = ('others' == $type || 'wpcore' == $type) ? false : true;
1133
1134 $copy_in = $this->copy_files_in($working_dir.'/'.$file, $dest_dir.$file, $filestruc['files'], $chmod, $delete_root);
1135 if (!empty($chmod)) $this->chmod_if_needed($dest_dir.$file, $chmod, false, $wpfs);
1136
1137 if (is_wp_error($copy_in) || !$copy_in) {
1138 $this->restore_log_permission_failure_message($dest_dir, 'Move '. $working_dir."/".$file." -> ".$dest_dir.$file, 'Destination');
1139 }
1140 if (is_wp_error($copy_in)) return $copy_in;
1141 if (!$copy_in) return new WP_Error('move_failed', $this->strings['move_failed'], "(2) ".$working_dir.'/'.$file." -> ".$dest_dir.$file);
1142
1143 if (!$wpfs->rmdir($working_dir.'/'.$file)) {
1144 $this->restore_log_permission_failure_message($working_dir, 'Delete '.$working_dir.'/'.$file);
1145 }
1146 } else {
1147 if (!$wpfs->rmdir($working_dir.'/'.$file)) {
1148 $this->restore_log_permission_failure_message($working_dir, 'Delete '.$working_dir.'/'.$file);
1149 }
1150 }
1151 }
1152
1153 return true;
1154
1155 }
1156
1157 /**
1158 * $dest_dir must already exist
1159 *
1160 * @param string $source_dir source directory
1161 * @param string $dest_dir destintion directory
1162 * @param string $files files to be placed in directory
1163 * @param boolean $chmod chmod type
1164 * @param boolean $delete_source indicate whether source needs deleting
1165 * @return boolean
1166 */
1167 private function copy_files_in($source_dir, $dest_dir, $files, $chmod = false, $delete_source = false) {
1168 global $wp_filesystem, $updraftplus;
1169 foreach ($files as $rname => $rfile) {
1170 if ('d' != $rfile['type']) {
1171 // Delete it if it already exists (or perhaps WP does it for us)
1172 if (!$wp_filesystem->move($source_dir.'/'.$rname, $dest_dir.'/'.$rname, true)) {
1173 $this->restore_log_permission_failure_message($dest_dir, $source_dir.'/'.$rname.' -> '.$dest_dir.'/'.$rname, 'Destination');
1174 return false;
1175 }
1176 } else {
1177 // Directory
1178 if ($wp_filesystem->is_file($dest_dir.'/'.$rname)) @$wp_filesystem->delete($dest_dir.'/'.$rname, false, 'f');
1179 // No such directory yet: just move it
1180 if (!$wp_filesystem->is_dir($dest_dir.'/'.$rname)) {
1181 if (!$wp_filesystem->move($source_dir.'/'.$rname, $dest_dir.'/'.$rname, false)) {
1182 $this->restore_log_permission_failure_message($dest_dir, 'Move '.$source_dir.'/'.$rname.' -> '.$dest_dir.'/'.$rname, 'Destination');
1183 $updraftplus->log_e('Failed to move directory (check your file permissions and disk quota): %s', $source_dir.'/'.$rname." -&gt; ".$dest_dir.'/'.$rname);
1184 return false;
1185 }
1186 } elseif (!empty($rfile['files'])) {
1187 // There is a directory - and we want to to copy in
1188 $docopy = $this->copy_files_in($source_dir.'/'.$rname, $dest_dir.'/'.$rname, $rfile['files'], $chmod, false);
1189 if (is_wp_error($docopy)) return $docopy;
1190 if (false === $docopy) {
1191 return false;
1192 }
1193 } else {
1194 // There is a directory: but nothing to copy in to it
1195 @$wp_filesystem->rmdir($source_dir.'/'.$rname);
1196 }
1197 }
1198 }
1199 // We are meant to leave the working directory empty. Hence, need to rmdir() once a directory is empty. But not the root of it all in case of others/wpcore.
1200 if ($delete_source || strpos($source_dir, '/') !== false) {
1201 if (!$wp_filesystem->rmdir($source_dir, false)) {
1202 $this->restore_log_permission_failure_message($source_dir, 'Delete '.$source_dir);
1203 }
1204 }
1205
1206 return true;
1207
1208 }
1209
1210 /**
1211 * Pre-flight check: chance to complain and abort before anything at all is done
1212 *
1213 * @param Array $backup_files - An array of backup files
1214 * @param String $type - Type of file
1215 * @param Array $info - Information about the backup
1216 *
1217 * @return Boolean|WP_Error
1218 */
1219 private function pre_restore_backup($backup_files, $type, $info) {
1220
1221 if (is_string($backup_files)) $backup_files = array($backup_files);
1222
1223 if ('more' == $type) {
1224 $this->skin->feedback($this->strings['not_possible']);
1225 return new WP_Error('not_possible', $this->strings['not_possible']);
1226 }
1227
1228 // Ensure access to the indicated directory - and to WP_CONTENT_DIR (in which we use upgrade/)
1229 $need_these = array(WP_CONTENT_DIR);
1230 if (!empty($info['path'])) $need_these[] = $info['path'];
1231
1232 $res = $this->wp_upgrader->fs_connect($need_these);
1233 if (false === $res || is_wp_error($res)) return $res;
1234
1235 // Check upgrade directory is writable (instead of having non-obvious messages when we try to write)
1236 // In theory, this is redundant (since we already checked for access to WP_CONTENT_DIR); but in practice, this extra check has been needed
1237
1238 global $wp_filesystem, $updraftplus, $updraftplus_admin, $updraftplus_addons_migrator;
1239
1240 if (empty($this->pre_restore_updatedir_writable)) {
1241 $upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
1242 @$wp_filesystem->mkdir($upgrade_folder, octdec($this->calculate_additive_chmod_oct(FS_CHMOD_DIR, 0775)));
1243 if (!$wp_filesystem->is_dir($upgrade_folder)) {
1244 return new WP_Error('no_dir', sprintf(__('UpdraftPlus needed to create a %s in your content directory, but failed - please check your file permissions and enable the access (%s)', 'updraftplus'), __('folder', 'updraftplus'), $upgrade_folder));
1245 }
1246 $rand_file = 'testfile_'.rand(0, 9999999).md5(microtime(true)).'.txt';
1247 if ($wp_filesystem->put_contents($upgrade_folder.$rand_file, 'testing...')) {
1248 @$wp_filesystem->delete($upgrade_folder.$rand_file);
1249 $this->pre_restore_updatedir_writable = true;
1250 } else {
1251 $this->restore_log_permission_failure_message($upgrade_folder, 'Put contents '.$upgrade_folder.$rand_file, 'Destination');
1252 return new WP_Error('no_file', sprintf(__('UpdraftPlus needed to create a %s in your content directory, but failed - please check your file permissions and enable the access (%s)', 'updraftplus'), __('file', 'updraftplus'), $upgrade_folder.$rand_file));
1253 }
1254 }
1255
1256 // Code below here assumes that we're dealing with file-based entities
1257 if ('db' == $type) return true;
1258
1259 $wp_filesystem_dir = $this->get_wp_filesystem_dir($info['path']);
1260 if (false === $wp_filesystem_dir) return false;
1261
1262 $ret_val = true;
1263 $updraft_dir = $updraftplus->backups_dir_location();
1264
1265 if (!is_array($this->continuation_data) && (('plugins' == $type || 'uploads' == $type || 'themes' == $type) && (!is_multisite() || 0 !== $this->ud_backup_is_multisite || ('uploads' != $type || empty($updraftplus_addons_migrator->new_blogid))))) {
1266 if (file_exists($updraft_dir.'/'.basename($wp_filesystem_dir)."-old")) {
1267 $ret_val = new WP_Error('already_exists', sprintf(__('Existing unremoved folders from a previous restore exist (please use the "Delete Old Directories" button to delete them before trying again): %s', 'updraftplus'), $wp_filesystem_dir.'-old'));
1268 }
1269 }
1270
1271 if (!empty($this->ud_foreign)) {
1272 $known_foreigners = apply_filters('updraftplus_accept_archivename', array());
1273 if (!is_array($known_foreigners) || empty($known_foreigners[$this->ud_foreign])) {
1274 return new WP_Error('uk_foreign', __('This version of UpdraftPlus does not know how to handle this type of foreign backup', 'updraftplus').' ('.$this->ud_foreign.')');
1275 }
1276 }
1277
1278 return $ret_val;
1279 }
1280
1281 private function get_wp_filesystem_dir($path) {
1282 global $wp_filesystem;
1283 // Get the wp_filesystem location for the folder on the local install
1284 switch ($path) {
1285 case ABSPATH:
1286 case '':
1287 $wp_filesystem_dir = $wp_filesystem->abspath();
1288 break;
1289 case WP_CONTENT_DIR:
1290 $wp_filesystem_dir = $wp_filesystem->wp_content_dir();
1291 break;
1292 case WP_PLUGIN_DIR:
1293 $wp_filesystem_dir = $wp_filesystem->wp_plugins_dir();
1294 break;
1295 case WP_CONTENT_DIR . '/themes':
1296 $wp_filesystem_dir = $wp_filesystem->wp_themes_dir();
1297 break;
1298 default:
1299 $wp_filesystem_dir = $wp_filesystem->find_folder($path);
1300 break;
1301 }
1302 if (!$wp_filesystem_dir) return false;
1303 return untrailingslashit($wp_filesystem_dir);
1304 }
1305
1306 private function can_version_ajax_restore($version) {
1307 if (!defined('UPDRAFTPLUS_EXPERIMENTAL_AJAX_RESTORE') || !UPDRAFTPLUS_EXPERIMENTAL_AJAX_RESTORE) return false;
1308 return ((version_compare($version, '2.0', '<') && version_compare($version, '1.11.10', '>=')) || (version_compare($version, '2.0', '>=') && version_compare($version, '2.11.10', '>='))) ? true : false;
1309 }
1310
1311 /**
1312 * $backup_file is just the basename, and must be a string; we expect the caller to deal with looping over an array (multi-archive sets). We do, however, record whether we have already unpacked an entity of the same type - so that we know to add (not replace).
1313 *
1314 * @param string $backup_file name of file being backed up
1315 * @param string $type type of file
1316 * @param array $info information array
1317 * @param boolean $last_one indicate if this is the last file to be restored
1318 * @param boolean $last_entity indicate if this is the last entity of this type to be restored
1319 * @return WP_Error|Boolean - true if successful; otherwise false or an error
1320 */
1321 private function restore_backup($backup_file, $type, $info, $last_one = false, $last_entity = false) {
1322
1323 if ('more' == $type) {
1324 $this->skin->feedback($this->strings['not_possible']);
1325 return false;
1326 }
1327
1328 global $wp_filesystem, $updraftplus_addons_migrator, $updraftplus;
1329
1330 $updraftplus->log("restore_backup(backup_file=$backup_file, type=$type, info=".serialize($info).", last_one=$last_one)");
1331
1332 $get_dir = empty($info['path']) ? '' : $info['path'];
1333
1334 if (false === ($wp_filesystem_dir = $this->get_wp_filesystem_dir($get_dir))) return false;
1335
1336 if (empty($this->abspath)) $this->abspath = trailingslashit($wp_filesystem->abspath());
1337
1338 @set_time_limit(1800);
1339
1340 /*
1341 TODO:
1342 - The backup set may no longer be in the DB - a restore may have over-written it.
1343 - UD might be installed, but not active. Test that too. (All combinations need testing - new/old UD vers, logged-in/not, etc.).
1344 - logging
1345 - authorisation on the AJAX call, given that our login may not even be valid any more.
1346 - pass on the WP filesystem credentials somehow - they have been POSTed, and should be included in what's POSTed back.
1347 - the restore function wants to know the UD version the backup set came from
1348 - the restore function wants to know whether we're restoring an individual blog into a multisite
1349 - the restore function has some things only done the first time, which isn't directly tracked (uses internal state instead, which won't work over AJAX)
1350 - how to handle/set this->delete
1351 - how to show the final result
1352 - how to do the clear-up of restored stuff in the restore function
1353 - remember, we need to do unauthenticated AJAX, as the authentication is happening via a different means. Use a separate procedure from the usual one (and no nonce, as login status may have changed).
1354 */
1355 if (defined('UPDRAFTPLUS_EXPERIMENTAL_AJAX_RESTORE') && UPDRAFTPLUS_EXPERIMENTAL_AJAX_RESTORE && 'uploads' == $type) {
1356 // Read this each time, as we don't know what might have been done in the mean-time (specifically with UD being replaced by a different UD from a backup). Of course, we know what the currently running process is capable of.
1357 if (file_exists(UPDRAFTPLUS_DIR.'/updraftplus.php') && $fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
1358 $file_data = fread($fp, 1024);
1359 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
1360 $ud_version = $matches[1];
1361 }
1362 fclose($fp);
1363 }
1364 if (!empty($ud_version) && $this->can_version_ajax_restore($ud_version) && !empty($this->ud_backup_set['timestamp'])) {
1365 $nonce = $updraftplus->nonce;
1366 if (!function_exists('crypt_random_string')) $updraftplus->ensure_phpseclib('Crypt_Random', 'Crypt/Random');
1367 $this->ajax_restore_auth_code = bin2hex(crypt_random_string(32));
1368 // TODO: Delete this when done, to prevent abuse
1369 update_site_option('updraft_ajax_restore_'.$nonce, $this->ajax_restore_auth_code.':'.time());
1370 $this->add_ajax_restore_admin_footer();
1371 $print_last_one = ($last_one) ? "1" : "0";
1372 // TODO: Also want the timestamp
1373 // We don't bother to include info, as that is backup-independent information that can be re-created when needed
1374 // TODO: Change to new log style, if ever using
1375 echo '<p style="margin: 0px 0px;" class="updraft-ajaxrestore" data-type="'.$type.'" data-lastone="'.$print_last_one.'" data-backupfile="'.esc_attr($backup_file).'">'."\n";
1376 $updraftplus->log("Deferring handling of uploads ($backup_file)");
1377 echo "$backup_file: ".'<span class="deferprogress">'.__('Deferring...', 'updraftplus').'</span>';
1378 echo '</p>';
1379 return true;
1380 }
1381 }
1382
1383 // This returns the wp_filesystem path
1384 $working_dir = $this->unpack_package($backup_file, $this->delete, $type);
1385 if (is_wp_error($working_dir)) return $working_dir;
1386
1387 $working_dir_localpath = WP_CONTENT_DIR.'/upgrade/'.basename($working_dir);
1388 @set_time_limit(1800);
1389
1390 // We copy the variable because we may be importing with a different prefix (e.g. on multisite imports of individual blog data)
1391 // The filter allows you to restore to a completely different prefix - i.e. don't replace this site; possibly useful for testing the restore process (but not yet tested)
1392 $import_table_prefix = apply_filters('updraftplus_restore_table_prefix', $updraftplus->get_table_prefix(false));
1393
1394 $this->import_table_prefix = $import_table_prefix;
1395
1396 $now_done = apply_filters('updraftplus_pre_restore_move_in', false, $type, $working_dir, $info, $this->ud_backup_set, $this, $wp_filesystem_dir);
1397 if (is_wp_error($now_done)) return $now_done;
1398
1399 // A slightly ugly way of getting a particular result back
1400 if (is_string($now_done)) {
1401 $wp_filesystem_dir = $now_done;
1402 $now_done = false;
1403 $do_not_move_old = true;
1404 }
1405
1406 if (!$now_done) {
1407
1408 if ('db' == $type) {
1409 $rdb = $this->restore_backup_db($working_dir, $working_dir_localpath, $import_table_prefix);
1410 if (false === $rdb || is_wp_error($rdb)) return $rdb;
1411 } elseif ('others' == $type) {
1412
1413 $dirname = basename($info['path']);
1414
1415 // For foreign 'Simple Backup', we need to keep going down until we find wp-content
1416 if (empty($this->ud_foreign)) {
1417 $move_from = $working_dir;
1418 } else {
1419 $move_from = $this->search_for_folder('wp-content', $working_dir);
1420 if (!is_string($move_from)) return new WP_Error('not_found', __('The WordPress content folder (wp-content) was not found in this zip file.', 'updraftplus'));
1421 }
1422
1423 // In this special case, the backup contents are not in a folder, so it is not simply a case of moving the folder around, but rather looping over all that we find
1424
1425 // On subsequent archives of a multi-archive set, don't move anything; but do on the first
1426 $preserve_existing = isset($this->been_restored['others']) ? self::MOVEIN_COPY_IN_CONTENTS : self::MOVEIN_MAKE_BACKUP_OF_EXISTING;
1427
1428 $preserve_existing = apply_filters('updraft_move_others_preserve_existing', $preserve_existing, $this->been_restored, $this->restore_options, $this->ud_backup_set);
1429
1430 $new_move_from = apply_filters('updraft_restore_backup_move_from', $move_from, 'others', $this->restore_options, $this->ud_backup_set);
1431
1432 if ($new_move_from != $move_from && 0 === strpos($new_move_from, $move_from)) {
1433 $new_suffix = substr($new_move_from, strlen($move_from));
1434 $wp_filesystem_dir .= $new_suffix;
1435 $move_from = $new_move_from;
1436 }
1437
1438 $move_in = $this->move_backup_in($move_from, trailingslashit($wp_filesystem_dir), $preserve_existing, array('plugins', 'themes', 'uploads', 'upgrade'), 'others');
1439 if (is_wp_error($move_in)) return $move_in;
1440 if (!$move_in) return new WP_Error('new_move_failed', $this->strings['new_move_failed']);
1441
1442 $this->been_restored['others'] = true;
1443
1444 } else {
1445
1446 // Default action: used for plugins, themes and uploads (and wpcore, via a filter)
1447 // Multi-archive sets: we record what we've already begun on, and on subsequent runs, copy in instead of replacing
1448 $movedin = apply_filters('updraftplus_restore_movein_'.$type, $working_dir, $this->abspath, $wp_filesystem_dir);
1449
1450 // A filter, to allow add-ons to perform the install of non-standard entities, or to indicate that it's not possible
1451 if (false === $movedin) {
1452 $this->skin->feedback($this->strings['not_possible']);
1453 } elseif (is_wp_error($movedin)) {
1454 return $movedin;
1455 } elseif (true !== $movedin) {
1456
1457 // We get the directory to move from early, in case there is a problem with the backup that affects the result - we want to detect that before moving existing data out of the way
1458
1459 $short_circuit = false;
1460
1461 // For foreign 'Simple Backup', we need to keep going down until we find wp-content
1462 if (empty($this->ud_foreign)) {
1463 $working_dir_use = $working_dir;
1464 } else {
1465 $working_dir_use = $this->search_for_folder('wp-content', $working_dir);
1466 if (!is_string($working_dir_use)) {
1467 if (empty($this->ud_foreign) || !apply_filters('updraftplus_foreign_allow_missing_entity', false, $type, $this->ud_foreign)) {
1468 return new WP_Error('not_found', __('The WordPress content folder (wp-content) was not found in this zip file.', 'updraftplus'));
1469 } else {
1470 $short_circuit = true;
1471 }
1472 }
1473 }
1474
1475 // The backup may not actually have /$type, since that is info from the present site
1476 $move_from = $this->get_first_directory($working_dir_use, array(basename($info['path']), $type));
1477 if (false !== $move_from) $move_from = apply_filters('updraft_restore_backup_move_from', $move_from, $type, $this->restore_options, $this->ud_backup_set);
1478
1479 if (false === $move_from) {
1480 if (!empty($this->ud_foreign) && !apply_filters('updraftplus_foreign_allow_missing_entity', false, $type, $this->ud_foreign)) {
1481 return new WP_Error('new_move_failed', $this->strings['new_move_failed']);
1482 }
1483 }
1484
1485 // On the first time, create the -old directory in updraft_dir
1486 // (Old style was: On the first time, move the existing data to -old)
1487 if (!isset($this->been_restored[$type]) && empty($do_not_move_old)) {
1488 $this->move_existing_to_old($type, $get_dir, $wp_filesystem, $wp_filesystem_dir);
1489 }
1490
1491 if (empty($short_circuit)) {
1492
1493 if (false === $move_from) {
1494 if (!empty($this->ud_foreign) && !apply_filters('updraftplus_foreign_allow_missing_entity', false, $type, $this->ud_foreign)) {
1495 return new WP_Error('new_move_failed', $this->strings['new_move_failed']);
1496 }
1497 } else {
1498
1499 $this->skin->feedback($this->strings['moving_backup']);
1500
1501 $move_in = $this->move_backup_in($move_from, trailingslashit($wp_filesystem_dir), self::MOVEIN_COPY_IN_CONTENTS, array(), $type);
1502
1503 if (is_wp_error($move_in)) return $move_in;
1504 if (!$move_in) return new WP_Error('new_move_failed', $this->strings['new_move_failed']);
1505
1506 if (!$wp_filesystem->rmdir($move_from)) {
1507 $this->restore_log_permission_failure_message(dirname($move_from), 'Delete '.$move_from);
1508 }
1509 }
1510 }
1511
1512 }
1513
1514 $this->been_restored[$type] = true;
1515
1516 }
1517 }
1518
1519 $attempt_delete = (!empty($this->ud_foreign) && !$last_one) ? false : true;
1520
1521 if ($attempt_delete) {
1522
1523 // Non-recursive, so the directory needs to be empty
1524 $this->skin->feedback($this->strings['cleaning_up']);
1525
1526 if (!empty($do_not_move_old)) @$wp_filesystem->delete($working_dir.'/'.$type);
1527
1528 // Foreign backups can contain extra data and thus leave stuff behind, thus causing errors
1529 $recurse = empty($this->ud_foreign) ? false : true;
1530 $recurse = apply_filters('updraftplus_restore_delete_recursive', $recurse, $this->ud_foreign, $this->restore_options, $type);
1531
1532 if (!$wp_filesystem->delete($working_dir, $recurse)) {
1533
1534 // Can remove this after 1-Jan-2015; or at least, make it so that it requires the version number to be present.
1535 $fixed_it_now = false;
1536 // Deal with a corner-case in version 1.8.5
1537 if ('uploads' == $type && (empty($this->created_by_version) || (version_compare($this->created_by_version, '1.8.5', '>=') && version_compare($this->created_by_version, '1.8.8', '<')))) {
1538 $updraftplus->log("Clean-up failed with uploads: will attempt 1.8.5-1.8.7 fix (".$this->created_by_version.")");
1539 $move_in = @$this->move_backup_in(dirname($move_from), trailingslashit($wp_filesystem_dir), 3, array(), $type);
1540 $updraftplus->log("Result: ".serialize($move_in));
1541 if ($wp_filesystem->delete($working_dir)) $fixed_it_now = true;
1542 }
1543
1544 if (file_exists($working_dir.DIRECTORY_SEPARATOR.'updraftplus-manifest.json')) {
1545 // Before we cleanup and remove the manifest check if this is the last entity of this type, if it is then we want to remove anything that no longer exists in this manifest
1546 if ($last_entity) {
1547 $incremental_restore_prune = $this->incremental_restore_prune_files($working_dir, $type);
1548 if (is_wp_error($incremental_restore_prune)) return $incremental_restore_prune;
1549 }
1550
1551 $wp_filesystem->delete($working_dir.DIRECTORY_SEPARATOR.'updraftplus-manifest.json');
1552 if ($wp_filesystem->delete($working_dir)) $fixed_it_now = true;
1553 }
1554
1555 if (!$fixed_it_now) {
1556 $updraftplus->log_e('Error: %s', $this->strings['delete_failed'].' ('.$working_dir.')');
1557 // List contents
1558 // No need to make this a restoration-aborting error condition - it's not
1559 $dirlist = $wp_filesystem->dirlist($working_dir, true, true);
1560 if (is_array($dirlist)) {
1561 $updraftplus->log(__('Files found:', 'updraftplus'), 'notice-restore');
1562 foreach ($dirlist as $name => $struc) {
1563 $updraftplus->log("* $name", 'notice-restore');
1564 }
1565 } else {
1566 $updraftplus->log_e('Unable to enumerate files in that directory.');
1567 }
1568 }
1569 }
1570 }
1571
1572 // Permissions changes (at the top level - i.e. this does not apply if using recursion) are now *additive* - i.e. there's no danger of permissions being removed from what's on-disk
1573 switch ($type) {
1574 case 'wpcore':
1575 $this->chmod_if_needed($wp_filesystem_dir, FS_CHMOD_DIR, false, $wp_filesystem);
1576 // In case we restored a .htaccess which is incorrect for the local setup
1577 $this->flush_rewrite_rules();
1578 break;
1579 case 'uploads':
1580 $this->chmod_if_needed($wp_filesystem_dir, FS_CHMOD_DIR, false, $wp_filesystem);
1581 break;
1582 case 'themes':
1583 // Cherry Framework needs its cache files removing after migration
1584 if ((empty($this->old_siteurl) || ($this->old_siteurl != $this->our_siteurl)) && function_exists('glob')) {
1585 $cherry_child = glob(WP_CONTENT_DIR.'/themes/theme*');
1586 if (is_array($cherry_child)) {
1587 foreach ($cherry_child as $theme) {
1588 if (file_exists($theme.'/style.less.cache')) unlink($theme.'/style.less.cache');
1589 if (file_exists($theme.'/bootstrap/less/bootstrap.less.cache')) unlink($theme.'/bootstrap/less/bootstrap.less.cache');
1590 }
1591 }
1592 }
1593 break;
1594 case 'db':
1595 if (function_exists('wp_cache_flush')) wp_cache_flush();
1596 do_action('updraftplus_restored_db', array(
1597 'expected_oldsiteurl' => $this->old_siteurl,
1598 'expected_oldhome' => $this->old_home,
1599 'expected_oldcontent' => $this->old_content
1600 ), $import_table_prefix);
1601
1602 // N.B. flush_rewrite_rules() causes $wp_rewrite to become up to date again - important for the no_mod_rewrite() call
1603 $this->flush_rewrite_rules();
1604
1605 if ($updraftplus->mod_rewrite_unavailable()) {
1606 $updraftplus->log("Using Apache, with permalinks (".get_option('permalink_structure').") but no mod_rewrite enabled - enable it to make your permalinks work");
1607 $warn_no_rewrite = sprintf(__('You are using the %s webserver, but do not seem to have the %s module loaded.', 'updraftplus'), 'Apache', 'mod_rewrite').' '.sprintf(__('You should enable %s to make any pretty permalinks (e.g. %s) work', 'updraftplus'), 'mod_rewrite', 'http://example.com/my-page/');
1608 $updraftplus->log($warn_no_rewrite, 'warning-restore');
1609 }
1610 break;
1611 default:
1612 $this->chmod_if_needed($wp_filesystem_dir, FS_CHMOD_DIR, false, $wp_filesystem);
1613 }
1614 // db was already done
1615 if ('db' != $type) do_action('updraftplus_restored_'.$type);
1616
1617 return true;
1618
1619 }
1620
1621 /**
1622 * This method will read in the latest manifest file for an entity type and start the file prune.
1623 *
1624 * @param string $working_dir - the directory we are working in
1625 * @param string $type - the type of file
1626 * @return boolean|WP_Error
1627 */
1628 private function incremental_restore_prune_files($working_dir, $type) {
1629 // Check file exists again just in case it some how got removed
1630 $manifest_file = $working_dir.DIRECTORY_SEPARATOR.'updraftplus-manifest.json';
1631 if (file_exists($manifest_file) && filesize($manifest_file) > 0) {
1632 $entity_manifest = file_get_contents($working_dir.DIRECTORY_SEPARATOR.'updraftplus-manifest.json');
1633 $decoded_manifest = json_decode($entity_manifest, true);
1634 if (null === $decoded_manifest) {
1635 // 2.16.0 could fail to put a comma after the first 'files' item
1636 $entity_manifest = preg_replace('/files":(.*)""/', 'files":$1","', $entity_manifest);
1637 $decoded_manifest = json_decode($entity_manifest, true);
1638 if (null === $decoded_manifest) return new WP_Error('decode_manifest_failed', 'Failed to JSON-decode the manifest file');
1639 global $updraftplus;
1640 $updraftplus->log('Manifest file had invalid JSON, but it was successfully patched');
1641 }
1642 $base_path = trailingslashit(WP_CONTENT_DIR);
1643 $path = $base_path.$type;
1644 return $this->incremental_restore_scan_dir($base_path, $path, 1, $decoded_manifest);
1645 } else {
1646 return new WP_Error('manifest_not_found', $this->strings['manifest_not_found']);
1647 }
1648 }
1649
1650 /**
1651 * This method will recursively scan each directory to the given listed_level which is located in the manifest and prune and files or folders that do not exist in the manifest.
1652 *
1653 * @param string $base_path - the base path of the entity type
1654 * @param string $path - the current path we are scanning
1655 * @param integer $current_level - the level we are currently scanning at
1656 * @param array $entity_manifest - the manifest array which includes the listed_level, directories and files to keep
1657 * @return boolean|WP_Error
1658 */
1659 private function incremental_restore_scan_dir($base_path, $path, $current_level, $entity_manifest) {
1660
1661 global $wp_filesystem;
1662
1663 $directory_level = $entity_manifest['listed_levels'];
1664 $entity_directories = $entity_manifest['contents']['directories'];
1665 $entity_files = $entity_manifest['contents']['files'];
1666
1667 if (!isset($directory_level) || !isset($entity_directories) || !isset($entity_files)) return new WP_Error('read_manifest_failed', $this->strings['read_manifest_failed']);
1668
1669 $directory_files = $wp_filesystem->dirlist($path);
1670
1671 if (isset($directory_files)) {
1672 foreach ($directory_files as $file => $filestruc) {
1673 if ($wp_filesystem->is_dir($path . DIRECTORY_SEPARATOR . $file)) {
1674 $directory = $path . DIRECTORY_SEPARATOR . $file;
1675 // Check if we should go deeper in the file path, if not then check if this directory exists in the manifest, if not then remove it.
1676 if ($current_level + 1 < $directory_level) {
1677 $incremental_restore_prune = $this->incremental_restore_scan_dir($base_path, $directory, $current_level + 1, $entity_manifest);
1678 if (is_wp_error($incremental_restore_prune)) return $incremental_restore_prune;
1679 } else {
1680 $directory = str_replace($base_path, "", $directory);
1681 if (!in_array($directory, $entity_directories)) {
1682 $wp_filesystem->delete($base_path . $directory, true);
1683 }
1684 }
1685 } else {
1686 $file = str_replace($base_path, "", $path . DIRECTORY_SEPARATOR . $file);
1687 if (!in_array($file, $entity_files)) {
1688 $wp_filesystem->delete($base_path . $file, false);
1689 }
1690 }
1691 }
1692
1693 return true;
1694 } else {
1695 return new WP_Error('read_working_dir_failed', $this->strings['read_working_dir_failed']);
1696 }
1697 }
1698
1699 private function move_existing_to_old($type, $get_dir, $wp_filesystem, $wp_filesystem_dir) {
1700
1701 if (apply_filters('updraft_move_existing_to_old_short_circuit', false, $type, $this->restore_options)) {
1702 // Users of the filter should do their own logging
1703 return;
1704 }
1705
1706 global $updraftplus;
1707 $updraft_dir = $updraftplus->backups_dir_location();
1708
1709 // Firstly, if there's already an '-old' directory, get rid of it
1710
1711 // Try filesystem-level move
1712 $old_dir = $updraft_dir.'/'.$type.'-old';
1713 if (is_dir($old_dir)) {
1714 $updraftplus->log_e('%s: This directory already exists, and will be replaced', $old_dir);
1715 UpdraftPlus_Filesystem_Functions::remove_local_directory($old_dir);
1716 }
1717
1718 $move_old_destination = apply_filters('updraftplus_restore_move_old_mode', 0, $type, $this->restore_options);
1719
1720 if (0 == $move_old_destination && @mkdir($old_dir)) {
1721 $updraftplus->log("Moving old data: filesystem method / updraft_dir is potentially possible");
1722 $move_old_destination = 1;
1723 }
1724
1725 // Try wp_filesystem instead
1726 if ($wp_filesystem->exists($wp_filesystem_dir."-old")) {
1727 // Is better to warn and delete the restore than abort mid-restore and leave inconsistent site
1728 $updraftplus->log_e('%s: This directory already exists, and will be replaced', $wp_filesystem_dir."-old");
1729 // In theory, supplying true as the 3rd parameter achieves this; in practice, not always so (leads to support requests)
1730 $wp_filesystem->delete($wp_filesystem_dir."-old", true);
1731 if ($wp_filesystem->exists($wp_filesystem_dir."-old")) {
1732 $updraftplus->log("Failed to remove existing directory (".$wp_filesystem_dir."-old");
1733 $failed_to_remove = true;
1734 }
1735 }
1736
1737 if (-1 != $move_old_destination && empty($failed_to_remove) && @$wp_filesystem->mkdir($wp_filesystem_dir."-old")) {
1738 $updraftplus->log("Moving old data: can potentially use wp_filesystem method / -old");
1739 $move_old_destination += 2;
1740 }
1741
1742 if (0 == $move_old_destination) {
1743 $updraftplus->log_e("File permissions do not allow the old data to be moved and retained; instead, it will be deleted.");
1744 }
1745
1746 $this->skin->feedback($this->strings['moving_old']);
1747
1748 // Firstly, try direct filesystem method into updraft_dir
1749 if ($move_old_destination > 0 && 1 == $move_old_destination % 2) {
1750 // The final 'true' forces direct filesystem access
1751 $move_old = @$this->move_backup_in($get_dir, $updraft_dir.'/'.$type.'-old/', 3, array(), $type, false, true);
1752 if (is_wp_error($move_old)) $updraftplus->log_wp_error($move_old);
1753 }
1754
1755 // Try wp_filesystem method into -old if that failed
1756 if (2 >= $move_old_destination && (0 == $move_old_destination % 2 || (!empty($move_old) && is_wp_error($move_old)))) {
1757 $move_old = @$this->move_backup_in($wp_filesystem_dir, $wp_filesystem_dir."-old/", 3, array(), $type);
1758 if (is_wp_error($move_old)) $updraftplus->log_wp_error($move_old);
1759 }
1760
1761 // Finally, when all else fails, nuke it
1762 if (-1 == $move_old_destination || 0 == $move_old_destination || (!empty($move_old) && is_wp_error($move_old))) {
1763 if (-1 == $move_old_destination) {
1764 $updraftplus->log("$type: $wp_filesystem_dir: deleting contents");
1765 } else {
1766 $updraftplus->log("$type: $wp_filesystem_dir: deleting contents (as attempts to copy failed)");
1767 }
1768 $del_files = $wp_filesystem->dirlist($wp_filesystem_dir, true, false);
1769 if (empty($del_files)) $del_files = array();
1770 foreach ($del_files as $file => $filestruc) {
1771 if (empty($file)) continue;
1772 if (!$wp_filesystem->delete($wp_filesystem_dir.'/'.$file, true)) {
1773 $this->restore_log_permission_failure_message($wp_filesystem_dir, 'Delete '.$wp_filesystem_dir.'/'.$file);
1774 }
1775 }
1776 }
1777
1778 }
1779
1780 private function add_ajax_restore_admin_footer() {
1781 static $already = false;
1782 if (!$already) {
1783 $already = true;
1784 add_action('admin_footer', array($this, 'admin_footer_ajax_restore'));
1785 }
1786 }
1787
1788 /**
1789 * Unused
1790 */
1791 public function admin_footer_ajax_restore() {
1792 // TODO: The timestamp parameter is mandatory - we should abort (earlier) if there isn't one.
1793
1794 global $updraftplus;
1795 $nonce = $updraftplus->nonce;
1796 // TODO: Apparently empty
1797 $auth_code = esc_js($this->ajax_restore_auth_code);
1798
1799 echo <<<ENDHERE
1800 <script>
1801 jQuery(document).ready(function() {
1802
1803 backupinfo = {
1804 action: 'updraft_ajaxrestore',
1805 subaction: 'restore',
1806 restorenonce: '$nonce',
1807 ajaxauth: '$auth_code'
1808 };
1809
1810 ENDHERE;
1811 $multisite = 0;
1812 $timestamp = $this->ud_backup_set['timestamp'];
1813 if (!empty($_REQUEST['updraft_restorer_backup_info'])) {
1814
1815 $backup_info = UpdraftPlus_Manipulation_Functions::wp_unslash($_REQUEST['updraft_restorer_backup_info']);
1816
1817 if (false != ($backup_info = json_decode($backup_info, true))) {
1818 if (!empty($backup_info['timestamp'])) echo "\t\tbackupinfo.timestamp = '".esc_js($backup_info['timestamp'])."';\n";
1819 if (!empty($backup_info['created_by_version'])) echo "\t\tbackupinfo.created_by_version = '".esc_js($backup_info['created_by_version'])."';\n";
1820 echo "\t\tbackupinfo.multisite = ".(empty($backup_info['multisite']) ? '0' : '1').";\n";
1821 }
1822
1823 }
1824
1825 echo <<<ENDHERE
1826
1827 // This is not just a list, but a queue
1828 var restore_these = [];
1829
1830 function do_ajax_restore(restore_this) {
1831
1832 var output = jQuery(restore_this.jqobject).find('.deferprogress');
1833 jQuery(output).html(updraftlion.processing);
1834
1835 alert("AJAX RESTORE: "+timestamp+" "+restore_this.type+" "+restore_this.backupfile);
1836
1837 backupinfo.type = restore_this.type;
1838 backupinfo.backupfile = restore_this.backupfile;
1839 backupinfo.lastone = restore_this.lastone
1840
1841 jQuery.post(ajaxurl, backupinfo, function(response) {
1842 console.log(response);
1843 try {
1844 var resp = JSON.parse(response);
1845 console.log(resp);
1846 // TODO: Do something
1847 } catch (err) {
1848 console.log(err);
1849 // TODO: Report error to user
1850 }
1851 // This recurses, but won't exhaust memory as there can only be a small number
1852 do_ajax_restore_queue();
1853 });
1854
1855
1856 }
1857
1858 function do_ajax_restore_queue() {
1859 if (restore_these.length < 1) { return; }
1860 // Shift gets the *first* element of the array
1861 restore_this = restore_these.shift();
1862 // This call is asychronous - i.e. the fact it returns doesn't indicate what has or hasn't now been done
1863 do_ajax_restore(restore_this);
1864 }
1865
1866 var multisite = $multisite;
1867 var timestamp = $timestamp;
1868
1869 // What follows is a slightly crude way of ensuring that the one marked 'lastone' actually does go last
1870 jQuery('.updraft-ajaxrestore').each(function(ind){
1871 var lastone = jQuery(this).data('lastone');
1872 if (!lastone) {
1873 var thing_to_restore = {};
1874 thing_to_restore.type = jQuery(this).data('type');
1875 thing_to_restore.backupfile = jQuery(this).data('backupfile');
1876 thing_to_restore.jqobject = this;
1877 thing_to_restore.lastone = 0;
1878 restore_these.push(thing_to_restore);
1879 }
1880 });
1881 jQuery('.updraft-ajaxrestore').each(function(ind){
1882 var lastone = jQuery(this).data('lastone');
1883 if (lastone) {
1884 var thing_to_restore = {};
1885 thing_to_restore.type = jQuery(this).data('type');
1886 thing_to_restore.backupfile = jQuery(this).data('backupfile');
1887 thing_to_restore.jqobject = this;
1888 restore_these.push(thing_to_restore);
1889 thing_to_restore.lastone = 1;
1890 }
1891 });
1892
1893 if (restore_these.length > 0) {
1894 do_ajax_restore_queue();
1895 }
1896
1897 });
1898 </script>
1899 ENDHERE;
1900 }
1901
1902 /**
1903 * First added in UD 1.9.47. We have only ever had reports of cached stuff from WP Super Cache being retained, so, being cautious, we will only clear that for now
1904 */
1905 public function clear_cache() {
1906 // Functions called here need to not assume that the relevant plugin actually exists - they should check for any functions they intend to call, before calling them.
1907 $this->clear_cache_wpsupercache();
1908 }
1909
1910 /**
1911 * Adapted from wp_cache_clean_cache($file_prefix, $all = false) in WP Super Cache (wp-cache.php)
1912 *
1913 * @return boolean
1914 */
1915 private function clear_cache_wpsupercache() {
1916 $all = true;
1917
1918 global $updraftplus, $cache_path, $wp_cache_object_cache;
1919
1920 if ($wp_cache_object_cache && function_exists('reset_oc_version')) reset_oc_version();
1921
1922 // Removed check: && wpsupercache_site_admin()
1923 if (true == $all && function_exists('prune_super_cache')) {
1924 if (!empty($cache_path)) {
1925 $updraftplus->log_e("Clearing cached pages (%s)...", 'WP Super Cache');
1926 prune_super_cache($cache_path, true);
1927 }
1928 return true;
1929 }
1930 }
1931
1932 private function search_for_folder($folder, $startat) {
1933 if (!is_dir($startat)) return false;
1934 // Exists in this folder?
1935 if (is_dir($startat.'/'.$folder)) return trailingslashit($startat).$folder;
1936 // Does not
1937 if ($handle = opendir($startat)) {
1938 while (($file = readdir($handle)) !== false) {
1939 if ('.' != $file && '..' != $file && is_dir($startat).'/'.$file) {
1940 $ss = $this->search_for_folder($folder, trailingslashit($startat).$file);
1941 if (is_string($ss)) return $ss;
1942 }
1943 }
1944 closedir($handle);
1945 }
1946 return false;
1947 }
1948
1949 /**
1950 * Returns an octal string (but not an octal number)
1951 *
1952 * @param String $file The file to get the permissions for
1953 * @param WP_Filesystem|Boolean $wpfs WP_Filesystem object, at least support the getchmod() method
1954 *
1955 * @return String
1956 */
1957 private function get_current_chmod($file, $wpfs = false) {
1958 if (false == $wpfs) {
1959 global $wp_filesystem;
1960 $wpfs = $wp_filesystem;
1961 }
1962 // getchmod() is broken at least as recently as WP3.8 - see: https://core.trac.wordpress.org/ticket/26598
1963 return (is_a($wpfs, 'WP_Filesystem_Direct')) ? substr(sprintf("%06d", decoct(@fileperms($file))), 3) : $wpfs->getchmod($file);
1964 }
1965
1966 /**
1967 * Returns a string in octal format
1968 * $new_chmod should be an octal, i.e. what you'd pass to chmod()
1969 *
1970 * @param string $old_chmod specify old chmod
1971 * @param string $new_chmod specify new chmod
1972 * @return string
1973 */
1974 private function calculate_additive_chmod_oct($old_chmod, $new_chmod) {
1975 // chmod() expects octal form, which means a preceding zero - see http://php.net/chmod
1976 $old_chmod = sprintf("%04d", $old_chmod);
1977 $new_chmod = sprintf("%04d", decoct($new_chmod));
1978
1979 for ($i=1; $i<=3; $i++) {
1980 $oldbit = substr($old_chmod, $i, 1);
1981 $newbit = substr($new_chmod, $i, 1);
1982 for ($j=0; $j<=2; $j++) {
1983 if (($oldbit & (1<<$j)) && !($newbit & (1<<$j))) {
1984 $newbit = (string) ($newbit | 1<<$j);
1985 $new_chmod = sprintf("%04d", substr($new_chmod, 0, $i).$newbit.substr($new_chmod, $i+1));
1986 }
1987 }
1988 }
1989
1990 return $new_chmod;
1991 }
1992
1993 /**
1994 * "If needed" means, "If the permissions are not already more permissive than this". i.e. This will not tighten permissions from what the user had before (we trust them)
1995 * $chmod should be an octal - i.e. the same as you'd pass to chmod()
1996 *
1997 * @param String $dir a WP_Filesystem path
1998 * @param String $chmod specific chmod
1999 * @param Boolean $recursive indicate if recursive chmod is needed
2000 * @param Boolean $wpfs indicate whether to use wpfs access methods
2001 * @param Boolean $suppress suppress PHP error/warning output
2002 *
2003 * @return Boolean - whether the operation was successfully carried out
2004 */
2005 private function chmod_if_needed($dir, $chmod, $recursive = false, $wpfs = false, $suppress = true) {
2006
2007 // Do nothing on Windows
2008 if ('WIN' === strtoupper(substr(php_uname('s'), 0, 3))) return true;
2009
2010 if (false == $wpfs) {
2011 global $wp_filesystem;
2012 $wpfs = $wp_filesystem;
2013 }
2014
2015 $old_chmod = $this->get_current_chmod($dir, $wpfs);
2016
2017 // Sanity check
2018 if (strlen($old_chmod) < 3) return false;
2019
2020 $new_chmod = $this->calculate_additive_chmod_oct($old_chmod, $chmod);
2021
2022 // Don't fix what isn't broken
2023 if (!$recursive && $new_chmod == $old_chmod) return true;
2024
2025 $new_chmod = octdec($new_chmod);
2026
2027 if ($suppress) {
2028 return @$wpfs->chmod($dir, $new_chmod, $recursive);
2029 } else {
2030 return $wpfs->chmod($dir, $new_chmod, $recursive);
2031 }
2032 }
2033
2034 /**
2035 * This will return the path with the actual content we want to restore, ignoring any other files that may be in the top level of the zip file
2036 * $dirnames: an array of preferred names
2037 *
2038 * @param string $working_dir specify working directory
2039 * @param string $dirnames directory names
2040 * @return string the final path with the content we want to restore
2041 */
2042 public function get_first_directory($working_dir, $dirnames) {
2043 global $wp_filesystem, $updraftplus;
2044 $fdirnames = array_flip($dirnames);
2045 $dirlist = $wp_filesystem->dirlist($working_dir, true, false);
2046 if (is_array($dirlist)) {
2047 $move_from = false;
2048 foreach ($dirlist as $name => $struc) {
2049 if (isset($struc['type']) && 'd' != $struc['type']) continue;
2050 if (false === $move_from) {
2051 if (isset($fdirnames[$name])) {
2052 $move_from = $working_dir . "/".$name;
2053 } elseif (preg_match('/^([^\.].*)$/', $name, $fmatch)) {
2054 // In the case of a third-party backup, the first entry may be the wrong entity. We could try a more sophisticated algorithm, but a third party backup requiring one has never been seen (and it is not easy to envisage what the algorithm might be).
2055 if (empty($this->ud_foreign)) {
2056 $first_entry = $working_dir."/".$fmatch[1];
2057 }
2058 }
2059 }
2060 }
2061 if (false === $move_from && isset($first_entry)) {
2062 $updraftplus->log_e('Using directory from backup: %s', basename($first_entry));
2063 $move_from = $first_entry;
2064 }
2065 } else {
2066 // That shouldn't happen. Fall back to default
2067 $move_from = $working_dir."/".$dirnames[0];
2068 }
2069 return $move_from;
2070 }
2071
2072 /**
2073 * Gets the table prefix to use, using the filter updraftplus_restore_set_import_table_prefix
2074 *
2075 * @param String $import_table_prefix - table prefix to act upon
2076 *
2077 * @return String|WP_Error|Boolean - the modified table prefix, or an error or indication of an error
2078 */
2079 private function pre_sql_actions($import_table_prefix) {
2080
2081 global $updraftplus;
2082
2083 $import_table_prefix = apply_filters('updraftplus_restore_set_table_prefix', $import_table_prefix, $this->ud_backup_is_multisite);
2084
2085 if (!is_string($import_table_prefix)) {
2086 $this->wp_upgrader->maintenance_mode(false);
2087 if (false === $import_table_prefix) {
2088 $updraftplus->log(__('Please supply the requested information, and then continue.', 'updraftplus'), 'notice-restore');
2089 return false;
2090 } elseif (is_wp_error($import_table_prefix)) {
2091 return $import_table_prefix;
2092 } else {
2093 return new WP_Error('invalid_table_prefix', __('Error:', 'updraftplus').' '.serialize($import_table_prefix));
2094 }
2095 }
2096
2097 $updraftplus->log_e('New table prefix: %s', $import_table_prefix);
2098
2099 return $import_table_prefix;
2100
2101 }
2102
2103 /**
2104 * WordPress options filter
2105 *
2106 * @param String $val - pre-filter value
2107 *
2108 * @return String - filtered value
2109 */
2110 public function option_filter_permalink_structure($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
2111 global $updraftplus;
2112 return $updraftplus->option_filter_get('permalink_structure');
2113 }
2114
2115 /**
2116 * WordPress options filter
2117 *
2118 * @param String $val - pre-filter value
2119 *
2120 * @return String - filtered value
2121 */
2122 public function option_filter_page_on_front($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
2123 global $updraftplus;
2124 return $updraftplus->option_filter_get('page_on_front');
2125 }
2126
2127 /**
2128 * WordPress options filter
2129 *
2130 * @param String $val - pre-filter value
2131 *
2132 * @return String - filtered value
2133 */
2134 public function option_filter_rewrite_rules($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
2135 global $updraftplus;
2136 return $updraftplus->option_filter_get('rewrite_rules');
2137 }
2138
2139 /**
2140 * Restore the database backup
2141 *
2142 * @param string $working_dir specify working directory
2143 * @param string $working_dir_localpath specify working local directory
2144 * @param string $import_table_prefix table prefix to use
2145 * @return boolean|WP_Error
2146 */
2147 private function restore_backup_db($working_dir, $working_dir_localpath, $import_table_prefix) {
2148
2149 global $updraftplus;
2150
2151 do_action('updraftplus_restore_db_pre');
2152
2153 // This is now a legacy option (at least on the front end), so we should not see it much
2154 $this->prior_upload_path = get_option('upload_path');
2155
2156 // There is a file backup.db(.gz) inside the working directory
2157
2158 // The 'off' check is for badly configured setups - http://wordpress.org/support/topic/plugin-wp-super-cache-warning-php-safe-mode-enabled-but-safe-mode-is-off
2159 // @codingStandardsIgnoreLine
2160 if (@ini_get('safe_mode') && 'off' != strtolower(@ini_get('safe_mode'))) {
2161 $updraftplus->log(__('Warning: PHP safe_mode is active on your server. Timeouts are much more likely. If these happen, then you will need to manually restore the file via phpMyAdmin or another method.', 'updraftplus'), 'notice-restore');
2162 }
2163
2164 $db_basename = 'backup.db.gz';
2165 if (!empty($this->ud_foreign)) {
2166 $plugins = apply_filters('updraftplus_accept_archivename', array());
2167
2168 if (empty($plugins[$this->ud_foreign])) return new WP_Error('unknown', sprintf(__('Backup created by unknown source (%s) - cannot be restored.', 'updraftplus'), $this->ud_foreign));
2169
2170 if (!file_exists($working_dir_localpath.'/'.$db_basename) && file_exists($working_dir_localpath.'/backup.db')) {
2171 $db_basename = 'backup.db';
2172 } elseif (!file_exists($working_dir_localpath.'/'.$db_basename) && file_exists($working_dir_localpath.'/backup.db.bz2')) {
2173 $db_basename = 'backup.db.bz2';
2174 }
2175
2176 if (!file_exists($working_dir_localpath.'/'.$db_basename)) {
2177 $separatedb = empty($plugins[$this->ud_foreign]['separatedb']) ? false : true;
2178 $filtered_db_name = apply_filters('updraftplus_foreign_dbfilename', false, $this->ud_foreign, $this->ud_backup_set, $working_dir_localpath, $separatedb);
2179 if (is_string($filtered_db_name)) $db_basename = $filtered_db_name;
2180 }
2181 }
2182
2183 // wp_filesystem has no gzopen method, so we switch to using the local filesystem (which is harmless, since we are performing read-only operations)
2184 if (false === $db_basename || !is_readable($working_dir_localpath.'/'.$db_basename)) return new WP_Error('dbopen_failed', __('Failed to find database file', 'updraftplus')." ($working_dir/".$db_basename.")");
2185
2186 global $wpdb, $updraftplus;
2187
2188 $this->skin->feedback($this->strings['restore_database']);
2189
2190 $is_plain = ('.db' == substr($db_basename, -3, 3));
2191 $is_bz2 = ('.db.bz2' == substr($db_basename, -7, 7));
2192
2193 // Read-only access: don't need to go through WP_Filesystem
2194 if ($is_plain) {
2195 $dbhandle = fopen($working_dir_localpath.'/'.$db_basename, 'r');
2196 } elseif ($is_bz2) {
2197 if (!function_exists('bzopen')) {
2198 $updraftplus->log_e("Your web server's PHP installation has these functions disabled: %s.", 'bzopen');
2199 $updraftplus->log_e('Your hosting company must enable these functions before %s can work.', __('restoration', 'updraftplus'));
2200 }
2201 $dbhandle = bzopen($working_dir_localpath.'/'.$db_basename, 'r');
2202 } else {
2203 $dbhandle = gzopen($working_dir_localpath.'/'.$db_basename, 'r');
2204 }
2205 if (!$dbhandle) return new WP_Error('dbopen_failed', __('Failed to open database file', 'updraftplus'));
2206
2207 $this->line = 0;
2208
2209 if ($this->use_wpdb()) {
2210 $updraftplus->log_e('Database access: Direct MySQL access is not available, so we are falling back to wpdb (this will be considerably slower)');
2211 } else {
2212 $updraftplus->log("Using direct MySQL access; value of use_mysqli is: ".($this->use_mysqli ? '1' : '0'));
2213 if ($this->use_mysqli) {
2214 @mysqli_query($this->mysql_dbh, 'SET SESSION query_cache_type = OFF;');
2215 } else {
2216 // @codingStandardsIgnoreLine
2217 @mysql_query('SET SESSION query_cache_type = OFF;', $this->mysql_dbh);
2218 }
2219 }
2220
2221 // Find the supported engines - in case the dump had something else (case seen: saved from MariaDB with engine Aria; imported into plain MySQL without)
2222 $supported_engines = $wpdb->get_results("SHOW ENGINES", OBJECT_K);
2223 $supported_charsets = $wpdb->get_results("SHOW CHARACTER SET", OBJECT_K);
2224 $db_supported_collations_res = $wpdb->get_results('SHOW COLLATION', OBJECT_K);
2225 $supported_collations = (null !== $db_supported_collations_res) ? $db_supported_collations_res : array();
2226 $updraft_restorer_collate = isset($this->restore_options['updraft_restorer_collate']) ? $this->restore_options['updraft_restorer_collate'] : '';
2227
2228 $this->errors = 0;
2229 $this->statements_run = 0;
2230 $this->insert_statements_run = 0;
2231 $this->tables_created = 0;
2232
2233 $sql_line = "";
2234 $sql_type = -1;
2235
2236 $this->start_time = microtime(true);
2237
2238 $old_wpversion = '';
2239 $this->old_siteurl = '';
2240 $this->old_home = '';
2241 $this->old_content = '';
2242 $this->old_uploads = '';
2243 $this->old_table_prefix = (defined('UPDRAFTPLUS_OVERRIDE_IMPORT_PREFIX') && UPDRAFTPLUS_OVERRIDE_IMPORT_PREFIX) ? UPDRAFTPLUS_OVERRIDE_IMPORT_PREFIX : '';
2244 $old_siteinfo = array();
2245 $gathering_siteinfo = true;
2246
2247 $this->create_forbidden = false;
2248 $this->drop_forbidden = false;
2249 $this->lock_forbidden = false;
2250
2251 $this->last_error = '';
2252 $random_table_name = 'updraft_tmp_'.rand(0, 9999999).md5(microtime(true));
2253
2254 // The only purpose in funnelling queries directly here is to be able to get the error number
2255 if ($this->use_wpdb()) {
2256 $req = $wpdb->query("CREATE TABLE $random_table_name (test INT)");
2257 // WPDB, for several query types, returns the number of rows changed; in distinction from an error, indicated by (bool)false
2258 if (0 === $req) {
2259 $req = true;
2260 }
2261 if (!$req) $this->last_error = $wpdb->last_error;
2262 $this->last_error_no = false;
2263 } else {
2264 if ($this->use_mysqli) {
2265 $req = mysqli_query($this->mysql_dbh, "CREATE TABLE $random_table_name (test INT)");
2266 } else {
2267 // @codingStandardsIgnoreLine
2268 $req = mysql_unbuffered_query("CREATE TABLE $random_table_name (test INT)", $this->mysql_dbh);
2269 }
2270 if (!$req) {
2271 // @codingStandardsIgnoreLine
2272 $this->last_error = ($this->use_mysqli) ? mysqli_error($this->mysql_dbh) : mysql_error($this->mysql_dbh);
2273 // @codingStandardsIgnoreLine
2274 $this->last_error_no = ($this->use_mysqli) ? mysqli_errno($this->mysql_dbh) : mysql_errno($this->mysql_dbh);
2275 }
2276 }
2277
2278 if (!$req && ($this->use_wpdb() || 1142 === $this->last_error_no)) {
2279 $this->create_forbidden = true;
2280 // If we can't create, then there's no point dropping
2281 $this->drop_forbidden = true;
2282
2283 // abort dummy restore process
2284 if ($this->is_dummy_db_restore) {
2285 return new WP_Error('abort_dummy_restore', __('Your database user does not have permission to drop tables', 'updraftplus'));
2286 }
2287
2288 $updraftplus->log(__('Your database user does not have permission to create tables. We will attempt to restore by simply emptying the tables; this should work as long as a) you are restoring from a WordPress version with the same database structure, and b) Your imported database does not contain any tables which are not already present on the importing site.', 'updraftplus'), 'warning-restore');
2289
2290 $updraftplus->log('Your database user does not have permission to create tables. We will attempt to restore by simply emptying the tables; this should work as long as a) you are restoring from a WordPress version with the same database structure, and b) Your imported database does not contain any tables which are not already present on the importing site.');
2291
2292 $updraftplus->log('Error was: '.$this->last_error.' ('.$this->last_error_no.')');
2293 } else {
2294
2295 if (1142 === $this->lock_table($random_table_name)) {
2296 $this->lock_forbidden = true;
2297 $updraftplus->log("Database user has no permission to lock tables - will not lock after CREATE");
2298 }
2299
2300 if ($this->use_wpdb()) {
2301 $req = $wpdb->query("DROP TABLE $random_table_name");
2302 // WPDB, for several query types, returns the number of rows changed; in distinction from an error, indicated by (bool)false
2303 if (0 === $req) {
2304 $req = true;
2305 }
2306 if (!$req) $this->last_error = $wpdb->last_error;
2307 $this->last_error_no = false;
2308 } else {
2309 if ($this->use_mysqli) {
2310 $req = mysqli_query($this->mysql_dbh, "DROP TABLE $random_table_name");
2311 } else {
2312 // @codingStandardsIgnoreLine
2313 $req = mysql_unbuffered_query("DROP TABLE $random_table_name", $this->mysql_dbh);
2314 }
2315 if (!$req) {
2316 // @codingStandardsIgnoreLine
2317 $this->last_error = ($this->use_mysqli) ? mysqli_error($this->mysql_dbh) : mysql_error($this->mysql_dbh);
2318 // @codingStandardsIgnoreLine
2319 $this->last_error_no = ($this->use_mysqli) ? mysqli_errno($this->mysql_dbh) : mysql_errno($this->mysql_dbh);
2320 }
2321 }
2322 if (!$req && ($this->use_wpdb() || 1142 === $this->last_error_no)) {
2323 $this->drop_forbidden = true;
2324
2325 // abort dummy restore process
2326 if ($this->is_dummy_db_restore) {
2327 return new WP_Error('abort_dummy_restore', __('Your database user does not have permission to drop tables', 'updraftplus'));
2328 }
2329
2330 $updraftplus->log(sprintf('Your database user does not have permission to drop tables. We will attempt to restore by simply emptying the tables; this should work as long as you are restoring from a WordPress version with the same database structure (%s)', '('.$this->last_error.', '.$this->last_error_no.')'));
2331
2332 $updraftplus->log(sprintf(__('Your database user does not have permission to drop tables. We will attempt to restore by simply emptying the tables; this should work as long as you are restoring from a WordPress version with the same database structure (%s)', 'updraftplus'), '('.$this->last_error.', '.$this->last_error_no.')'), 'warning-restore');
2333
2334 }
2335 }
2336
2337 $restoring_table = '';
2338
2339 $this->max_allowed_packet = $updraftplus->max_packet_size();
2340
2341 $updraftplus->log("Entering maintenance mode");
2342 $this->wp_upgrader->maintenance_mode(true);
2343
2344 // N.B. There is no such function as bzeof() - we have to detect that another way
2345 while (($is_plain && !feof($dbhandle)) || (!$is_plain && (($is_bz2) || (!$is_bz2 && !gzeof($dbhandle))))) {
2346 // Up to 1Mb
2347 if ($is_plain) {
2348 $buffer = rtrim(fgets($dbhandle, 1048576));
2349 } elseif ($is_bz2) {
2350 if (!isset($bz2_buffer)) $bz2_buffer = '';
2351 $buffer = '';
2352 if (strlen($bz2_buffer) < 524288) $bz2_buffer .= bzread($dbhandle, 1048576);
2353 if (bzerrno($dbhandle) !== 0) {
2354 $updraftplus->log("bz2 error: ".bzerrstr($dbhandle)." (code: ".bzerrno($bzhandle).")");
2355 break;
2356 }
2357 if (false !== $bz2_buffer && '' !== $bz2_buffer) {
2358 if (false !== ($p = strpos($bz2_buffer, "\n"))) {
2359 $buffer .= substr($bz2_buffer, 0, $p+1);
2360 $bz2_buffer = substr($bz2_buffer, $p+1);
2361 } else {
2362 $buffer .= $bz2_buffer;
2363 $bz2_buffer = '';
2364 }
2365 } else {
2366 break;
2367 }
2368 $buffer = rtrim($buffer);
2369 } else {
2370 $buffer = rtrim(gzgets($dbhandle, 1048576));
2371 }
2372
2373 // Discard comments
2374 if (empty($buffer) || '#' == substr($buffer, 0, 1) || preg_match('/^--(\s|$)/', substr($buffer, 0, 3))) {
2375 if ('' == $this->old_siteurl && preg_match('/^\# Backup of: (http(.*))$/', $buffer, $matches)) {
2376 $this->old_siteurl = untrailingslashit($matches[1]);
2377 $updraftplus->log("Backup of: ".$this->old_siteurl);
2378 $updraftplus->log(sprintf(__('Backup of: %s', 'updraftplus'), $this->old_siteurl), 'notice-restore', 'backup-of');
2379 do_action('updraftplus_restore_db_record_old_siteurl', $this->old_siteurl);
2380
2381 $this->save_configuration_bundle();
2382
2383 } elseif (false === $this->created_by_version && preg_match('/^\# Created by UpdraftPlus version ([\d\.]+)/', $buffer, $matches)) {
2384 $this->created_by_version = trim($matches[1]);
2385 $updraftplus->log(__('Backup created by:', 'updraftplus').' '.$this->created_by_version, 'notice-restore', 'created-by');
2386 $updraftplus->log('Backup created by: '.$this->created_by_version);
2387 } elseif ('' == $this->old_home && preg_match('/^\# Home URL: (http(.*))$/', $buffer, $matches)) {
2388 $this->old_home = untrailingslashit($matches[1]);
2389 if ($this->old_siteurl && $this->old_home != $this->old_siteurl) {
2390 $updraftplus->log(__('Site home:', 'updraftplus').' '.$this->old_home, 'notice-restore', 'site-home');
2391 $updraftplus->log('Site home: '.$this->old_home);
2392 }
2393 do_action('updraftplus_restore_db_record_old_home', $this->old_home);
2394 } elseif ('' == $this->old_content && preg_match('/^\# Content URL: (http(.*))$/', $buffer, $matches)) {
2395 $this->old_content = untrailingslashit($matches[1]);
2396 $updraftplus->log(__('Content URL:', 'updraftplus').' '.$this->old_content, 'notice-restore', 'content-url');
2397 $updraftplus->log('Content URL: '.$this->old_content);
2398 do_action('updraftplus_restore_db_record_old_content', $this->old_content);
2399 } elseif ('' == $this->old_uploads && preg_match('/^\# Uploads URL: (http(.*))$/', $buffer, $matches)) {
2400 $this->old_uploads = untrailingslashit($matches[1]);
2401 $updraftplus->log(__('Uploads URL:', 'updraftplus').' '.$this->old_uploads, 'notice-restore', 'uploads-url');
2402 $updraftplus->log('Uploads URL: '.$this->old_uploads);
2403 do_action('updraftplus_restore_db_record_old_uploads', $this->old_uploads);
2404 } elseif ('' == $this->old_table_prefix && (preg_match('/^\# Table prefix: (\S+)$/', $buffer, $matches) || preg_match('/^-- Table Prefix: (\S+)$/i', $buffer, $matches))) {
2405 // We also support backwpup style:
2406 // -- Table Prefix: wp_
2407 $this->old_table_prefix = $matches[1];
2408 $updraftplus->log(__('Old table prefix:', 'updraftplus').' '.$this->old_table_prefix, 'notice-restore', 'old-table-prefix');
2409 $updraftplus->log("Old table prefix: ".$this->old_table_prefix);
2410 } elseif (preg_match('/^\# Skipped tables: (.*)$/', $buffer, $matches)) {
2411 $skipped_tables = explode(',', $matches[1]);
2412 $updraftplus->log(__('Skipped tables:', 'updraftplus').' '.$matches[1], 'notice-restore', 'skipped-tables');
2413 $updraftplus->log("Skipped tables: ".$matches[1]);
2414 } elseif ($gathering_siteinfo && preg_match('/^\# Site info: (\S+)$/', $buffer, $matches)) {
2415 if ('end' == $matches[1]) {
2416 $gathering_siteinfo = false;
2417 // Sanity checks
2418 if (isset($old_siteinfo['multisite']) && !$old_siteinfo['multisite'] && is_multisite()) {
2419 if (!class_exists('UpdraftPlusAddOn_MultiSite') || !class_exists('UpdraftPlus_Addons_Migrator')) {
2420 return new WP_Error('missing_addons', sprintf(__('To import an ordinary WordPress site into a multisite installation requires %s.', 'updraftplus'), 'UpdraftPlus Premium'));
2421 }
2422 }
2423 } elseif (preg_match('/^([^=]+)=(.*)$/', $matches[1], $kvmatches)) {
2424 $key = $kvmatches[1];
2425 $val = $kvmatches[2];
2426 $updraftplus->log(__('Site information:', 'updraftplus')." $key = $val", 'notice-restore', 'site-information');
2427 $updraftplus->log("Site information: $key=$val");
2428 $old_siteinfo[$key] = $val;
2429 if ('multisite' == $key) {
2430 $this->ud_backup_is_multisite = ($val) ? 1 : 0;
2431 }
2432 }
2433 }
2434 continue;
2435 }
2436
2437 // Detect INSERT commands early, so that we can split them if necessary
2438 if (preg_match('/^\s*(insert into \`?([^\`]*)\`?\s+(values|\())/i', $sql_line.$buffer, $matches)) {
2439 $this->table_name = $matches[2];
2440 $sql_type = 3;
2441 $insert_prefix = $matches[1];
2442 }
2443
2444 // Deal with case where adding this line will take us over the MySQL max_allowed_packet limit - must split, if we can (if it looks like consecutive rows)
2445 // Allow a 100-byte margin for error (including searching/replacing table prefix)
2446 if (3 == $sql_type && $sql_line && strlen($sql_line.$buffer) > ($this->max_allowed_packet - 100) && preg_match('/,\s*$/', $sql_line) && preg_match('/^\s*\(/', $buffer)) {
2447 // Remove the final comma; replace with semi-colon
2448 $sql_line = substr(rtrim($sql_line), 0, strlen($sql_line)-1).';';
2449 if ('' != $this->old_table_prefix && $import_table_prefix != $this->old_table_prefix) $sql_line = UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $sql_line);
2450 // Run the SQL command; then set up for the next one.
2451 $this->line++;
2452 $updraftplus->log(__("Split line to avoid exceeding maximum packet size", 'updraftplus')." (".strlen($sql_line)." + ".strlen($buffer)." : ".$this->max_allowed_packet.")", 'notice-restore');
2453 $updraftplus->log("Split line to avoid exceeding maximum packet size (".strlen($sql_line)." + ".strlen($buffer)." : ".$this->max_allowed_packet.")");
2454 $do_exec = $this->sql_exec($sql_line, $sql_type, $import_table_prefix);
2455 if (is_wp_error($do_exec)) return $do_exec;
2456 // Reset, then carry on
2457 $sql_line = $insert_prefix." ";
2458 }
2459
2460 $sql_line .= $buffer;
2461 // Do we have a complete line yet? We used to just test the final character for ';' here (up to 1.8.12), but that was too unsophisticated
2462 if ((3 == $sql_type && !preg_match('/\)\s*;$/', substr($sql_line, -3, 3))) || (3 != $sql_type && ';' != substr($sql_line, -1, 1))) continue;
2463
2464 $this->line++;
2465
2466 // We now have a complete line - process it
2467
2468 if (3 == $sql_type && $sql_line && strlen($sql_line) > $this->max_allowed_packet) {
2469 $this->log_oversized_packet($sql_line);
2470 // Reset
2471 $sql_line = '';
2472 $sql_type = -1;
2473 // If this is the very first SQL line of the options table, we need to bail; it's essential
2474 if (0 == $this->insert_statements_run && $restoring_table && $restoring_table == $import_table_prefix.'options') {
2475 $updraftplus->log("Leaving maintenance mode");
2476 $this->wp_upgrader->maintenance_mode(false);
2477 return new WP_Error('initial_db_error', sprintf(__('An error occurred on the first %s command - aborting run', 'updraftplus'), 'INSERT (options)'));
2478 }
2479 continue;
2480 }
2481
2482 // The timed overhead of this is negligible
2483 if (preg_match('/^\s*drop table (if exists )?\`?([^\`]*)\`?\s*;/i', $sql_line, $matches)) {
2484 $sql_type = 1;
2485
2486 if (!isset($printed_new_table_prefix)) {
2487 $import_table_prefix = $this->pre_sql_actions($import_table_prefix);
2488 if (false === $import_table_prefix || is_wp_error($import_table_prefix)) return $import_table_prefix;
2489 $printed_new_table_prefix = true;
2490 }
2491
2492 $this->table_name = $matches[2];
2493
2494 // Legacy, less reliable - in case it was not caught before
2495 if ('' == $this->old_table_prefix && preg_match('/^([a-z0-9]+)_.*$/i', $this->table_name, $tmatches)) {
2496 $this->old_table_prefix = $tmatches[1].'_';
2497 $updraftplus->log(__('Old table prefix:', 'updraftplus').' '.$this->old_table_prefix, 'notice-restore', 'old-table-prefix');
2498 $updraftplus->log("Old table prefix (detected from first table): ".$this->old_table_prefix);
2499 }
2500
2501 $this->new_table_name = $this->old_table_prefix ? UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $this->table_name) : $this->table_name;
2502
2503 if ('' != $this->old_table_prefix && $import_table_prefix != $this->old_table_prefix) {
2504 $sql_line = UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $sql_line);
2505 }
2506
2507 if (empty($matches[1])) {
2508 // Seen with some foreign backups
2509 $sql_line = preg_replace('/drop table/i', 'drop table if exists', $sql_line, 1);
2510 }
2511
2512 $this->tables_been_dropped[] = $this->new_table_name;
2513
2514 } elseif (preg_match('/^\s*create table \`?([^\`\(]*)\`?\s*\(/i', $sql_line, $matches)) {
2515
2516 $sql_type = 2;
2517 $this->insert_statements_run = 0;
2518 $this->table_name = $matches[1];
2519
2520 // Legacy, less reliable - in case it was not caught before. We added it in here (CREATE) as well as in DROP because of SQL dumps which lack DROP statements.
2521 if ('' == $this->old_table_prefix && preg_match('/^([a-z0-9]+)_.*$/i', $this->table_name, $tmatches)) {
2522 $this->old_table_prefix = $tmatches[1].'_';
2523 $updraftplus->log(__('Old table prefix:', 'updraftplus').' '.$this->old_table_prefix, 'notice-restore', 'old-table-prefix');
2524 $updraftplus->log("Old table prefix (detected from creating first table): ".$this->old_table_prefix);
2525 }
2526
2527 // MySQL 4.1 outputs TYPE=, but accepts ENGINE=; 5.1 onwards accept *only* ENGINE=
2528 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace('TYPE=', 'ENGINE=', $sql_line);
2529
2530 if (empty($printed_new_table_prefix)) {
2531 $import_table_prefix = $this->pre_sql_actions($import_table_prefix);
2532 if (false === $import_table_prefix || is_wp_error($import_table_prefix)) return $import_table_prefix;
2533 $printed_new_table_prefix = true;
2534 }
2535
2536 $this->new_table_name = $this->old_table_prefix ? UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $this->table_name) : $this->table_name;
2537
2538 // This CREATE TABLE command may be the de-facto mark for the end of processing a previous table (which is so if this is not the first table in the SQL dump)
2539 if ($restoring_table) {
2540
2541 // Attempt to reconnect if the DB connection dropped (may not succeed, of course - but that will soon become evident)
2542 $updraftplus->check_db_connection($this->wpdb_obj);
2543
2544 // After restoring the options table, we can set old_siteurl if on legacy (i.e. not already set)
2545 if ($restoring_table == $import_table_prefix.'options') {
2546 if ('' == $this->old_siteurl || '' == $this->old_home || '' == $this->old_content) {
2547 global $updraftplus_addons_migrator;
2548 if (!empty($updraftplus_addons_migrator->new_blogid)) switch_to_blog($updraftplus_addons_migrator->new_blogid);
2549
2550 if ('' == $this->old_siteurl) {
2551 $this->old_siteurl = untrailingslashit($wpdb->get_row("SELECT option_value FROM $wpdb->options WHERE option_name='siteurl'")->option_value);
2552 do_action('updraftplus_restore_db_record_old_siteurl', $this->old_siteurl);
2553 }
2554 if ('' == $this->old_home) {
2555 $this->old_home = untrailingslashit($wpdb->get_row("SELECT option_value FROM $wpdb->options WHERE option_name='home'")->option_value);
2556 do_action('updraftplus_restore_db_record_old_home', $this->old_home);
2557 }
2558 if ('' == $this->old_content) {
2559 $this->old_content = $this->old_siteurl.'/wp-content';
2560 do_action('updraftplus_restore_db_record_old_content', $this->old_content);
2561 }
2562 if (!empty($updraftplus_addons_migrator->new_blogid)) restore_current_blog();
2563 }
2564 }
2565
2566 if ($restoring_table != $this->new_table_name) $this->restored_table($restoring_table, $import_table_prefix, $this->old_table_prefix);
2567
2568 }
2569 $engine = "(?)";
2570 $engine_change_message = '';
2571 if (preg_match('/ENGINE=([^\s;]+)/', $sql_line, $eng_match)) {
2572 $engine = $eng_match[1];
2573 if (isset($supported_engines[$engine])) {
2574 if ('myisam' == strtolower($engine)) {
2575 $sql_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $sql_line, 1);
2576 }
2577 } else {
2578 $engine_change_message = sprintf(__('Requested table engine (%s) is not present - changing to MyISAM.', 'updraftplus'), $engine)."<br>";
2579 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace("ENGINE=$engine", "ENGINE=MyISAM", $sql_line);
2580 // Remove (M)aria options
2581 if ('maria' == strtolower($engine) || 'aria' == strtolower($engine)) {
2582 $sql_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $sql_line, 1);
2583 $sql_line = preg_replace('/TRANSACTIONAL=\d\s?/', '', $sql_line, 1);
2584 }
2585 }
2586 }
2587 $charset_change_message = '';
2588 if (preg_match('/ CHARSET=([^\s;]+)/i', $sql_line, $charset_match)) {
2589 $charset = $charset_match[1];
2590 if (!isset($supported_charsets[$charset])) {
2591 $charset_change_message = sprintf(__('Requested table character set (%s) is not present - changing to %s.', 'updraftplus'), esc_html($charset), esc_html($this->restore_options['updraft_restorer_charset']));
2592 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace("CHARSET=$charset", "CHARSET=".$this->restore_options['updraft_restorer_charset'], $sql_line);
2593 // Allow default COLLLATE to database
2594 if (preg_match('/ COLLATE=([^\s;]+)/i', $sql_line, $collate_match)) {
2595 $collate = $collate_match[1];
2596 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace(" COLLATE=$collate", "", $sql_line);
2597 }
2598 }
2599 }
2600 $collate_change_message = '';
2601 $unsupported_collates_in_sql_line = array();
2602 if (!empty($updraft_restorer_collate) && preg_match('/ COLLATE=([^\s]+)/i', $sql_line, $collate_match)) {
2603 $collate = $collate_match[1];
2604 if (!isset($supported_collations[$collate])) {
2605 $unsupported_collates_in_sql_line[] = $collate;
2606 if ('choose_a_default_for_each_table' == $updraft_restorer_collate) {
2607 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace("COLLATE=$collate", "", $sql_line, false);
2608 } else {
2609 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace("COLLATE=$collate", "COLLATE=".$updraft_restorer_collate, $sql_line, false);
2610 }
2611 }
2612 }
2613 if (!empty($updraft_restorer_collate) && preg_match_all('/ COLLATE ([a-zA-Z0-9._-]+) /i', $sql_line, $collate_matches)) {
2614 $collates = array_unique($collate_matches[1]);
2615 foreach ($collates as $collate) {
2616 if (!isset($supported_collations[$collate])) {
2617 $unsupported_collates_in_sql_line[] = $collate;
2618 if ('choose_a_default_for_each_table' == $updraft_restorer_collate) {
2619 $sql_line = str_ireplace("COLLATE $collate ", "", $sql_line);
2620 } else {
2621 $sql_line = str_ireplace("COLLATE $collate ", "COLLATE ".$updraft_restorer_collate." ", $sql_line);
2622 }
2623 }
2624 }
2625 }
2626 if (!empty($updraft_restorer_collate) && preg_match_all('/ COLLATE ([a-zA-Z0-9._-]+),/i', $sql_line, $collate_matches)) {
2627 $collates = array_unique($collate_matches[1]);
2628 foreach ($collates as $collate) {
2629 if (!isset($supported_collations[$collate])) {
2630 $unsupported_collates_in_sql_line[] = $collate;
2631 if ('choose_a_default_for_each_table' == $updraft_restorer_collate) {
2632 $sql_line = str_ireplace("COLLATE $collate,", ",", $sql_line);
2633 } else {
2634 $sql_line = str_ireplace("COLLATE $collate,", "COLLATE ".$updraft_restorer_collate.",", $sql_line);
2635 }
2636 }
2637 }
2638 }
2639 if (count($unsupported_collates_in_sql_line) > 0) {
2640 $unsupported_unique_collates_in_sql_line = array_unique($unsupported_collates_in_sql_line);
2641 $collate_change_message = sprintf(_n('Requested table collation (%1$s) is not present - changing to %2$s.', 'Requested table collations (%1$s) are not present - changing to %2$s.', count($unsupported_unique_collates_in_sql_line), 'updraftplus'), esc_html(implode(', ', $unsupported_unique_collates_in_sql_line)), esc_html($this->restore_options['updraft_restorer_collate']));
2642 }
2643 $print_line = sprintf(__('Processing table (%s)', 'updraftplus'), $engine).": ".$this->table_name;
2644 $logline = "Processing table ($engine): ".$this->table_name;
2645 if ('' != $this->old_table_prefix && $import_table_prefix != $this->old_table_prefix) {
2646 if ($this->restore_this_table($this->table_name)) {
2647 $print_line .= ' - '.__('will restore as:', 'updraftplus').' '.htmlspecialchars($this->new_table_name);
2648 $logline .= " - will restore as: ".$this->new_table_name;
2649 } else {
2650 $logline .= ' - skipping';
2651 }
2652 $sql_line = UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $sql_line);
2653
2654 $this->restored_table_names[] = $this->new_table_name;
2655 }
2656 $updraftplus->log($logline);
2657 $updraftplus->log($print_line, 'notice-restore');
2658 $restoring_table = $this->new_table_name;
2659 if ($charset_change_message) $updraftplus->log($charset_change_message, 'notice-restore');
2660 if ($collate_change_message) $updraftplus->log($collate_change_message, 'notice-restore');
2661 if ($engine_change_message) $updraftplus->log($engine_change_message, 'notice-restore');
2662
2663 } elseif (preg_match('/^\s*(insert into \`?([^\`]*)\`?\s+(values|\())/i', $sql_line, $matches)) {
2664 $sql_type = 3;
2665 $this->table_name = $matches[2];
2666 if ('' != $this->old_table_prefix && $import_table_prefix != $this->old_table_prefix) $sql_line = UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $sql_line);
2667 } elseif (preg_match('/^\s*(\/\*\!40000 )?(alter|lock) tables? \`?([^\`\(]*)\`?\s+(write|disable|enable)/i', $sql_line, $matches)) {
2668 // Only binary mysqldump produces this pattern (LOCK TABLES `table` WRITE, ALTER TABLE `table` (DISABLE|ENABLE) KEYS)
2669 $sql_type = 4;
2670 if ('' != $this->old_table_prefix && $import_table_prefix != $this->old_table_prefix) $sql_line = UpdraftPlus_Manipulation_Functions::str_replace_once($this->old_table_prefix, $import_table_prefix, $sql_line);
2671 } elseif (preg_match('/^(un)?lock tables/i', $sql_line)) {
2672 // BackWPup produces these
2673 $sql_type = 5;
2674 } elseif (preg_match('/^(create|drop) database /i', $sql_line)) {
2675 // WPB2D produces these, as do some phpMyAdmin dumps
2676 $sql_type = 6;
2677 } elseif (preg_match('/^use /i', $sql_line)) {
2678 // WPB2D produces these, as do some phpMyAdmin dumps
2679 $sql_type = 7;
2680 } elseif (preg_match('#^\s*/\*\!40\d+ (SET NAMES) (.*)\*\/#i', $sql_line, $smatches)) {
2681 $sql_type = 8;
2682 $charset = rtrim($smatches[2]);
2683 $connection_charset = $updraftplus->get_connection_charset();
2684 if ('utf8' === $charset && 'utf8mb4' === $connection_charset) {
2685 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace("SET NAMES $charset", "SET NAMES $connection_charset", $sql_line);
2686 $updraftplus->log(sprintf(__('Found SET NAMES %s, but changing to %s as suggested by WPDB::determine_charset().', 'updraftplus'), $charset, $connection_charset), 'notice-restore');
2687 $charset = $connection_charset;
2688 }
2689 $this->set_names = $charset;
2690 if (!isset($supported_charsets[$charset])) {
2691 $sql_line = UpdraftPlus_Manipulation_Functions::str_lreplace($smatches[1]." ".$charset, "SET NAMES ".$this->restore_options['updraft_restorer_charset'], $sql_line);
2692 $updraftplus->log('SET NAMES: '.sprintf(__('Requested character set (%s) is not present - changing to %s.', 'updraftplus'), esc_html($charset), esc_html($this->restore_options['updraft_restorer_charset'])), 'notice-restore');
2693 }
2694 } else {
2695 // Prevent the previous value of $sql_type being retained for an unknown type
2696 $sql_type = 0;
2697 }
2698
2699 if (6 != $sql_type && 7 != $sql_type) {
2700 $do_exec = $this->sql_exec($sql_line, $sql_type);
2701 if (is_wp_error($do_exec)) return $do_exec;
2702 } else {
2703 $updraftplus->log("Skipped SQL statement (unwanted type=$sql_type): $sql_line");
2704 }
2705
2706 // Reset
2707 $sql_line = '';
2708 $sql_type = -1;
2709
2710 }
2711
2712 // Rescan storage, but only if there was remote storage and a database; otherwise just re-scan locally
2713 if (!empty($this->ud_backup_set['db']) && !empty($this->ud_backup_set['service']) && ('none' !== $this->ud_backup_set['service'] && 'email' !== $this->ud_backup_set['service'] && array('') !== $this->ud_backup_set['service'] && array('none') !== $this->ud_backup_set['service'] && array('email') !== $this->ud_backup_set['service'])) {
2714 $only_add_this_file = array('file' => $this->ud_backup_set['db']);
2715 UpdraftPlus_Backup_History::rebuild(true, $only_add_this_file);
2716 } else {
2717 UpdraftPlus_Backup_History::rebuild();
2718 }
2719
2720 if (!empty($this->lock_forbidden)) {
2721 $updraftplus->log("Leaving maintenance mode");
2722 } else {
2723 $updraftplus->log("Unlocking database and leaving maintenance mode");
2724 $this->unlock_tables();
2725 }
2726 $this->wp_upgrader->maintenance_mode(false);
2727
2728 if ($restoring_table) $this->restored_table($restoring_table, $import_table_prefix, $this->old_table_prefix);
2729
2730 // drop the dummy restored tables
2731 if ($this->is_dummy_db_restore) $this->drop_tables($this->restored_table_names);
2732
2733 $time_taken = microtime(true) - $this->start_time;
2734 $updraftplus->log_e('Finished: lines processed: %d in %.2f seconds', $this->line, $time_taken);
2735 if ($is_plain) {
2736 fclose($dbhandle);
2737 } elseif ($is_bz2) {
2738 bzclose($dbhandle);
2739 } else {
2740 gzclose($dbhandle);
2741 }
2742
2743 global $wp_filesystem;
2744
2745 if (!$wp_filesystem->delete($working_dir.'/'.$db_basename, false, 'f')) {
2746 $this->restore_log_permission_failure_message($working_dir, 'Delete '.$working_dir.'/'.$db_basename);
2747 }
2748 return true;
2749
2750 }
2751
2752 private function lock_table($table) {
2753
2754 // Not yet working
2755 return true;
2756
2757 global $updraftplus;
2758 $table = UpdraftPlus_Manipulation_Functions::backquote($table);
2759
2760 if ($this->use_wpdb()) {
2761 $req = $wpdb->query("LOCK TABLES $table WRITE;");
2762 } else {
2763 if ($this->use_mysqli) {
2764 $req = mysqli_query($this->mysql_dbh, "LOCK TABLES $table WRITE;");
2765 } else {
2766 // @codingStandardsIgnoreLine
2767 $req = mysql_unbuffered_query("LOCK TABLES $table WRITE;", $this->mysql_dbh);
2768 }
2769 if (!$req) {
2770 // @codingStandardsIgnoreLine
2771 $lock_error_no = $this->use_mysqli ? mysqli_errno($this->mysql_dbh) : mysql_errno($this->mysql_dbh);
2772 }
2773 }
2774 if (!$req && ($this->use_wpdb() || 1142 === $lock_error_no)) {
2775 // Permission denied
2776 return 1142;
2777 }
2778 return true;
2779 }
2780
2781 public function unlock_tables() {
2782 return;
2783 // Not yet working
2784 if ($this->use_wpdb()) {
2785 $wpdb->query("UNLOCK TABLES;");
2786 } elseif ($this->use_mysqli) {
2787 $req = mysqli_query($this->mysql_dbh, "UNLOCK TABLES;");
2788 } else {
2789 // @codingStandardsIgnoreLine
2790 $req = mysql_unbuffered_query("UNLOCK TABLES;");
2791 }
2792 }
2793
2794 /**
2795 * Save configuration bundle, ready to restore it once the options table has been restored
2796 */
2797 private function save_configuration_bundle() {
2798 $this->configuration_bundle = array();
2799 // Some items must always be saved + restored; others only on a migration
2800 // Remember, if modifying this, that a restoration can include restoring a destroyed site from a backup onto a fresh WP install on the same URL. So, it is not necessarily desirable to retain the current settings and drop the ones in the backup.
2801 $keys_to_save = array('updraft_remotesites', 'updraft_migrator_localkeys', 'updraft_central_localkeys');
2802
2803 if ($this->old_siteurl != $this->our_siteurl || (defined('UPDRAFTPLUS_RESTORE_ALL_SETTINGS') && UPDRAFTPLUS_RESTORE_ALL_SETTINGS)) {
2804 global $updraftplus;
2805 $keys_to_save = array_merge($keys_to_save, $updraftplus->get_settings_keys());
2806 $keys_to_save[] = 'updraft_backup_history';
2807 }
2808
2809 foreach ($keys_to_save as $key) {
2810 $this->configuration_bundle[$key] = UpdraftPlus_Options::get_updraft_option($key);
2811 }
2812 }
2813
2814 /**
2815 * The table here is just for logging/info. The actual restoration itself is done via the standard options class.
2816 *
2817 * @param string $table specific table
2818 */
2819 private function restore_configuration_bundle($table) {
2820
2821 if (!is_array($this->configuration_bundle)) return;
2822 global $updraftplus;
2823 $updraftplus->log("Restoring prior UD configuration (table: $table; keys: ".count($this->configuration_bundle).")");
2824 foreach ($this->configuration_bundle as $key => $value) {
2825 UpdraftPlus_Options::delete_updraft_option($key);
2826 UpdraftPlus_Options::update_updraft_option($key, $value);
2827 }
2828 }
2829
2830 /**
2831 * Log the information that a particular SQL commandment is too long
2832 *
2833 * @param String $sql_line - the SQL
2834 */
2835 private function log_oversized_packet($sql_line) {
2836 global $updraftplus;
2837 $logit = substr($sql_line, 0, 100);
2838 $updraftplus->log(sprintf("An SQL line that is larger than the maximum packet size and cannot be split was found: %s", '('.strlen($sql_line).', '.$logit.' ...)'));
2839
2840 $updraftplus->log(__('Warning:', 'updraftplus').' '.sprintf(__("An SQL line that is larger than the maximum packet size and cannot be split was found; this line will not be processed, but will be dropped: %s", 'updraftplus'), '('.strlen($sql_line).', '.$this->max_allowed_packet.', '.$logit.' ...)'), 'notice-restore');
2841 }
2842
2843 private function restore_this_table($table_name) {
2844
2845 global $updraftplus;
2846 $unprefixed_table_name = substr($table_name, strlen($this->old_table_prefix));
2847
2848 // First, check whether it's a multisite site which we're not restoring. This is stored in restore_this_site (once we know the site).
2849 if (!empty($this->ud_multisite_selective_restore)) {
2850 if (preg_match('/^(\d+)_.*$/', $unprefixed_table_name, $matches)) {
2851 $site_id = $matches[1];
2852
2853 if (!isset($this->restore_this_site[$site_id])) {
2854 $this->restore_this_site[$site_id] = apply_filters(
2855 'updraftplus_restore_this_site',
2856 true,
2857 $site_id,
2858 $unprefixed_table_name,
2859 $this->restore_options
2860 );
2861 }
2862
2863 if (false === $this->restore_this_site[$site_id]) {
2864 // The first time it's looked into, it gets logged
2865 $updraftplus->log_e('Skipping site %s: this table (%s) and others from the site will not be restored', $site_id, $table_name);
2866 $this->restore_this_site[$site_id] = 0;
2867 }
2868
2869 if (!$this->restore_this_site[$site_id]) {
2870 return false;
2871 }
2872
2873 }
2874
2875 }
2876
2877 // Secondly, if we're still intending to proceed, check the table specifically
2878 if (!isset($this->restore_this_table[$table_name])) {
2879
2880 $this->restore_this_table[$table_name] = apply_filters(
2881 'updraftplus_restore_this_table',
2882 true,
2883 $unprefixed_table_name,
2884 $this->restore_options
2885 );
2886
2887 if (false === $this->restore_this_table[$table_name]) {
2888 // The first time it's looked into, it gets logged
2889 $updraftplus->log_e('Skipping table %s: this table will not be restored', $table_name);
2890 $this->restore_this_table[$table_name] = 0;
2891 }
2892
2893 }
2894
2895 return $this->restore_this_table[$table_name];
2896 }
2897
2898 /**
2899 * UPDATE is sql_type=5 (not used in the function, but used in Migrator and so noted here for reference)
2900 * $import_table_prefix is only use in one place in this function (long INSERTs), and otherwise need/should not be supplied
2901 *
2902 * @param string $sql_line sql line to execute
2903 * @param integer $sql_type sql type
2904 * @param string $import_table_prefix import type prefix
2905 * @param boolean $check_skipping if true, then check whether the table is on the list of tables to skip
2906 * @return Boolean|WP_Error|Void
2907 */
2908 public function sql_exec($sql_line, $sql_type, $import_table_prefix = '', $check_skipping = true) {
2909
2910 global $wpdb, $updraftplus;
2911
2912 if ($check_skipping && !empty($this->table_name) && !$this->restore_this_table($this->table_name)) return;
2913
2914 $ignore_errors = false;
2915 // Type 2 = CREATE TABLE
2916 if (2 == $sql_type && $this->create_forbidden) {
2917 $updraftplus->log_e('Cannot create new tables, so skipping this command (%s)', htmlspecialchars($sql_line));
2918 $req = true;
2919 } else {
2920
2921 if (2 == $sql_type && !$this->drop_forbidden) {
2922 // We choose, for now, to be very conservative - we only do the apparently-missing drop if we have never seen any drop - i.e. assume that in SQL dumps with missing DROPs, that it's because there are no DROPs at all
2923 if (!in_array($this->new_table_name, $this->tables_been_dropped)) {
2924 $updraftplus->log_e('Table to be implicitly dropped: %s', $this->new_table_name);
2925 $this->sql_exec('DROP TABLE IF EXISTS '.UpdraftPlus_Manipulation_Functions::backquote($this->new_table_name), 1, '', false);
2926 $this->tables_been_dropped[] = $this->new_table_name;
2927 }
2928 }
2929
2930 // Type 1 = DROP TABLE
2931 if (1 == $sql_type) {
2932 if ($this->drop_forbidden) {
2933 $sql_line = "DELETE FROM ".UpdraftPlus_Manipulation_Functions::backquote($this->new_table_name);
2934 $updraftplus->log_e('Cannot drop tables, so deleting instead (%s)', $sql_line);
2935 $ignore_errors = true;
2936 }
2937 }
2938
2939 if (3 == $sql_type && $sql_line && strlen($sql_line) > $this->max_allowed_packet) {
2940 $this->log_oversized_packet($sql_line);
2941 // If this is the very first SQL line of the options table, we need to bail; it's essential
2942 $this->errors++;
2943 if (0 == $this->insert_statements_run && $this->new_table_name && $this->new_table_name == $import_table_prefix.'options') {
2944 $updraftplus->log('Leaving maintenance mode');
2945 $this->wp_upgrader->maintenance_mode(false);
2946 return new WP_Error('initial_db_error', sprintf(__('An error occurred on the first %s command - aborting run', 'updraftplus'), 'INSERT (options)'));
2947 }
2948 return false;
2949 }
2950
2951 if ($this->use_wpdb()) {
2952 $req = $wpdb->query($sql_line);
2953 // WPDB, for several query types, returns the number of rows changed; in distinction from an error, indicated by (bool)false
2954 if (0 === $req) {
2955 $req = true;
2956 }
2957 if (!$req) $this->last_error = $wpdb->last_error;
2958 } else {
2959 if ($this->use_mysqli) {
2960 $req = mysqli_query($this->mysql_dbh, $sql_line);
2961 if (!$req) $this->last_error = mysqli_error($this->mysql_dbh);
2962 } else {
2963 // @codingStandardsIgnoreLine
2964 $req = mysql_unbuffered_query($sql_line, $this->mysql_dbh);
2965 // @codingStandardsIgnoreLine
2966 if (!$req) $this->last_error = mysql_error($this->mysql_dbh);
2967 }
2968 }
2969 if (3 == $sql_type) $this->insert_statements_run++;
2970 if (1 == $sql_type) $this->tables_been_dropped[] = $this->new_table_name;
2971 $this->statements_run++;
2972 }
2973
2974 if (!$req) {
2975 if (!$ignore_errors) $this->errors++;
2976 $print_err = (strlen($sql_line) > 100) ? substr($sql_line, 0, 100).' ...' : $sql_line;
2977 $updraftplus->log(sprintf(_x('An error (%s) occurred:', 'The user is being told the number of times an error has happened, e.g. An error (27) occurred', 'updraftplus'), $this->errors)." - ".$this->last_error." - ".__('the database query being run was:', 'updraftplus').' '.$print_err, 'notice-restore');
2978 $updraftplus->log("An error (".$this->errors.") occurred: ".$this->last_error." - SQL query was (type=$sql_type): ".substr($sql_line, 0, 65536));
2979
2980 // First command is expected to be DROP TABLE
2981 if (1 == $this->errors && 2 == $sql_type && 0 == $this->tables_created) {
2982 if ($this->drop_forbidden) {
2983 $updraftplus->log_e("Create table failed - probably because there is no permission to drop tables and the table already exists; will continue");
2984 } else {
2985 $updraftplus->log("Leaving maintenance mode");
2986 $this->wp_upgrader->maintenance_mode(false);
2987 return new WP_Error('initial_db_error', sprintf(__('An error occurred on the first %s command - aborting run', 'updraftplus'), 'CREATE TABLE'));
2988 }
2989 } elseif (2 == $sql_type && 0 == $this->tables_created && $this->drop_forbidden) {
2990 // Decrease error counter again; otherwise, we'll cease if there are >=50 tables
2991 if (!$ignore_errors) $this->errors--;
2992 } elseif (8 == $sql_type && 1 == $this->errors) {
2993 $updraftplus->log("Aborted: SET NAMES ".$this->set_names." failed: leaving maintenance mode");
2994 $this->wp_upgrader->maintenance_mode(false);
2995 $extra_msg = '';
2996 $dbv = $wpdb->db_version();
2997 if ('utf8mb4' == strtolower($this->set_names) && $dbv && version_compare($dbv, '5.2.0', '<=')) {
2998 $extra_msg = ' '.__('This problem is caused by trying to restore a database on a very old MySQL version that is incompatible with the source database.', 'updraftplus').' '.sprintf(__('This database needs to be deployed on MySQL version %s or later.', 'updraftplus'), '5.5');
2999 }
3000 return new WP_Error('initial_db_error', sprintf(__('An error occurred on the first %s command - aborting run', 'updraftplus'), 'SET NAMES').'. '.sprintf(__('To use this backup, your database server needs to support the %s character set.', 'updraftplus'), $this->set_names).$extra_msg);
3001 }
3002
3003 if ($this->errors > 49) {
3004 $this->wp_upgrader->maintenance_mode(false);
3005 return new WP_Error('too_many_db_errors', __('Too many database errors have occurred - aborting', 'updraftplus'));
3006 }
3007 } elseif (2 == $sql_type) {
3008 if (!$this->lock_forbidden) $this->lock_table($this->new_table_name);
3009 $this->tables_created++;
3010 do_action('updraftplus_creating_table', $this->new_table_name);
3011 }
3012
3013 if ($this->line >0 && 0 == $this->line % 50) {
3014 if ($this->line > $this->line_last_logged && (0 == $this->line % 250 || $this->line < 250)) {
3015 $this->line_last_logged = $this->line;
3016 $time_taken = microtime(true) - $this->start_time;
3017 $updraftplus->log_e('Database queries processed: %d in %.2f seconds', $this->line, $time_taken);
3018 }
3019 }
3020 return $req;
3021 }
3022
3023 private function flush_rewrite_rules() {
3024
3025 // We have to deal with the fact that the procedures used call get_option, which could be looking at the wrong table prefix, or have the wrong thing cached
3026
3027 global $updraftplus_addons_migrator;
3028 if (!empty($updraftplus_addons_migrator->new_blogid)) switch_to_blog($updraftplus_addons_migrator->new_blogid);
3029
3030 $filter_these = array('permalink_structure', 'rewrite_rules', 'page_on_front');
3031
3032 foreach ($filter_these as $opt) {
3033 add_filter('pre_option_'.$opt, array($this, 'option_filter_'.$opt));
3034 }
3035
3036 global $wp_rewrite;
3037 $wp_rewrite->init();
3038 // Don't do this: it will cause rules created by plugins that weren't active at the start of the restore run to be lost
3039 // flush_rewrite_rules(true);
3040
3041 if (function_exists('save_mod_rewrite_rules')) save_mod_rewrite_rules();
3042 if (function_exists('iis7_save_url_rewrite_rules')) iis7_save_url_rewrite_rules();
3043
3044 foreach ($filter_these as $opt) {
3045 remove_filter('pre_option_'.$opt, array($this, 'option_filter_'.$opt));
3046 }
3047
3048 if (!empty($updraftplus_addons_migrator->new_blogid)) restore_current_blog();
3049
3050 }
3051
3052 /**
3053 * WordPress options filter
3054 *
3055 * @param String $val - pre-filter value
3056 *
3057 * @return String - filtered value
3058 */
3059 public function option_filter_template($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
3060 global $updraftplus;
3061 return $updraftplus->option_filter_get('template');
3062 }
3063
3064 /**
3065 * WordPress options filter
3066 *
3067 * @param String $val - pre-filter value
3068 *
3069 * @return String - filtered value
3070 */
3071 public function option_filter_stylesheet($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
3072 global $updraftplus;
3073 return $updraftplus->option_filter_get('stylesheet');
3074 }
3075
3076 /**
3077 * WordPress options filter
3078 *
3079 * @param String $val - pre-filter value
3080 *
3081 * @return String - filtered value
3082 */
3083 public function option_filter_template_root($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
3084 global $updraftplus;
3085 return $updraftplus->option_filter_get('template_root');
3086 }
3087
3088 /**
3089 * WordPress options filter
3090 *
3091 * @param String $val - pre-filter value
3092 *
3093 * @return String - filtered value
3094 */
3095 public function option_filter_stylesheet_root($val) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
3096 global $updraftplus;
3097 return $updraftplus->option_filter_get('stylesheet_root');
3098 }
3099
3100 private function restored_table($table, $import_table_prefix, $old_table_prefix) {
3101
3102 $table_without_prefix = substr($table, strlen($import_table_prefix));
3103
3104 if (isset($this->restore_this_table[$old_table_prefix.$table_without_prefix]) && !$this->restore_this_table[$old_table_prefix.$table_without_prefix]) return;
3105
3106 global $wpdb, $updraftplus;
3107
3108 if ($table == $import_table_prefix.UpdraftPlus_Options::options_table()) {
3109 // This became necessary somewhere around WP 4.5 - otherwise deleting and re-saving options stopped working
3110 wp_cache_flush();
3111 $this->restore_configuration_bundle($table);
3112 }
3113
3114 if (preg_match('/^([\d+]_)?options$/', substr($table, strlen($import_table_prefix)), $matches)) {
3115 // The second prefix here used to have a '!$this->is_multisite' on it (i.e. 'options' table on non-multisite). However, the user_roles entry exists in the main options table on multisite too.
3116 if (($this->is_multisite && !empty($matches[1])) || $table == $import_table_prefix.'options') {
3117
3118 $mprefix = empty($matches[1]) ? '' : $matches[1];
3119
3120 $new_table_name = $import_table_prefix.$mprefix."options";
3121
3122 // WordPress has an option name predicated upon the table prefix. Yuk.
3123 if ($import_table_prefix != $old_table_prefix) {
3124 $updraftplus->log("Table prefix has changed: changing options table field(s) accordingly (".$mprefix."options)");
3125 $print_line = sprintf(__('Table prefix has changed: changing %s table field(s) accordingly:', 'updraftplus'), 'option').' ';
3126 if (false === $wpdb->query("UPDATE $new_table_name SET option_name='${import_table_prefix}".$mprefix."user_roles' WHERE option_name='${old_table_prefix}".$mprefix."user_roles' LIMIT 1")) {
3127 $print_line .= __('Error', 'updraftplus');
3128 $updraftplus->log("Error when changing options table fields: ".$wpdb->last_error);
3129 } else {
3130 $updraftplus->log("Options table fields changed OK");
3131 $print_line .= __('OK', 'updraftplus');
3132 }
3133 $updraftplus->log($print_line, 'notice-restore');
3134 }
3135
3136 // Now deal with the situation where the imported database sets a new over-ride upload_path that is absolute - which may not be wanted
3137 $new_upload_path = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM ${import_table_prefix}".$mprefix."options WHERE option_name = %s LIMIT 1", 'upload_path'));
3138 $new_upload_path = (is_object($new_upload_path)) ? $new_upload_path->option_value : '';
3139 // The danger situation is absolute and points somewhere that is now perhaps not accessible at all
3140
3141 if (!empty($new_upload_path) && $new_upload_path != $this->prior_upload_path && (strpos($new_upload_path, '/') === 0) || preg_match('#^[A-Za-z]:[/\\\]#', $new_upload_path)) {
3142
3143 // $this->old_siteurl != untrailingslashit(site_url()) is not a perfect proxy for "is a migration" (other possibilities exist), but since the upload_path option should not exist since WP 3.5 anyway, the chances of other possibilities are vanishingly small
3144 if (!file_exists($new_upload_path) || $this->old_siteurl != $this->our_siteurl) {
3145
3146 if (!file_exists($new_upload_path)) {
3147 $updraftplus->log_e("Uploads path (%s) does not exist - resetting (%s)", $new_upload_path, $this->prior_upload_path);
3148 } else {
3149 $updraftplus->log_e("Uploads path (%s) has changed during a migration - resetting (to: %s)", $new_upload_path, $this->prior_upload_path);
3150 }
3151 if (false === $wpdb->query($wpdb->prepare("UPDATE ${import_table_prefix}".$mprefix."options SET option_value='%s' WHERE option_name='upload_path' LIMIT 1", array($this->prior_upload_path)))) {
3152 $updraftplus->log(__('Error', 'updraftplus'), 'notice-restore');
3153 $updraftplus->log("Error when changing upload path: ".$wpdb->last_error);
3154 $updraftplus->log("Failed");
3155 }
3156 }
3157 }
3158
3159 // TODO:Do on all WPMU tables
3160 if ($table == $import_table_prefix.'options') {
3161 // Bad plugin that hard-codes path references - https://wordpress.org/plugins/custom-content-type-manager/
3162 $cctm_data = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $new_table_name WHERE option_name = %s LIMIT 1", 'cctm_data'));
3163 if (!empty($cctm_data->option_value)) {
3164 $cctm_data = maybe_unserialize($cctm_data->option_value);
3165 if (is_array($cctm_data) && !empty($cctm_data['cache']) && is_array($cctm_data['cache'])) {
3166 $cctm_data['cache'] = array();
3167 $updraftplus->log_e("Custom content type manager plugin data detected: clearing option cache");
3168 update_option('cctm_data', $cctm_data);
3169 }
3170 }
3171 // Another - http://www.elegantthemes.com/gallery/elegant-builder/
3172 $elegant_data = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $new_table_name WHERE option_name = %s LIMIT 1", 'et_images_temp_folder'));
3173 if (!empty($elegant_data->option_value)) {
3174 $dbase = basename($elegant_data->option_value);
3175 $wp_upload_dir = wp_upload_dir();
3176 $edir = $wp_upload_dir['basedir'];
3177 if (!is_dir($edir.'/'.$dbase)) @mkdir($edir.'/'.$dbase);
3178 $updraftplus->log_e("Elegant themes theme builder plugin data detected: resetting temporary folder");
3179 update_option('et_images_temp_folder', $edir.'/'.$dbase);
3180 }
3181 }
3182
3183 // The gantry menu plugin sometimes uses too-long transient names, causing the timeout option to be missing; and hence the transient becomes permanent.
3184 // WP 3.4 onwards has $wpdb->delete(). But we support 3.2 onwards.
3185 $wpdb->query("DELETE FROM $new_table_name WHERE option_name LIKE '_transient_gantry-menu%' OR option_name LIKE '_transient_timeout_gantry-menu%'");
3186
3187 // Jetpack: see: https://wordpress.org/support/topic/issues-with-dev-site
3188 if ($this->old_siteurl != $this->our_siteurl) {
3189 $wpdb->query("DELETE FROM $new_table_name WHERE option_name = 'jetpack_options'");
3190 }
3191
3192 }
3193
3194 } elseif ($import_table_prefix != $old_table_prefix && preg_match('/^([\d+]_)?usermeta$/', substr($table, strlen($import_table_prefix)), $matches)) {
3195
3196 // This table is not a per-site table, but per-install
3197
3198 $updraftplus->log("Table prefix has changed: changing usermeta table field(s) accordingly");
3199
3200 $print_line = sprintf(__('Table prefix has changed: changing %s table field(s) accordingly:', 'updraftplus'), 'usermeta').' ';
3201
3202 $errors_occurred = false;
3203
3204 if (false === strpos($old_table_prefix, '_')) {
3205 // Old, slow way: do it row-by-row
3206 // By Jul 2015, doing this on the updraftplus.com database took 20 minutes on a slow test machine
3207 $old_prefix_length = strlen($old_table_prefix);
3208
3209 $um_sql = "SELECT umeta_id, meta_key
3210 FROM ${import_table_prefix}usermeta
3211 WHERE meta_key
3212 LIKE '".str_replace('_', '\_', $old_table_prefix)."%'";
3213 $meta_keys = $wpdb->get_results($um_sql);
3214
3215 foreach ($meta_keys as $meta_key) {
3216 // Create new meta key
3217 $new_meta_key = $import_table_prefix . substr($meta_key->meta_key, $old_prefix_length);
3218
3219 $query = "UPDATE " . $import_table_prefix . "usermeta
3220 SET meta_key='".$new_meta_key."'
3221 WHERE umeta_id=".$meta_key->umeta_id;
3222
3223 if (false === $wpdb->query($query)) $errors_occurred = true;
3224 }
3225 } else {
3226 // New, fast way: do it in a single query
3227 $sql = "UPDATE ${import_table_prefix}usermeta SET meta_key = REPLACE(meta_key, '$old_table_prefix', '${import_table_prefix}') WHERE meta_key LIKE '".str_replace('_', '\_', $old_table_prefix)."%';";
3228 if (false === $wpdb->query($sql)) $errors_occurred = true;
3229 }
3230
3231 if ($errors_occurred) {
3232 $updraftplus->log("Error when changing usermeta table fields");
3233 $print_line .= __('Error', 'updraftplus');
3234 } else {
3235 $updraftplus->log("Usermeta table fields changed OK");
3236 $print_line .= __('OK', 'updraftplus');
3237 }
3238 $updraftplus->log($print_line, 'notice-restore');
3239
3240 }
3241
3242 do_action('updraftplus_restored_db_table', $table, $import_table_prefix);
3243
3244 // Re-generate permalinks. Do this last - i.e. make sure everything else is fixed up first.
3245 if ($table == $import_table_prefix.'options') $this->flush_rewrite_rules();
3246
3247 }
3248
3249 /**
3250 * Log permission failure message when restoring a backup
3251 *
3252 * @param string $path full path of file or folder
3253 * @param string $log_message_prefix action which is performed to path
3254 * @param string $directory_prefix_in_log_message Directory Prefix. It should be either "Parent" or "Destination"
3255 */
3256 private function restore_log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message = 'Parent') {
3257 global $updraftplus;
3258 $log_message = $updraftplus->log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message);
3259 if ($log_message) {
3260 $updraftplus->log($log_message, 'warning-restore');
3261 }
3262 }
3263
3264 /**
3265 * This function will loop through all the sites available and get their active plugins and ensure any missing plugins are removed from the active list to prevent crashes.
3266 *
3267 * @param string $import_table_prefix - the table prefix
3268 *
3269 * @return void
3270 */
3271 private function check_active_plugins($import_table_prefix) {
3272 global $wpdb;
3273
3274 if ($this->is_multisite) {
3275 // Get the site wide active plugins
3276 $plugins = $wpdb->get_row("SELECT meta_value FROM ${import_table_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins'");
3277 if (!empty($plugins->meta_value)) {
3278 $plugins = $this->deactivate_missing_plugins($plugins->meta_value);
3279 $wpdb->query($wpdb->prepare("UPDATE ${import_table_prefix}sitemeta SET meta_value=%s WHERE meta_key='active_sitewide_plugins'", $plugins));
3280 }
3281
3282 $offset = 0;
3283 $limit = 250;
3284
3285 while (true) {
3286 // Loop over and get each sites active plugins
3287 $blogs = $wpdb->get_results("SELECT blog_id FROM {$wpdb->blogs} LIMIT ${offset}, ${limit}", ARRAY_A);
3288
3289 if (empty($blogs)) break;
3290
3291 foreach ($blogs as $row) {
3292 if (!apply_filters('updraftplus_restore_this_site', true, $row['blog_id'], '', $this->restore_options)) continue;
3293 $plugins = $wpdb->get_row("SELECT option_value FROM ".$wpdb->get_blog_prefix($row['blog_id'])."options WHERE option_name = 'active_plugins'");
3294 if (empty($plugins->option_value)) continue;
3295 $plugins = $this->deactivate_missing_plugins($plugins->option_value);
3296 $wpdb->query($wpdb->prepare("UPDATE ".$wpdb->get_blog_prefix($row['blog_id'])."options SET option_value=%s WHERE option_name='active_plugins'", $plugins));
3297 }
3298
3299 $offset += $limit;
3300 }
3301
3302 } else {
3303 $plugins = $wpdb->get_row("SELECT option_value FROM ${import_table_prefix}options WHERE option_name = 'active_plugins'");
3304 if (empty($plugins->option_value)) return;
3305 $plugins = $this->deactivate_missing_plugins($plugins->option_value);
3306 $wpdb->query($wpdb->prepare("UPDATE ${import_table_prefix}options SET option_value=%s WHERE option_name='active_plugins'", $plugins));
3307 }
3308 }
3309
3310 /**
3311 * This function will check the list of active plugins and ensure they are still installed, if any are missing it will deactivate them to prevent the site from crashing.
3312 *
3313 * @param String $plugins - serialized active plugins
3314 *
3315 * @return String - filtered results
3316 */
3317 private function deactivate_missing_plugins($plugins) {
3318 global $updraftplus;
3319
3320 if (!function_exists('get_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
3321 $installed_plugins = array_keys(get_plugins());
3322 $plugins = maybe_unserialize($plugins);
3323
3324 foreach ($plugins as $key => $path) {
3325 // Single site and multisite have a different array structure, in single site the path is the array value, in multisite the path is the array key.
3326 if (!in_array($key, $installed_plugins) && !in_array($path, $installed_plugins)) {
3327 $log_path = $this->is_multisite ? $key : $path;
3328 $updraftplus->log_e('Plugin path %s not found: de-activating.', $log_path);
3329 unset($plugins[$key]);
3330 }
3331 }
3332
3333 $plugins = serialize($plugins);
3334
3335 return $plugins;
3336 }
3337
3338 /**
3339 * This function will return the prefix which will use as a dummy table prefix
3340 *
3341 * @param String $string - default prefix
3342 *
3343 * @return String - dummy prefix
3344 */
3345 public function updraftplus_restore_table_prefix_dummy($string) {
3346 global $wpdb;
3347 while (true) {
3348 $random_string = UpdraftPlus_Manipulation_Functions::generate_random_string(2). '_';
3349 if ($string != $random_string) {
3350 if (0 === $wpdb->query("SHOW TABLES LIKE '".$random_string."%'")) return $random_string;
3351 }
3352 }
3353 }
3354
3355 /**
3356 * This function will drop all tables from the database
3357 *
3358 * @param Array $tables - list of table names
3359 */
3360 private function drop_tables($tables) {
3361 foreach ($tables as $table) $this->sql_exec('DROP TABLE IF EXISTS '.UpdraftPlus_Manipulation_Functions::backquote($table), 1, '', false);
3362 }
3363 }
3364
3365 // The purpose of this is that, in a certain case, we want to forbid the "move" operation from doing a copy/delete if a direct move fails... because we have our own method for retrying (and don't want to risk copying a tonne of data if we can avoid it)
3366 if (!class_exists('WP_Filesystem_Direct')) {
3367 if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH.'wp-admin/includes/class-wp-filesystem-base.php');
3368 include_once(ABSPATH.'wp-admin/includes/class-wp-filesystem-direct.php');
3369 }
3370 class UpdraftPlus_WP_Filesystem_Direct extends WP_Filesystem_Direct {
3371
3372 public function move($source, $destination, $overwrite = false) {
3373 if (!$overwrite && $this->exists($destination))
3374 return false;
3375
3376 // try using rename first. if that fails (for example, source is read only) try copy
3377 if (@rename($source, $destination))
3378 return true;
3379
3380 return false;
3381 }
3382 }
3383
3384 /**
3385 * Get a protected property
3386 */
3387 class UpdraftPlus_WPDB extends wpdb {
3388
3389 /**
3390 * Get the database handle
3391 *
3392 * @return Mixed - the database handle
3393 */
3394 public function updraftplus_get_database_handle() {
3395 return $this->dbh;
3396 }
3397
3398 /**
3399 * Return whether the object is using mysqli or not.
3400 *
3401 * @return Boolean
3402 */
3403 public function updraftplus_use_mysqli() {
3404 return !empty($this->use_mysqli);
3405 }
3406 }
3407