Ultimate-Multisite / Ultimate-Multisite/ultimate-multisite

Site duplication can silently abort mid-copy on a wp_options key collision, leaving new sites unthemed/unconfigured (race with domain verification)

Open
#1,863 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

auto-dispatch needs-maintainer-review review:approve status:available
Dominant language
PHP
Stars
214
Forks
86
Avg merge
5h 29m
Merged PRs (30d)
60

Description

Summary

MUCD_Data::db_copy_tables() in inc/duplication/data.php drops and recreates each destination table, then runs a plain INSERT ... SELECT to copy rows from the source (template) site's table:

// Populate database with data from source table
self::do_sql_query('INSERT `' . $table_name . '` SELECT * FROM `' . $table . '`');

This statement has no IGNORE / ON DUPLICATE KEY UPDATE. If the freshly-created destination table receives even a single row with a colliding unique key (e.g. option_name in {prefix}_options) before this INSERT runs, the entire INSERT is aborted and none of the source site's rows for that table are copied.

How the collision happens

We observed this in production (WordPress Multisite + network-active security/utility plugins). Immediately after a new site is registered, Ultimate Multisite's own domain-verification loopback check fires an HTTP request against the brand-new site's front-end, in the same window as (or interleaved with) copy_data(). That front-end hit bootstraps WordPress for the new blog ID, and network-active plugins/WP core populate a handful of default wp_options rows (e.g. uninstall_plugins, or a security plugin's own *_version option) via their normal init hooks. If that happens between the destination table's CREATE and the bulk INSERT, the INSERT collides on that one row and aborts entirely — not just for that row.

Confirmed via site-duplication-errors.log on two separate site creations (different colliding option each time):

[...] [ERROR] Got error "Table '...bdsc_10_loginizer_logs' doesn't exist" while running: INSERT `bdsc_10_loginizer_logs` SELECT * FROM `bdsc_3_loginizer_logs`
[...] [ERROR] Got error "Duplicate entry 'loginizer_version' for key 'option_name'" while running: INSERT `bdsc_10_options` SELECT * FROM `bdsc_3_options`
...
[...] [ERROR] Got error "Duplicate entry 'uninstall_plugins' for key 'option_name'" while running: INSERT `bdsc_11_options` SELECT * FROM `bdsc_3_options`

site-duplication.log shows the duplication attempt was reported as "successful" both times despite the options table copy having been aborted — the new site ends up as a bare default-theme WordPress install instead of a clone of the template site (wrong template/stylesheet, missing content, missing plugin configuration), with no error surfaced to the admin beyond the (misleading) "Domain verification failed via loopback ... DNS propagation not finished" notice, which is actually just a downstream symptom of the broken site returning an error page instead of the expected response.

This isn't deterministic — most site creations succeed, since the race window is small. But on any network with several network-active plugins that write options/tables on blog init, it will eventually hit.

Suggested fix

Make the bulk copy tolerant of a partial pre-existing row instead of all-or-nothing, e.g.:

self::do_sql_query('INSERT IGNORE `' . $table_name . '` SELECT * FROM `' . $table . '`');

This keeps the (harmless, auto-regenerated) destination-side default row when there's a collision, while still copying every other row from the template site — turning a full clone failure into, at worst, one skipped option. We applied this one-line change locally and it resolved the issue for us across several subsequent site creations.

A more complete fix might also look at not firing the domain-verification loopback request until after copy_data() completes, to shrink/eliminate the race window itself, but the INSERT IGNORE change alone is enough to prevent data loss.

Environment

  • Ultimate Multisite 2.16.1
  • WordPress Multisite, subdomain-based
  • MySQL/MariaDB via standard shared hosting (HostGator)
  • Reproduced with Loginizer + Loginizer Pro network-active (first occurrence) and again after deactivating Loginizer Pro, colliding instead on WordPress core's uninstall_plugins option (second occurrence) — confirming the root cause is the missing IGNORE, not any specific third-party plugin.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in inc/duplication/data.php at MUCD_Data::db_copy_tables() and its do_sql_query call that copies each source table. Reproduce or inspect the logged wp_options collision, then verify that a pre-existing destination row no longer aborts the rest of the table copy and that duplication completes with the template configuration intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
mariadb, mysql, php
Domain
backend, database
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.