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.

Assessment

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

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.

Description

auto-dispatch needs-maintainer-review review:approve status:available

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.
Dominant language
PHP
Stars
214
Forks
86
Avg merge
7h 6m
Merged PRs (30d)
62

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.

More from Ultimate-Multisite/ultimate-multisite

All issues in Ultimate-Multisite/ultimate-multisite

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.