| 1 |
<?php |
| 2 |
|
| 3 |
namespace WPSynchro\Masterdata; |
| 4 |
|
| 5 |
use WPSynchro\Database\DatabaseSync; |
| 6 |
use WPSynchro\Migration\MigrationController; |
| 7 |
use WPSynchro\Transport\Destination; |
| 8 |
use WPSynchro\Utilities\CommonFunctions; |
| 9 |
use WPSynchro\Utilities\Licensing\Licensing; |
| 10 |
use WPSynchro\Utilities\SyncTimerList; |
| 11 |
use WPSynchro\Migration\Migration; |
| 12 |
use WPSynchro\Migration\Job; |
| 13 |
use WPSynchro\Logger\FileLogger; |
| 14 |
|
| 15 |
/** |
| 16 |
* Class for handling the masterdata of the sync |
| 17 |
* |
| 18 |
*/ |
| 19 |
class MasterdataSync |
| 20 |
{ |
| 21 |
// Base data |
| 22 |
public int $starttime = 0; |
| 23 |
public ?Migration $migration = null; |
| 24 |
public ?Job $job = null; |
| 25 |
public ?\wpdb $remote_wpdb = null; |
| 26 |
// Dependencies |
| 27 |
public ?FileLogger $logger = null; |
| 28 |
public ?SyncTimerList $timer = null; |
| 29 |
|
| 30 |
/** |
| 31 |
* Constructor |
| 32 |
*/ |
| 33 |
public function __construct() |
| 34 |
{ |
| 35 |
$this->logger = MigrationController::getInstance()->getLogger(); |
| 36 |
$this->timer = SyncTimerList::getInstance(); |
| 37 |
} |
| 38 |
|
| 39 |
/** |
| 40 |
* Handle masterdata step |
| 41 |
*/ |
| 42 |
public function runMasterdataStep(Migration &$migration, Job &$job) |
| 43 |
{ |
| 44 |
$masterdata_timer = $this->timer->startTimer("masterdata", "overall", "timer"); |
| 45 |
|
| 46 |
$this->migration = &$migration; |
| 47 |
$this->job = &$job; |
| 48 |
|
| 49 |
$this->logger->log("INFO", "Getting masterdata from source and target with remaining time:" . $this->timer->getRemainingSyncTime()); |
| 50 |
|
| 51 |
// Figure out what data is needed |
| 52 |
$data_to_retrieve = []; |
| 53 |
$data_to_retrieve[] = "dbdetails"; |
| 54 |
$data_to_retrieve[] = "filedetails"; |
| 55 |
|
| 56 |
// Retrieve data |
| 57 |
$metadata_results = []; |
| 58 |
$metadata_results['from'] = $this->retrieveMasterdata(new Destination(Destination::SOURCE), $data_to_retrieve); |
| 59 |
$metadata_results['to'] = $this->retrieveMasterdata(new Destination(Destination::TARGET), $data_to_retrieve); |
| 60 |
|
| 61 |
foreach ($metadata_results as $prefix => $masterdata_content) { |
| 62 |
if (in_array("dbdetails", $data_to_retrieve)) { |
| 63 |
if (!isset($masterdata_content->dbdetails) || $masterdata_content->dbdetails == null) { |
| 64 |
$errormsg = sprintf(__("Did not retrieve correct database masterdata from target '%s' - See log file", "wpsynchro"), $prefix); |
| 65 |
$this->job->errors[] = $errormsg; |
| 66 |
$this->logger->log("CRITICAL", $errormsg, $metadata_results); |
| 67 |
return; |
| 68 |
} |
| 69 |
} |
| 70 |
|
| 71 |
// Process and map the data to Job object |
| 72 |
$this->handleMasterdataMapping($prefix, $masterdata_content); |
| 73 |
} |
| 74 |
|
| 75 |
// Handle system search replaces depending on setup |
| 76 |
$this->findSystemSearchReplaces(); |
| 77 |
|
| 78 |
// Initize some configuration in the job object |
| 79 |
$this->inititializeConfigurations(); |
| 80 |
|
| 81 |
// Check that the migration is compatible with the masterdata gotten |
| 82 |
$this->checkMigrationCompatibility(); |
| 83 |
|
| 84 |
if (count($this->job->errors) == 0) { |
| 85 |
// Check that plugin versions are identical on both sides, otherwise raise error |
| 86 |
if ($this->job->from_plugin_version != $this->job->to_plugin_version) { |
| 87 |
$this->job->errors[] = sprintf(__("WP Synchro plugin versions do not match on both sides. One runs version %s and other runs %s. Make sure they use same version to prevent problems caused by different versions of plugin.", "wpsynchro"), $this->job->from_plugin_version, $this->job->to_plugin_version); |
| 88 |
} |
| 89 |
|
| 90 |
// Check if WP versions are different, then raise a warning, as it might not be a problem |
| 91 |
if ($this->job->to_wp_version != $this->job->from_wp_version) { |
| 92 |
$this->job->warnings[] = sprintf(__("WordPress versions are different on the source and target. Source WP version is: %s and target WP version is: %s. This is just a warning, as in most cases it will not cause problems.", "wpsynchro"), $this->job->from_wp_version, $this->job->to_wp_version); |
| 93 |
} |
| 94 |
|
| 95 |
// Check that prefix are the same or issue warning |
| 96 |
if ($this->migration->sync_database && !$this->migration->db_table_prefix_change && $this->job->from_wpdb_prefix != $this->job->to_wpdb_prefix) { |
| 97 |
$prefix_warning = sprintf(__("Database table prefixes are different on the source and target site. Source uses '%s' and target uses '%s'. Table prefix migration is not enabled in the migration configuration. This is just a warning, as the migration can complete, but the tables will not be used by the target site. Recommended action is to turn on the table prefix migration in the migration configuration.", "wpsynchro"), $this->job->from_wpdb_prefix, $this->job->to_wpdb_prefix); |
| 98 |
$this->job->warnings[] = $prefix_warning; |
| 99 |
$this->logger->log("WARNING", $prefix_warning); |
| 100 |
} |
| 101 |
|
| 102 |
// If doing file sync, make sure the MU plugin is enabled on the target to prevent all kinds of PHP errors for half-transferred code |
| 103 |
if ($this->migration->sync_files && $this->job->to_mu_plugin_enabled == false) { |
| 104 |
$this->job->errors[] = __("When doing file migrations, it is required to have the WP Synchro MU plugin active on the target site. It can be enabled on the target site in menu WP Synchro > Setup.", "wpsynchro"); |
| 105 |
} |
| 106 |
|
| 107 |
// Check licensing |
| 108 |
if (CommonFunctions::isPremiumVersion() && count($this->job->errors) === 0) { |
| 109 |
$licensing = new Licensing(); |
| 110 |
$licens_sync_result = $licensing->verifyLicenseForMigration($this->job->from_client_home_url, $this->job->to_client_home_url); |
| 111 |
|
| 112 |
if ($licens_sync_result->state === false) { |
| 113 |
$this->job->errors = array_merge($this->job->errors, $licens_sync_result->errors); |
| 114 |
} |
| 115 |
} |
| 116 |
} |
| 117 |
|
| 118 |
$this->logger->log("INFO", "Completed masterdata on: " . $this->timer->endTimer($masterdata_timer) . " seconds"); |
| 119 |
|
| 120 |
if (count($this->job->errors) === 0) { |
| 121 |
$this->job->masterdata_completed = true; |
| 122 |
} |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* Check that the masterdata is compatible with the migration requested |
| 127 |
*/ |
| 128 |
public function checkMigrationCompatibility() |
| 129 |
{ |
| 130 |
// Database migration checks |
| 131 |
if ($this->migration->sync_database) { |
| 132 |
// Make sure no tables using "json" column type is migrated to a pre 5.7 version. |
| 133 |
foreach ($this->job->from_dbmasterdata as $table) { |
| 134 |
if (version_compare($this->job->to_sql_version, '5.7', '<')) { |
| 135 |
if ($table->column_types->isColumnTypeUsed('json')) { |
| 136 |
$this->job->errors[] = sprintf( |
| 137 |
__('The database table "%s" is not supported on the target database version (%s) and therefore it can not be migrated. To fix this error, either exclude the table from migration or upgrade the target database to a newer version.', 'wpsynchro'), |
| 138 |
$table->name, |
| 139 |
$this->job->to_sql_version |
| 140 |
); |
| 141 |
} |
| 142 |
} |
| 143 |
} |
| 144 |
} |
| 145 |
} |
| 146 |
|
| 147 |
/** |
| 148 |
* Masterdata mapping |
| 149 |
*/ |
| 150 |
public function handleMasterdataMapping($prefix, $masterdata_content) |
| 151 |
{ |
| 152 |
/** |
| 153 |
* Base mappings |
| 154 |
*/ |
| 155 |
if (isset($masterdata_content->base)) { |
| 156 |
$mappings = [ |
| 157 |
"_client_home_url" => "client_home_url", |
| 158 |
"_wpdb_prefix" => "wpdb_prefix", |
| 159 |
"_wp_options_table" => "wp_options_table", |
| 160 |
"_wp_users_table" => "wp_users_table", |
| 161 |
"_wp_usermeta_table" => "wp_usermeta_table", |
| 162 |
"_max_allowed_packet_size" => "max_allowed_packet_size", |
| 163 |
"_max_post_size" => "max_post_size", |
| 164 |
"_memory_limit" => "memory_limit", |
| 165 |
"_sql_version" => "sql_version", |
| 166 |
"_plugin_version" => "plugin_version", |
| 167 |
"_wp_version" => "wp_version", |
| 168 |
"_mu_plugin_enabled" => "mu_plugin_enabled", |
| 169 |
]; |
| 170 |
|
| 171 |
foreach ($mappings as $job_key => $masterdata_key) { |
| 172 |
if (!isset($masterdata_content->base->$masterdata_key)) { |
| 173 |
continue; |
| 174 |
} |
| 175 |
$tmp_var = $prefix . $job_key; |
| 176 |
$this->job->$tmp_var = $masterdata_content->base->$masterdata_key; |
| 177 |
} |
| 178 |
} |
| 179 |
|
| 180 |
/** |
| 181 |
* Multisite mapping |
| 182 |
*/ |
| 183 |
if (isset($masterdata_content->multisite)) { |
| 184 |
$key = $prefix . '_is_multisite'; |
| 185 |
$this->job->$key = $masterdata_content->multisite->is_multisite; |
| 186 |
$key = $prefix . '_is_multisite_main_site'; |
| 187 |
$this->job->$key = $masterdata_content->multisite->is_main_site; |
| 188 |
$key = $prefix . '_defined_uploads_location'; |
| 189 |
$this->job->$key = $masterdata_content->multisite->defined_uploads_location; |
| 190 |
|
| 191 |
if ($masterdata_content->multisite->is_multisite) { |
| 192 |
$key = $prefix . '_main_blog_id'; |
| 193 |
$this->job->$key = $masterdata_content->multisite->main_blog_id; |
| 194 |
$key = $prefix . '_current_blog_id'; |
| 195 |
$this->job->$key = $masterdata_content->multisite->current_blog_id; |
| 196 |
$key = $prefix . '_blogs'; |
| 197 |
$this->job->$key = $masterdata_content->multisite->blogs; |
| 198 |
$key = $prefix . '_default_super_admin_id'; |
| 199 |
$this->job->$key = $masterdata_content->multisite->default_super_admin_id; |
| 200 |
$key = $prefix . '_default_super_admin_username'; |
| 201 |
$this->job->$key = $masterdata_content->multisite->default_super_admin_username; |
| 202 |
} |
| 203 |
} |
| 204 |
|
| 205 |
|
| 206 |
/** |
| 207 |
* DB details mapping |
| 208 |
*/ |
| 209 |
if (isset($masterdata_content->dbdetails)) { |
| 210 |
$tmp_var = $prefix . '_dbmasterdata'; |
| 211 |
$this->job->$tmp_var = $masterdata_content->dbdetails; |
| 212 |
} |
| 213 |
|
| 214 |
/** |
| 215 |
* Files data mapping |
| 216 |
*/ |
| 217 |
if (isset($masterdata_content->files)) { |
| 218 |
$mappings = [ |
| 219 |
"_files_above_webroot_dir" => "files_above_webroot_dir", |
| 220 |
"_files_home_dir" => "files_home_dir", |
| 221 |
"_files_wp_content_dir" => "files_wp_content_dir", |
| 222 |
"_files_wp_dir" => "files_wp_dir", |
| 223 |
"_files_uploads_dir" => "files_uploads_dir", |
| 224 |
"_files_plugins_dir" => "files_plugins_dir", |
| 225 |
"_files_themes_dir" => "files_themes_dir", |
| 226 |
"_files_plugin_list" => "files_plugin_list", |
| 227 |
"_files_theme_list" => "files_theme_list", |
| 228 |
]; |
| 229 |
|
| 230 |
foreach ($mappings as $job_key => $masterdata_key) { |
| 231 |
if (!isset($masterdata_content->files->$masterdata_key)) { |
| 232 |
continue; |
| 233 |
} |
| 234 |
$tmp_var = $prefix . $job_key; |
| 235 |
$this->job->$tmp_var = $masterdata_content->files->$masterdata_key; |
| 236 |
} |
| 237 |
} |
| 238 |
|
| 239 |
/** |
| 240 |
* Debug data mapping |
| 241 |
*/ |
| 242 |
if (isset($masterdata_content->debug)) { |
| 243 |
$tmp_var = $prefix . '_debug'; |
| 244 |
$this->job->$tmp_var = $masterdata_content->debug; |
| 245 |
} |
| 246 |
} |
| 247 |
|
| 248 |
/** |
| 249 |
* Retrieve masterdata |
| 250 |
*/ |
| 251 |
public function retrieveMasterdata(Destination $destination, $slugs_to_retrieve = []) |
| 252 |
{ |
| 253 |
// Get masterdata retrival object |
| 254 |
$retrieval = new MasterdataRetrieval($destination); |
| 255 |
$retrieval->setDataToRetrieve($slugs_to_retrieve); |
| 256 |
|
| 257 |
$this->logger->log("DEBUG", "Calling masterdata service on: " . $destination->getFullURL() . " with intent to user as '" . $destination->getDestination() . "'"); |
| 258 |
$result = $retrieval->getMasterdata(); |
| 259 |
|
| 260 |
// Check for errors |
| 261 |
if ($result) { |
| 262 |
return $retrieval->data; |
| 263 |
} else { |
| 264 |
$errormsg = sprintf(__("Could not retrieve masterdata from target '%s', which means we can not continue the migration.", "wpsynchro"), $destination->getFullURL()); |
| 265 |
$this->job->errors[] = $errormsg; |
| 266 |
$this->logger->log("CRITICAL", $errormsg); |
| 267 |
return []; |
| 268 |
} |
| 269 |
} |
| 270 |
|
| 271 |
/** |
| 272 |
* Initialize some configurations based on the data retrieved from masterdata |
| 273 |
*/ |
| 274 |
public function inititializeConfigurations() |
| 275 |
{ |
| 276 |
// Add system search/replaces |
| 277 |
$this->job->db_search_replaces = array_merge($this->migration->searchreplaces, $this->job->db_system_search_replaces); |
| 278 |
|
| 279 |
// Regenerate the search/replaces if set in the migration |
| 280 |
if ($this->migration->searchreplaces_regenerate) { |
| 281 |
$common_functions = new CommonFunctions(); |
| 282 |
$this->job->db_search_replaces = $common_functions->generateDefaultSearchReplace( |
| 283 |
$this->job->from_client_home_url, |
| 284 |
$this->job->to_client_home_url, |
| 285 |
$this->job->from_files_home_dir, |
| 286 |
$this->job->to_files_home_dir |
| 287 |
); |
| 288 |
} |
| 289 |
|
| 290 |
// Ignore all search/replaces - Only used for testing purposes |
| 291 |
if ($this->migration->ignore_all_search_replaces) { |
| 292 |
$this->job->db_search_replaces = []; |
| 293 |
} |
| 294 |
|
| 295 |
// Clear duplicates from search/replaces |
| 296 |
$this->job->db_search_replaces = $this->removeDuplicatesFromSearchReplaces($this->job->db_search_replaces); |
| 297 |
|
| 298 |
// Remove tables from dbdata, if not all tables should be synced |
| 299 |
if ($this->migration->include_all_database_tables === false) { |
| 300 |
$onlyinclude = $this->migration->only_include_database_table_names; |
| 301 |
$newdbdata = []; |
| 302 |
foreach ($this->job->from_dbmasterdata as $table) { |
| 303 |
if (in_array($table->name, $onlyinclude)) { |
| 304 |
$newdbdata[] = $table; |
| 305 |
} |
| 306 |
} |
| 307 |
$this->job->from_dbmasterdata = $newdbdata; |
| 308 |
} |
| 309 |
|
| 310 |
// Check if any of the tables are actually views, to be handled in a special way |
| 311 |
$views = []; |
| 312 |
$this->job->from_dbmasterdata = array_filter($this->job->from_dbmasterdata, function ($table_obj) use (&$views) { |
| 313 |
if ($table_obj->is_view) { |
| 314 |
$views[] = $table_obj; |
| 315 |
return false; |
| 316 |
} |
| 317 |
return true; |
| 318 |
}); |
| 319 |
$this->job->db_views_to_be_synced = $views; |
| 320 |
|
| 321 |
// Check if tables have 0 rows, so we mark them complete |
| 322 |
foreach ($this->job->from_dbmasterdata as &$table) { |
| 323 |
if ($table->rows == 0) { |
| 324 |
$table->is_completed = true; |
| 325 |
} |
| 326 |
} |
| 327 |
|
| 328 |
// Remove any temporary tables |
| 329 |
$this->job->from_dbmasterdata = array_filter($this->job->from_dbmasterdata, function ($table_obj) { |
| 330 |
if (strpos($table_obj->name, DatabaseSync::TMP_TABLE_PREFIX) === 0) { |
| 331 |
return false; |
| 332 |
} |
| 333 |
return true; |
| 334 |
}); |
| 335 |
|
| 336 |
// Set max packet size we can send to SQL server at a time |
| 337 |
$this->job->masterdata_max_sql_packet_bytes = min($this->job->from_max_allowed_packet_size, $this->job->to_max_allowed_packet_size); |
| 338 |
// Set max size request data that can be POST'ed to a remote service, based on the lowest on both clients |
| 339 |
$this->job->masterdata_max_tcp_request_bytes = min($this->job->from_max_post_size, $this->job->to_max_post_size) * 0.9; |
| 340 |
// Set the memory limit, based on the lowest on both clients |
| 341 |
$this->job->masterdata_max_memory_limit_bytes = round((min($this->job->from_memory_limit, $this->job->to_memory_limit) - memory_get_usage()) * 0.7); |
| 342 |
} |
| 343 |
|
| 344 |
/** |
| 345 |
* Clear duplicates for search/replaces |
| 346 |
*/ |
| 347 |
public function removeDuplicatesFromSearchReplaces(array $search_replaces) |
| 348 |
{ |
| 349 |
$new_search_replaces = []; |
| 350 |
$search_replace_hashes = []; |
| 351 |
foreach ($search_replaces as $search_replace) { |
| 352 |
$hash = md5($search_replace->to . '-' . $search_replace->from); |
| 353 |
if ($search_replace->to != $search_replace->from && !isset($search_replace_hashes[$hash])) { |
| 354 |
$new_search_replaces[] = $search_replace; |
| 355 |
} |
| 356 |
$search_replace_hashes[$hash] = true; |
| 357 |
} |
| 358 |
return $new_search_replaces; |
| 359 |
} |
| 360 |
|
| 361 |
/** |
| 362 |
* Find needed system search/replaces |
| 363 |
*/ |
| 364 |
public function findSystemSearchReplaces() |
| 365 |
{ |
| 366 |
|
| 367 |
// Check for multisite and if we need any search/replaces |
| 368 |
if ($this->job->from_is_multisite || $this->job->to_is_multisite) { |
| 369 |
// Exclude WP Synchro on these locations also |
| 370 |
$this->job->files_population_source_excludes[] = $this->job->from_files_uploads_dir . "/wpsynchro"; |
| 371 |
$this->job->files_population_target_excludes[] = $this->job->to_files_uploads_dir . "/wpsynchro"; |
| 372 |
|
| 373 |
// If both are multisites - So either main<>main or subsite<>subsite |
| 374 |
if ($this->job->from_is_multisite && $this->job->to_is_multisite) { |
| 375 |
} |
| 376 |
|
| 377 |
// Multisite subsite to singlesize or other way around |
| 378 |
if (($this->job->from_is_multisite && !$this->job->to_is_multisite) || (!$this->job->from_is_multisite && $this->job->to_is_multisite)) { |
| 379 |
// If the source is a multisite and not the main site, exclude all other sites uploads folder |
| 380 |
if ($this->job->from_is_multisite && !$this->job->from_is_multisite_main_site) { |
| 381 |
$uploads_sites_folder_on_source = dirname(dirname(trailingslashit($this->job->from_files_uploads_dir))) . "/sites/"; |
| 382 |
foreach ($this->job->from_blogs as $blog) { |
| 383 |
if ($blog->id != $this->job->from_current_blog_id) { |
| 384 |
$this->job->files_population_source_excludes[] = $uploads_sites_folder_on_source . $blog->id; |
| 385 |
} |
| 386 |
} |
| 387 |
} |
| 388 |
} |
| 389 |
|
| 390 |
// If migrating from multisite subsite to single site, make sure that UPLOADS constant is defined for single site. |
| 391 |
if ($this->job->from_is_multisite && !$this->job->to_is_multisite) { |
| 392 |
$relative_uploads_dir = trim(str_replace($this->job->to_files_home_dir, "", $this->job->to_files_uploads_dir), "/"); |
| 393 |
$define_uploads_dir = "DEFINE('UPLOADS','" . $relative_uploads_dir . "');"; |
| 394 |
if ($this->job->to_defined_uploads_location != $relative_uploads_dir) { |
| 395 |
$this->job->finalize_success_messages_frontend[] = sprintf(__("If you are moving uploads directory (media), you must add the following line to your wp-config.php on the site %s : <code>%s</code> - Make sure to add this line before loading WordPress in the bottom of wp-config.php", "wpsynchro"), untrailingslashit($this->job->to_client_home_url), $define_uploads_dir); |
| 396 |
} |
| 397 |
$this->job->finalize_success_messages_frontend[] = __("All users from multisite is moved - Make sure to check and remove unwanted users on single site.", "wpsynchro"); |
| 398 |
} |
| 399 |
// If migrating from single site to multisite subsite |
| 400 |
if (!$this->job->from_is_multisite && $this->job->to_is_multisite) { |
| 401 |
$this->job->finalize_success_messages_frontend[] = __("Users from source single site was merged with the users on the multisite. Make sure to check that the users are as expected.", "wpsynchro"); |
| 402 |
} |
| 403 |
} |
| 404 |
} |
| 405 |
} |
| 406 |
|