Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 96 additions & 6 deletions inc/helpers/class-site-duplicator.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,20 @@ protected static function process_duplication($args) {
* customer's real choice in the wu_template_id site meta key.
* Prefer that over the explicit param when available.
*
* Intentionally kept in a separate variable: copy_data() and
* copy_files() have already run with $args->from_site_id. Mutating
* that property would cause copy_users() and downstream callers to
* reference a different source than the one whose data was copied,
* creating an inconsistent clone. Use $template_site_id only for the
* post-copy backfill, integrity check, and action payload.
*
* @since 2.3.1
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
*/
$meta_template = (int) get_site_meta($args->to_site_id, 'wu_template_id', true);
if ($meta_template > 0 && $meta_template !== (int) $args->from_site_id) {
$args->from_site_id = $meta_template;
$template_site_id = (int) $args->from_site_id;
$meta_template = (int) get_site_meta($args->to_site_id, 'wu_template_id', true);
if (0 < $meta_template && $meta_template !== (int) $args->from_site_id) {
$template_site_id = $meta_template;
}

/*
Expand All @@ -281,7 +289,19 @@ protected static function process_duplication($args) {
* @since 2.3.1
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
*/
self::backfill_postmeta($args->from_site_id, $args->to_site_id);
self::backfill_postmeta($template_site_id, $args->to_site_id);

/*
* Rewrite source URLs to target URLs in backfilled postmeta rows.
*
* backfill_postmeta() inserts rows after MUCD_Data::copy_data() has
* already run its source→target URL replacement pass, so those rows
* contain raw template URLs. Apply the same replacement here.
*
* @since 2.3.2
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/834
*/
self::rewrite_backfilled_postmeta_urls($template_site_id, $args->to_site_id);

/*
* Verify Kit integrity after backfill.
Expand All @@ -293,7 +313,7 @@ protected static function process_duplication($args) {
* @since 2.3.1
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/820
*/
self::verify_kit_integrity($args->from_site_id, $args->to_site_id);
self::verify_kit_integrity($template_site_id, $args->to_site_id);

if ($args->keep_users) {
\MUCD_Duplicate::copy_users($args->from_site_id, $args->to_site_id);
Expand All @@ -316,7 +336,7 @@ protected static function process_duplication($args) {
do_action(
'wu_duplicate_site',
[
'from_site_id' => $args->from_site_id,
'from_site_id' => $template_site_id,
'site_id' => $args->to_site_id,
]
);
Expand Down Expand Up @@ -384,6 +404,76 @@ protected static function backfill_postmeta($from_site_id, $to_site_id) {
self::backfill_kit_settings($from_site_id, $to_site_id);
}

/**
* Rewrite source-site URLs to target-site URLs in backfilled postmeta rows.
*
* backfill_postmeta() inserts rows after MUCD_Data::copy_data() has already
* run its source→target URL replacement pass (db_update_data()), so those
* rows contain raw template-site URLs. This method applies the same URL
* substitution to the target's postmeta table, correcting any template
* references left by the backfill (e.g. _menu_item_url custom links,
* _elementor_* JSON containing the template domain).
*
* Safe to run after MUCD has already rewritten the copied rows: those rows
* no longer contain the source URL, so REPLACE() is a no-op for them.
*
* @since 2.3.2
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/834
*
* @param int $from_site_id Source (template) blog ID.
* @param int $to_site_id Target (cloned) blog ID.
*/
protected static function rewrite_backfilled_postmeta_urls($from_site_id, $to_site_id) {

global $wpdb;

$from_site_id = (int) $from_site_id;
$to_site_id = (int) $to_site_id;

if ( ! $from_site_id || ! $to_site_id || $from_site_id === $to_site_id) {
return;
}

$from_blog_url = get_blog_option($from_site_id, 'siteurl');
$to_blog_url = get_blog_option($to_site_id, 'siteurl');

$from_clean = wu_replace_scheme((string) $from_blog_url);
$to_clean = wu_replace_scheme((string) $to_blog_url);

if ($from_clean === $to_clean) {
return;
}

$to_prefix = $wpdb->get_blog_prefix($to_site_id);

/*
* Mirror MUCD's two-pass approach: plain URL replacement and a
* JSON-escaped variant (forward slashes encoded as \/).
*/
$replacements = [
$from_clean => $to_clean,
str_replace('/', '\\/', $from_clean) => str_replace('/', '\\/', $to_clean),
];

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
foreach ($replacements as $from => $to) {

if ($from === $to) {
continue;
}

$wpdb->query(
$wpdb->prepare(
"UPDATE {$to_prefix}postmeta SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_value LIKE %s",
$from,
$to,
'%' . $wpdb->esc_like($from) . '%'
)
);
}
// phpcs:enable
}

/**
* Backfill nav_menu_item postmeta from template to cloned site.
*
Expand Down
Loading