array(0, 3, __('Preparing restore', 'enmigrate-pro')), 'database' => array(3, 50, __('Importing database', 'enmigrate-pro')), 'replace' => array(50, 62, __('Updating URLs and paths', 'enmigrate-pro')), 'files' => array(62, 95, __('Restoring files', 'enmigrate-pro')), 'swap' => array(95, 98, __('Activating the restored database', 'enmigrate-pro')), 'finish' => array(98, 100, __('Finishing', 'enmigrate-pro')), ); } /* ------------------------------------------------------------------ * Job lifecycle * ------------------------------------------------------------------ */ public static function jobs_dir() { $dir = WPMIGPRO_TEMP_DIR . '/jobs'; if (!is_dir($dir)) { WPMigPro_FS::protect_dir($dir); } return $dir; } /** * Create a restore job for an archive. Returns array(job_id, token) or WP_Error. */ /** * Does the archive contain encrypted entries? */ public static function is_encrypted($zip) { for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); if (!empty($stat['encryption_method'])) { return true; } } return false; } /** * Set the archive password (provided by an add-on) and check it opens a file. */ public static function unlock($zip) { $password = WPMigPro_Exporter::archive_password(); if ($password === '') { return false; } $zip->setPassword($password); for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); if (!empty($stat['encryption_method']) && $stat['size'] > 0) { return $zip->getFromIndex($i, 1) !== false; } } return true; } public static function create_job($archive_path, $backup_id = 0) { global $wpdb; if (!class_exists('ZipArchive')) { return new WP_Error('nozip', __('The PHP ZipArchive extension is not available on this server.', 'enmigrate-pro')); } if (!is_file($archive_path)) { return new WP_Error('nofile', __('Backup file not found', 'enmigrate-pro')); } $zip = new ZipArchive(); if ($zip->open($archive_path) !== true) { return new WP_Error('badzip', __('Cannot open the backup archive. The file may be corrupt or incomplete.', 'enmigrate-pro')); } $has_sql = $zip->locateName('database.sql') !== false; $has_files = $zip->numFiles > 2; $encrypted = self::is_encrypted($zip); if ($encrypted && !self::unlock($zip)) { $zip->close(); return new WP_Error('locked', __('This backup is encrypted. Enter its password in the backup encryption settings (Enwebs Suite Pro) and try again.', 'enmigrate-pro')); } $zip->close(); if (!$has_sql && !$has_files) { return new WP_Error('empty', __('The archive does not look like an EnMigrate Pro backup.', 'enmigrate-pro')); } $job_id = 'r' . gmdate('YmdHis') . strtolower(wp_generate_password(8, false, false)); $token = wp_generate_password(40, false, false); $work_dir = self::jobs_dir() . '/' . $job_id; wp_mkdir_p($work_dir); $job = array( 'id' => $job_id, 'token_hash' => hash('sha256', $token), 'archive' => $archive_path, 'backup_id' => intval($backup_id), 'work_dir' => $work_dir, 'created' => time(), 'step' => 'prepare', 'progress' => 0, 'cursor' => array(), 'log' => array(), 'done' => false, 'error' => '', 'db_errors' => array(), 'db_error_count' => 0, // Values from the site we restore INTO, captured now while its database is intact. 'target' => array( 'prefix' => $wpdb->prefix, 'home' => get_option('home'), 'siteurl' => get_option('siteurl'), 'abspath' => wp_normalize_path(ABSPATH), 'content_dir' => wp_normalize_path(WP_CONTENT_DIR), 'plugin' => plugin_basename(WPMIGPRO_PLUGIN_FILE), ), ); $instance = new self(); $instance->job = $job; $instance->job_file = self::jobs_dir() . '/' . $job_id . '.json'; /* translators: 1: file name, 2: file size */ $instance->log(sprintf(__('Restore started: %1$s (%2$s)', 'enmigrate-pro'), basename($archive_path), size_format(filesize($archive_path), 2))); $instance->save(); WPMigPro_Logger::log("Restore job $job_id created for $archive_path"); return array('job_id' => $job_id, 'token' => $token); } /** * Load a job, validating its token. Returns instance or WP_Error. */ public static function load($job_id, $token) { $job_id = preg_replace('/[^a-z0-9]/i', '', (string) $job_id); $file = self::jobs_dir() . '/' . $job_id . '.json'; if (!$job_id || !is_file($file)) { return new WP_Error('nojob', __('Restore job not found', 'enmigrate-pro')); } $job = json_decode((string) WPMigPro_FS::read($file), true); if (!is_array($job) || empty($job['token_hash']) || !hash_equals($job['token_hash'], hash('sha256', (string) $token))) { return new WP_Error('auth', __('Unauthorized', 'enmigrate-pro')); } $instance = new self(); $instance->job = $job; $instance->job_file = $file; return $instance; } private function save() { $this->last_save = microtime(true); WPMigPro_FS::put($this->job_file . '.tmp', wp_json_encode($this->job)); WPMigPro_FS::move($this->job_file . '.tmp', $this->job_file); } private function maybe_save() { if (microtime(true) - $this->last_save > 3) { $this->save(); } } private function time_left() { return (microtime(true) - $this->started) < self::TIME_BUDGET; } private function log($message, $level = 'info') { $this->job['log'][] = '[' . wp_date('H:i:s') . '] ' . $message; if ($level !== 'info') { WPMigPro_Logger::log('[restore ' . $this->job['id'] . '] ' . $message, $level); } } private function set_progress($fraction, $details = '') { $steps = self::steps(); list($start, $end) = $steps[$this->job['step']]; $fraction = max(0, min(1, $fraction)); $this->job['progress'] = (int) floor($start + ($end - $start) * $fraction); if ($details !== '') { $this->job['details'] = $details; } } private function next_step($step) { $steps = self::steps(); $this->job['step'] = $step; $this->job['cursor'] = array(); $this->job['details'] = ''; $this->set_progress(0); $this->log('== ' . $steps[$step][2] . ' =='); } /** * Run one slice of work and return the state for the browser. */ public function run($log_from = 0) { $this->lock_handle = WPMigPro_FS::open($this->job_file . '.lock', 'c'); if (!$this->lock_handle || !flock($this->lock_handle, LOCK_EX | LOCK_NB)) { return array('busy' => true) + $this->response($log_from); } WPMigPro_Exporter::raise_limits(); $this->started = microtime(true); $this->last_save = $this->started; if (!$this->job['done'] && empty($this->job['error'])) { try { $this->ensure_guard(); do { $step_before = $this->job['step']; $method = 'step_' . $this->job['step']; $this->$method(); // Keep going within the same request when a step finishes early. } while (!$this->job['done'] && $this->time_left() && $step_before !== $this->job['step']); } catch (Exception $e) { $this->fail($e->getMessage()); } catch (Error $e) { $this->fail('PHP: ' . $e->getMessage() . ' (' . basename($e->getFile()) . ':' . $e->getLine() . ')'); } } $this->save(); flock($this->lock_handle, LOCK_UN); WPMigPro_FS::close($this->lock_handle); return $this->response($log_from); } private function fail($message) { $this->job['error'] = $message; $this->log('ERROR: ' . $message, 'error'); $this->log(__('The restore stopped. If the error happened before the database activation step, your site still uses its previous database.', 'enmigrate-pro')); $this->remove_guard(); } private function response($log_from) { $log_from = max(0, intval($log_from)); $steps = self::steps(); return array( 'progress' => $this->job['progress'], 'step' => $steps[$this->job['step']][2], 'details' => isset($this->job['details']) ? $this->job['details'] : '', 'log' => array_slice($this->job['log'], $log_from), 'log_total' => count($this->job['log']), 'done' => $this->job['done'], 'error' => $this->job['error'], 'login_url' => wp_login_url(), ); } /* ------------------------------------------------------------------ * Guard: while the restore runs, our step requests load no other * plugin and no theme, so half-copied plugin files can't fatal them, * and WP-Cron doesn't run against a half-restored site. * ------------------------------------------------------------------ */ private function guard_path() { $dir = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins'; return $dir . '/' . self::GUARD_FILE; } private function ensure_guard() { $path = $this->guard_path(); if (is_file($path)) { return; } wp_mkdir_p(dirname($path)); $plugin = "'" . addcslashes($this->job['target']['plugin'], "'\\") . "'"; $code = "guard_path()); } /* ------------------------------------------------------------------ * Step: prepare * ------------------------------------------------------------------ */ private function step_prepare() { $zip = new ZipArchive(); if ($zip->open($this->job['archive']) !== true) { throw new Exception(esc_html__('Cannot open the backup archive.', 'enmigrate-pro')); } if (self::is_encrypted($zip)) { self::unlock($zip); } $meta = array(); $raw = $zip->getFromName('metadata.json'); if ($raw) { $meta = json_decode($raw, true); $meta = is_array($meta) ? $meta : array(); } if (!empty($meta['backup_date'])) { $this->log(sprintf( /* translators: 1: source site URL, 2: date, 3: WordPress version */ __('Backup of %1$s created on %2$s (WordPress %3$s)', 'enmigrate-pro'), isset($meta['home_url']) ? $meta['home_url'] : $meta['site_url'], $meta['backup_date'], isset($meta['wordpress_version']) ? $meta['wordpress_version'] : '?' )); } $sql_file = $this->job['work_dir'] . '/database.sql'; $has_sql = $zip->locateName('database.sql') !== false; if ($has_sql) { $this->log(__('Extracting database.sql...', 'enmigrate-pro')); if (!$zip->extractTo($this->job['work_dir'], 'database.sql')) { throw new Exception(esc_html__('Could not extract database.sql:', 'enmigrate-pro') . ' ' . esc_html($zip->getStatusString())); } /* translators: %s: file size */ $this->log(sprintf(__('database.sql extracted (%s)', 'enmigrate-pro'), size_format(filesize($sql_file), 2))); } else { $this->log(__('The backup has no database; only files will be restored.', 'enmigrate-pro'), 'warning'); } $zip->close(); // Source site info $source = array( 'prefix' => isset($meta['table_prefix']) ? $meta['table_prefix'] : '', 'home' => isset($meta['home_url']) ? $meta['home_url'] : (isset($meta['site_url']) ? $meta['site_url'] : ''), 'siteurl' => isset($meta['site_url']) ? $meta['site_url'] : '', 'abspath' => isset($meta['abspath']) ? $meta['abspath'] : '', 'content_dir' => isset($meta['content_dir']) ? $meta['content_dir'] : '', ); if ($has_sql) { $detected = $this->scan_sql_header($sql_file); if (!$source['prefix']) { $source['prefix'] = $detected['prefix']; } if (!$source['home']) { $source['home'] = $detected['url']; } if (!$source['prefix'] || !WPMigPro_Database::is_valid_table_name($source['prefix'])) { throw new Exception(esc_html__('Could not detect the table prefix of the backup (no *options table found).', 'enmigrate-pro')); } } if (!$source['siteurl']) { $source['siteurl'] = $source['home']; } $this->job['source'] = $source; $this->job['has_sql'] = $has_sql; $target = $this->job['target']; $this->log(sprintf( /* translators: 1: source URL, 2: source table prefix, 3: destination URL, 4: destination table prefix */ __('Source: %1$s (prefix %2$s) → Destination: %3$s (prefix %4$s)', 'enmigrate-pro'), $source['home'] ? $source['home'] : '?', $source['prefix'] ? $source['prefix'] : '?', $target['home'], $target['prefix'] )); // Leftovers from a previous interrupted restore $this->drop_tables_with_prefix(self::TMP_PREFIX); $this->drop_tables_with_prefix(self::OLD_PREFIX); $this->next_step($has_sql ? 'database' : 'files'); } /** * Find the source URL and table prefix in the dump (for backups made by older versions). */ private function scan_sql_header($sql_file) { $result = array('url' => '', 'prefix' => ''); $fh = WPMigPro_FS::open($sql_file, 'r'); $candidates = array(); $posts = array(); $lines = 0; while ($fh && ($line = fgets($fh)) !== false) { $lines++; if ($lines < 20) { if (preg_match('/^-- URL: (\S+)/', $line, $m)) { $result['url'] = trim($m[1]); } if (preg_match('/^-- Prefix: (\S+)/', $line, $m)) { $result['prefix'] = trim($m[1]); break; } } if (strncasecmp($line, 'CREATE TABLE', 12) === 0 && preg_match('/^CREATE TABLE\s+(?:IF NOT EXISTS\s+)?`?([A-Za-z0-9_$]+?)(options|posts)`?\s*\(/i', $line, $m)) { if (strtolower($m[2]) === 'options') { $candidates[] = $m[1]; } else { $posts[] = $m[1]; } } } WPMigPro_FS::close($fh); if (!$result['prefix'] && $candidates) { $both = array_values(array_intersect($candidates, $posts)); $list = $both ? $both : $candidates; usort($list, function ($a, $b) { return strlen($a) - strlen($b); }); $result['prefix'] = $list[0]; } return $result; } /* ------------------------------------------------------------------ * Step: database (into temporary tables) * ------------------------------------------------------------------ */ private function db_session() { WPMigPro_Database::query("SET NAMES 'utf8mb4'"); WPMigPro_Database::query('SET FOREIGN_KEY_CHECKS=0'); WPMigPro_Database::query('SET UNIQUE_CHECKS=0'); WPMigPro_Database::query("SET SESSION sql_mode='NO_AUTO_VALUE_ON_ZERO'"); WPMigPro_Database::query('SET autocommit=0'); } private function step_database() { $sql_file = $this->job['work_dir'] . '/database.sql'; $size = max(1, filesize($sql_file)); $c = &$this->job['cursor']; $c += array('offset' => 0, 'statements' => 0, 'table' => '', 'skipped' => array()); $this->db_session(); $fh = WPMigPro_FS::open($sql_file, 'r'); if (!$fh) { throw new Exception(esc_html__('Cannot read database.sql', 'enmigrate-pro')); } fseek($fh, $c['offset']); $buffer = ''; $at_end = true; while (($line = fgets($fh)) !== false) { if ($buffer === '') { $trim = ltrim($line); if ($trim === '' || strncmp($trim, '--', 2) === 0 || strncmp($trim, '/*', 2) === 0) { $c['offset'] = ftell($fh); continue; } } $buffer .= $line; // A statement ends with ";" at end of line. INSERT values never contain raw // newlines (they are escaped), so this can't split a statement in the middle. if (substr(rtrim($buffer), -1) !== ';') { continue; } $this->execute_statement(rtrim($buffer)); $buffer = ''; $c['statements']++; $c['offset'] = ftell($fh); if (!$this->time_left()) { $at_end = false; break; } } if ($at_end && trim($buffer) !== '') { // Last statement without a trailing ";" $this->execute_statement(rtrim($buffer)); $c['statements']++; $c['offset'] = $size; } WPMigPro_FS::close($fh); WPMigPro_Database::query('COMMIT'); $this->set_progress($c['offset'] / $size, sprintf( /* translators: 1: processed size, 2: total size, 3: number of SQL statements */ __('%1$s of %2$s (%3$d statements)', 'enmigrate-pro'), size_format($c['offset'], 1), size_format($size, 1), $c['statements'] )); if ($at_end) { /* translators: 1: number of SQL statements, 2: number of errors */ $this->log(sprintf(__('Database imported: %1$d statements, %2$d errors', 'enmigrate-pro'), $c['statements'], $this->job['db_error_count'])); if ($this->job['db_error_count'] > 0) { $this->log(__('First errors:', 'enmigrate-pro') . ' ' . implode(' | ', array_slice($this->job['db_errors'], 0, 5)), 'warning'); } $this->next_step('replace'); } } private function execute_statement($sql) { $source_prefix = $this->job['source']['prefix']; $c = &$this->job['cursor']; if (preg_match('/^SET\s/i', $sql) || preg_match('/^(LOCK|UNLOCK)\s+TABLES/i', $sql)) { return; } if (!preg_match('/^(INSERT\s+(?:IGNORE\s+)?INTO|REPLACE\s+INTO|CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE)\s+`?([A-Za-z0-9_$]+)`?/i', $sql, $m)) { return; // anything else (views, triggers...) is not part of a normal WP dump } $verb = strtoupper(preg_replace('/\s+/', ' ', $m[1])); $table = $m[2]; if (strpos($table, $source_prefix) !== 0) { if (empty($c['skipped'][$table])) { $c['skipped'][$table] = 1; /* translators: 1: table name, 2: table prefix */ $this->log(sprintf(__('Table %1$s skipped (does not use prefix %2$s)', 'enmigrate-pro'), $table, $source_prefix)); } return; } $base = substr($table, strlen($source_prefix)); if (in_array($base, WPMigPro_Database::$own_tables, true)) { return; // keep this site's own backup list and logs } $tmp_table = self::TMP_PREFIX . $base; $sql = substr_replace($sql, $m[1] . ' `' . $tmp_table . '`', 0, strlen($m[0])); if (strpos($verb, 'DROP') === 0) { $sql = "DROP TABLE IF EXISTS `$tmp_table`;"; } elseif (strpos($verb, 'CREATE') === 0) { // Old backups have no DROP before CREATE WPMigPro_Database::query("DROP TABLE IF EXISTS `$tmp_table`"); $sql = preg_replace('/^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS/i', 'CREATE TABLE', $sql); $sql = str_replace('REFERENCES `' . $source_prefix, 'REFERENCES `' . self::TMP_PREFIX, $sql); if ($c['table'] !== $base) { $c['table'] = $base; /* translators: %s: table name */ $this->log(sprintf(__('Table %s', 'enmigrate-pro'), $table)); } } if (WPMigPro_Database::query($sql)) { return; } $error = WPMigPro_Database::last_error(); // MySQL 8 collations don't exist on MariaDB / MySQL 5.x if (stripos($error, 'collation') !== false || stripos($error, 'character set') !== false) { $fixed = preg_replace('/utf8mb4_0900_[a-z_]+/i', 'utf8mb4_unicode_520_ci', $sql); $fixed = preg_replace('/utf8mb4_uca1400_[a-z_]+/i', 'utf8mb4_unicode_520_ci', $fixed); if ($fixed !== $sql && WPMigPro_Database::query($fixed)) { return; } $fixed = str_replace('utf8mb4_unicode_520_ci', 'utf8mb4_unicode_ci', $fixed); if (WPMigPro_Database::query($fixed)) { return; } $error = WPMigPro_Database::last_error(); } $this->job['db_error_count']++; if (count($this->job['db_errors']) < 20) { $this->job['db_errors'][] = "$table: $error"; WPMigPro_Logger::log("Restore SQL error on $table: $error", 'warning'); } } /* ------------------------------------------------------------------ * Step: replace (URLs and paths, serialization-safe) * ------------------------------------------------------------------ */ private function replacement_pairs() { $s = $this->job['source']; $t = $this->job['target']; $pairs = array(); $add_url = function ($from, $to) use (&$pairs) { $from = untrailingslashit($from); $to = untrailingslashit($to); if (!$from || !$to || $from === $to) { return; } $pairs[$from] = $to; $pairs[str_replace('/', '\\/', $from)] = str_replace('/', '\\/', $to); // Scheme-less (//host) and the other scheme $from_nos = preg_replace('#^https?:#i', '', $from); $to_nos = preg_replace('#^https?:#i', '', $to); if ($from_nos !== $to_nos) { $pairs[$from_nos] = $to_nos; $pairs[str_replace('/', '\\/', $from_nos)] = str_replace('/', '\\/', $to_nos); $pairs[rawurlencode($from)] = rawurlencode($to); } }; $add_url($s['home'], $t['home']); $add_url($s['siteurl'], $t['siteurl']); foreach (array('content_dir', 'abspath') as $key) { $from = untrailingslashit($s[$key]); $to = untrailingslashit($t[$key]); if ($from && $to && $from !== $to) { $pairs[$from] = $to; $pairs[str_replace('/', '\\/', $from)] = str_replace('/', '\\/', $to); } } unset($pairs['']); return $pairs; } private function step_replace() { if (!isset($this->job['pairs'])) { $this->job['pairs'] = $this->replacement_pairs(); if (empty($this->job['pairs'])) { $this->log(__('Same address and paths: no URL replacement needed.', 'enmigrate-pro')); $this->next_step('files'); return; } /* translators: 1: old URL, 2: new URL */ $this->log(sprintf(__('Replacing %1$s → %2$s', 'enmigrate-pro'), untrailingslashit($this->job['source']['home']), untrailingslashit($this->job['target']['home']))); $this->job['replace_tables'] = WPMigPro_Database::tables_with_prefix(self::TMP_PREFIX); $this->job['replaced_rows'] = 0; } global $wpdb; $pairs = $this->job['pairs']; $tables = $this->job['replace_tables']; $c = &$this->job['cursor']; $c += array('table' => 0, 'last' => null, 'offset' => 0); $this->db_session(); while ($c['table'] < count($tables)) { $table = $tables[$c['table']]; list($pk, $columns) = $this->table_text_columns($table); if (empty($columns)) { $c['table']++; $c['last'] = null; $c['offset'] = 0; continue; } if ($pk) { $select = array_unique(array_merge(array($pk), $columns)); $cols_sql = '`' . implode('`, `', $select) . '`'; if ($c['last'] === null) { $sql = $wpdb->prepare("SELECT $cols_sql FROM %i ORDER BY %i LIMIT 500", $table, $pk); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list from SHOW COLUMNS. } else { $sql = $wpdb->prepare("SELECT $cols_sql FROM %i WHERE %i > %s ORDER BY %i LIMIT 500", $table, $pk, $c['last'], $pk); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list from SHOW COLUMNS. } } else { $sql = $wpdb->prepare('SELECT * FROM %i LIMIT %d, 500', $table, $c['offset']); } $rows = WPMigPro_Database::rows($sql); if (empty($rows) && WPMigPro_Database::last_error()) { /* translators: 1: table name, 2: database error */ $this->log(sprintf(__('Could not read %1$s: %2$s', 'enmigrate-pro'), $table, WPMigPro_Database::last_error()), 'warning'); } foreach ($rows as $row) { $set = array(); foreach ($columns as $col) { $value = $row[$col]; if ($value === null || $value === '' || !$this->contains_any($value, $pairs)) { continue; } $new = $this->replace_value($value, $pairs); if ($new !== $value) { $set[] = "`$col` = '" . WPMigPro_Database::escape($new) . "'"; } } if ($set) { if ($pk) { $cond = "`$pk` = '" . WPMigPro_Database::escape($row[$pk]) . "'"; } else { $parts = array(); foreach ($row as $k => $v) { $parts[] = $v === null ? "`$k` IS NULL" : "`$k` = '" . WPMigPro_Database::escape($v) . "'"; } $cond = implode(' AND ', $parts) . ' LIMIT 1'; } WPMigPro_Database::query("UPDATE `$table` SET " . implode(', ', $set) . " WHERE $cond"); $this->job['replaced_rows']++; } if ($pk) { $c['last'] = $row[$pk]; } } $count = count($rows); $c['offset'] += $count; if ($count < 500) { $c['table']++; $c['last'] = null; $c['offset'] = 0; } $this->set_progress($c['table'] / max(1, count($tables)), sprintf( /* translators: 1: table number, 2: total tables, 3: table name, 4: rows updated */ __('Table %1$d of %2$d (%3$s) - %4$d rows updated', 'enmigrate-pro'), min($c['table'] + 1, count($tables)), count($tables), $table, $this->job['replaced_rows'] )); if (!$this->time_left()) { WPMigPro_Database::query('COMMIT'); return; } } WPMigPro_Database::query('COMMIT'); /* translators: %d: number of rows */ $this->log(sprintf(__('URLs and paths updated in %d rows', 'enmigrate-pro'), $this->job['replaced_rows'])); $this->next_step('files'); } /** * Returns array(single-column primary key or '', text columns). */ private function table_text_columns($table) { global $wpdb; $pk = array(); $cols = array(); foreach (WPMigPro_Database::rows($wpdb->prepare('SHOW COLUMNS FROM %i', $table)) as $col) { if (!WPMigPro_Database::is_valid_table_name($col['Field'])) { continue; } if ($col['Key'] === 'PRI') { $pk[] = $col['Field']; } if (preg_match('/char|text|json/i', $col['Type']) && $col['Field'] !== 'user_pass') { $cols[] = $col['Field']; } } return array(count($pk) === 1 ? $pk[0] : '', $cols); } private function contains_any($value, $pairs) { foreach (array_keys($pairs) as $from) { if (strpos($value, (string) $from) !== false) { return true; } } return false; } /** * Replace inside a value, recursing into serialized data so string lengths stay valid. */ private function replace_value($value, $pairs) { if (is_string($value)) { if (is_serialized($value, false)) { // The data is the site's own database being restored by an administrator. $data = @unserialize($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize if ($data !== false || $value === 'b:0;') { if (is_object($data) && $data instanceof __PHP_Incomplete_Class) { return $value; } return serialize($this->replace_recursive($data, $pairs)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize } } return strtr($value, $pairs); } return $value; } private function replace_recursive($data, $pairs) { if (is_string($data)) { return $this->replace_value($data, $pairs); } if (is_array($data)) { foreach ($data as $key => $item) { $data[$key] = $this->replace_recursive($item, $pairs); } return $data; } if (is_object($data) && !($data instanceof __PHP_Incomplete_Class)) { foreach (get_object_vars($data) as $key => $item) { $data->$key = $this->replace_recursive($item, $pairs); } } return $data; } /* ------------------------------------------------------------------ * Step: files (straight from the archive into wp-content) * ------------------------------------------------------------------ */ private function step_files() { $zip = new ZipArchive(); if ($zip->open($this->job['archive']) !== true) { throw new Exception(esc_html__('Cannot open the backup archive.', 'enmigrate-pro')); } if (self::is_encrypted($zip)) { self::unlock($zip); } $c = &$this->job['cursor']; $c += array('index' => 0, 'written' => 0, 'skipped' => 0, 'errors' => 0, 'top' => '', 'cleaned' => array()); $total = $zip->numFiles; $prefix = 'files/wp-content/'; $plen = strlen($prefix); $content_dir = WP_CONTENT_DIR; $own_plugin_dir = dirname($this->job['target']['plugin']); $excluded_top = WPMigPro_Exporter::excluded_content_dirs(); // Drop-ins tied to the old server's environment (object cache, page cache, custom db driver) $skip_dropins = array('object-cache.php', 'advanced-cache.php', 'db.php', 'db-error.php'); while ($c['index'] < $total) { $i = $c['index']++; $name = $zip->getNameIndex($i); if ($name === false || strncmp($name, $prefix, $plen) !== 0) { continue; } $relative = substr($name, $plen); if ($relative === '' || strpos('/' . $relative . '/', '/../') !== false || strpos($relative, "\0") !== false) { continue; } $parts = explode('/', $relative); $top = $parts[0]; if (in_array($top, $excluded_top, true) || (count($parts) === 1 && in_array($top, $skip_dropins, true)) || ($top === 'plugins' && isset($parts[1]) && $parts[1] === $own_plugin_dir) || ($top === 'mu-plugins' && isset($parts[1]) && $parts[1] === self::GUARD_FILE)) { $c['skipped']++; continue; } if ($top !== $c['top']) { $c['top'] = $top; /* translators: %s: folder name inside wp-content */ $this->log(sprintf(__('Restoring wp-content/%s', 'enmigrate-pro'), $top)); } // Replace whole plugin/theme folders instead of merging, so files of a different // version left on this site can't mix with the restored ones. if (in_array($top, array('plugins', 'themes'), true) && !empty($parts[1]) && (count($parts) > 2 || substr($name, -1) === '/')) { $key = $top . '/' . $parts[1]; if (empty($c['cleaned'][$key])) { $c['cleaned'][$key] = 1; WPMigPro_FS::delete_tree($content_dir . '/' . $key); } } $dest = $content_dir . '/' . $relative; if (substr($name, -1) === '/') { if (!is_dir($dest)) { wp_mkdir_p($dest); } continue; } if (!is_dir(dirname($dest))) { wp_mkdir_p(dirname($dest)); } $in = $zip->getStream($name); $out = $in ? WPMigPro_FS::open($dest, 'wb') : false; if ($in && $out) { WPMigPro_FS::copy_stream($in, $out); WPMigPro_FS::close($out); WPMigPro_FS::close($in); $c['written']++; } else { WPMigPro_FS::close($in); $c['errors']++; if ($c['errors'] <= 20) { /* translators: %s: file path */ $this->log(sprintf(__('Could not write %s', 'enmigrate-pro'), $relative), 'warning'); } } if (!$this->time_left()) { break; } $this->maybe_save(); } $zip->close(); $this->set_progress($c['index'] / max(1, $total), sprintf( /* translators: 1: entries processed, 2: total entries, 3: files written */ __('%1$d of %2$d entries, %3$d files written', 'enmigrate-pro'), $c['index'], $total, $c['written'] )); if ($c['index'] >= $total) { /* translators: 1: files written, 2: files skipped, 3: errors */ $this->log(sprintf(__('Files restored: %1$d written, %2$d skipped, %3$d errors', 'enmigrate-pro'), $c['written'], $c['skipped'], $c['errors'])); $this->next_step($this->job['has_sql'] ? 'swap' : 'finish'); } } /* ------------------------------------------------------------------ * Step: swap * ------------------------------------------------------------------ */ private function step_swap() { global $wpdb; $tmp = self::TMP_PREFIX; $src = $this->job['source']['prefix']; $dst = $this->job['target']['prefix']; $options = $tmp . 'options'; $usermeta = $tmp . 'usermeta'; $this->db_session(); WPMigPro_Database::query('SET autocommit=1'); $tmp_tables = WPMigPro_Database::tables_with_prefix($tmp); if (!in_array($options, $tmp_tables, true)) { throw new Exception(esc_html__('The options table of the backup was not imported; the database is not replaced to avoid breaking the site.', 'enmigrate-pro')); } // 1. Keep this site's address foreach (array('siteurl', 'home') as $name) { WPMigPro_Database::query($wpdb->prepare('UPDATE %i SET option_value = %s WHERE option_name = %s', $options, $this->job['target'][$name], $name)); } // 2. Prefix-dependent keys (roles and user capabilities) if ($src !== $dst) { /* translators: 1: old table prefix, 2: new table prefix */ $this->log(sprintf(__('Adapting table prefix: %1$s → %2$s', 'enmigrate-pro'), $src, $dst)); WPMigPro_Database::query($wpdb->prepare('UPDATE %i SET option_name = %s WHERE option_name = %s', $options, $dst . 'user_roles', $src . 'user_roles')); if (in_array($usermeta, $tmp_tables, true)) { WPMigPro_Database::query($wpdb->prepare( 'UPDATE %i SET meta_key = CONCAT(%s, SUBSTRING(meta_key, %d)) WHERE meta_key LIKE %s', $usermeta, $dst, strlen($src) + 1, $wpdb->esc_like($src) . '%' )); } } // 3. Active plugins: only those whose files exist, plus this plugin $raw = $wpdb->get_var($wpdb->prepare('SELECT option_value FROM %i WHERE option_name = %s', $options, 'active_plugins')); $active = $raw ? maybe_unserialize($raw) : array(); if (!is_array($active)) { $active = array(); } $kept = array(); foreach ($active as $plugin) { if (is_string($plugin) && validate_file($plugin) === 0 && is_file(WP_PLUGIN_DIR . '/' . $plugin)) { $kept[] = $plugin; } else { /* translators: %s: plugin file */ $this->log(sprintf(__('Plugin not found, left inactive: %s', 'enmigrate-pro'), is_string($plugin) ? $plugin : '?'), 'warning'); } } if (!in_array($this->job['target']['plugin'], $kept, true)) { $kept[] = $this->job['target']['plugin']; } WPMigPro_Database::query($wpdb->prepare('UPDATE %i SET option_value = %s WHERE option_name = %s', $options, maybe_serialize(array_values($kept)), 'active_plugins')); /* translators: %d: number of plugins */ $this->log(sprintf(__('%d active plugins', 'enmigrate-pro'), count($kept))); // 4. Theme check $theme = $wpdb->get_var($wpdb->prepare('SELECT option_value FROM %i WHERE option_name = %s', $options, 'stylesheet')); if ($theme) { if (is_dir(get_theme_root() . '/' . $theme)) { /* translators: %s: theme folder */ $this->log(sprintf(__('Active theme: %s', 'enmigrate-pro'), $theme)); } else { /* translators: %s: theme folder */ $this->log(sprintf(__('The backup theme "%s" is not in wp-content/themes', 'enmigrate-pro'), $theme), 'warning'); } } // 5. Caches that hold old URLs / rules WPMigPro_Database::query($wpdb->prepare( 'DELETE FROM %i WHERE option_name = %s OR option_name LIKE %s OR option_name LIKE %s', $options, 'rewrite_rules', $wpdb->esc_like('_transient_') . '%', $wpdb->esc_like('_site_transient_') . '%' )); // 6. Atomic swap: current tables → wpmigold_*, temp tables → live names $own = array_map(function ($t) use ($dst) { return $dst . $t; }, WPMigPro_Database::$own_tables); $renames = array(); foreach (WPMigPro_Database::tables_with_prefix($dst) as $table) { if (in_array($table, $own, true) || strpos($table, $tmp) === 0 || strpos($table, self::OLD_PREFIX) === 0) { continue; } $renames[] = "`$table` TO `" . self::OLD_PREFIX . substr($table, strlen($dst)) . '`'; } $old_count = count($renames); foreach ($tmp_tables as $table) { $renames[] = "`$table` TO `" . $dst . substr($table, strlen($tmp)) . '`'; } /* translators: 1: number of current tables, 2: number of backup tables */ $this->log(sprintf(__('Replacing %1$d site tables with %2$d backup tables...', 'enmigrate-pro'), $old_count, count($tmp_tables))); if (!WPMigPro_Database::query('RENAME TABLE ' . implode(', ', $renames))) { throw new Exception(esc_html__('Could not rename the tables:', 'enmigrate-pro') . ' ' . esc_html(WPMigPro_Database::last_error())); } $this->drop_tables_with_prefix(self::OLD_PREFIX); wp_cache_flush(); $this->log(__('Backup database activated', 'enmigrate-pro')); $this->next_step('finish'); } private function drop_tables_with_prefix($prefix) { $tables = WPMigPro_Database::tables_with_prefix($prefix); if ($tables) { WPMigPro_Database::query('SET FOREIGN_KEY_CHECKS=0'); WPMigPro_Database::query('DROP TABLE IF EXISTS `' . implode('`, `', $tables) . '`'); } } /* ------------------------------------------------------------------ * Step: finish * ------------------------------------------------------------------ */ private function step_finish() { global $wpdb, $wp_db_version; wp_cache_flush(); if ($this->job['has_sql']) { $db_version = (int) $wpdb->get_var($wpdb->prepare('SELECT option_value FROM %i WHERE option_name = %s', $wpdb->options, 'db_version')); if ($db_version && $db_version < (int) $wp_db_version) { /* translators: 1: old database version, 2: new database version */ $this->log(sprintf(__('Upgrading the database schema (%1$d → %2$d)...', 'enmigrate-pro'), $db_version, $wp_db_version)); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; wp_upgrade(); } } $this->remove_guard(); // Temporary working files (extracted SQL) WPMigPro_FS::delete($this->job['work_dir'] . '/database.sql'); WPMigPro_FS::rmdir($this->job['work_dir']); $this->job['progress'] = 100; $this->job['done'] = true; $this->job['details'] = ''; $this->log(__('Restore completed! Log in with the username and password of the restored site.', 'enmigrate-pro')); WPMigPro_Logger::log('Restore job ' . $this->job['id'] . ' completed'); } /** * Remove job files older than a day. */ public static function cleanup_old_jobs() { foreach ((array) glob(self::jobs_dir() . '/*') as $path) { if (in_array(basename($path), array('index.php', '.htaccess', 'web.config'), true)) { continue; } if (filemtime($path) < time() - DAY_IN_SECONDS) { if (is_dir($path)) { WPMigPro_FS::delete_tree($path); } else { WPMigPro_FS::delete($path); } } } } }