fix(template-switch): 13 bugs in override_site() causing customer site corruption — VALIDATED 18/18 PASS in production-mirror#1082
Conversation
…ite corruption PROBLEM ======= Customers switching templates from the panel UI experienced multiple catastrophic issues, all traced to Site_Duplicator::override_site(): 1. AJAX timeout 30s on copy_files for sites >50MB media 2. blogname (customer brand) overwritten with template's name 3. blogmeta corruption when AJAX dies mid-flight 4. Elementor Kit/CSS broken (Lorem ipsum visible in production) 5. Object cache returns stale wu_type post-restore 6. No recovery path for affected customers 7. Elementor breakpoints null fatal in copy_data context 8. No 'reset current template' option (UX confusing) 9. Thumbnail not refreshed when template changes 10. Kit Elementor settings/data NOT copied (wrong colors persist) 11. create_admin email/login fail after 1st switch 12. override_site returns FALSE on consecutive switches 13. wu_template_id reverts to old value via cached restore ROOT CAUSES =========== Two distinct issues: A) MUCD_Data::copy_data() leaves $wpdb in template-blog context without restore_current_blog(). On next override call, email_exists() looks up the WRONG users table, returning false for legitimate users. create_admin() then tries wpmu_create_user() with the network domain as login, which fails. B) MUCD copies postmeta rows for the Elementor Kit (post 3) but Elementor's serialize/cache layer retains stale settings. Customer site renders with previous template's colors even though postmeta is updated. REAL-WORLD CASE =============== abconline.kursopro.com (KursoPro production) — customer attempted template switch on 2026-05-03. Symptoms: - blogname changed to 'Plantilla Belleza' - Kit colors stuck on previous template (azul instead of rosa) - 2nd switch attempt failed with 'Could not create admin user' - Subsite became unreachable (503 + suspended page) Required manual SQL recovery + custom mu-plugin to fix. FIX === 1. Reset blog context (restore_current_blog loop) BEFORE override 2. Pre-extend timeouts (300s + 512M) for large media copies 3. Snapshot identity (blogname/admin_email/etc) BEFORE process_duplication 4. Restore identity IMMEDIATELY after copy_data overwrites it 5. Force-copy Elementor Kit settings/data from source template (new public method force_copy_elementor_kit) 6. Regenerate Kit CSS via \Elementor\Core\Files\CSS\Post 7. Cache cleanup AFTER override (clean_blog_cache + clean_user_cache) VALIDATION ========== Tested in production-mirror staging environment with 18 consecutive switches across 6 different templates (3 runs x 6 cycles): RUN 1: PASS=6 FAIL=0 RUN 2: PASS=6 FAIL=0 RUN 3: PASS=6 FAIL=0 TOTAL: 18/18 (100%) Each cycle verified: blogname preservation, admin_email preservation, wu_template_id correctness, customer relationship preservation, and Kit colors matching template. DEPLOYED ======== Patch deployed as KP mu-plugin in KursoPro production since 2026-05-03 (kp-um-template-switch-fix.php v3.4.4). Active on production handling ~300+ subsites with zero regressions reported. This PR ports the patch into UM core so the mu-plugin can be removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR enhances site duplication by adding environment preparation, identity field capture/restoration, and explicit Elementor Kit copying. A new public method ChangesSite Duplication with Identity & Elementor Kit Preservation
Sequence DiagramsequenceDiagram
participant Caller
participant override_site as override_site()
participant WP as WordPress<br/>(Blog/Options)
participant Duplication as process_duplication()
participant ElementorKit as force_copy_elementor_kit()
participant Elementor as Elementor<br/>(CSS Regen)
Caller->>override_site: Call override_site(from_id, to_id)
rect rgba(70, 130, 180, 0.5)
Note over override_site,WP: Preparation Phase
override_site->>WP: Reset blog context
override_site->>WP: Flush cache
override_site->>WP: Increase PHP limits, define WP_IMPORTING
override_site->>WP: Snapshot target identity fields
override_site->>WP: Clean customer user cache
end
rect rgba(100, 150, 200, 0.5)
Note over override_site,Duplication: Duplication Phase
override_site->>Duplication: Call process_duplication()
Duplication->>WP: Copy site content/settings
Duplication-->>override_site: Return status
end
rect rgba(144, 202, 249, 0.5)
Note over override_site,ElementorKit: Restoration & Sync Phase
override_site->>WP: Restore identity snapshot options
override_site->>ElementorKit: Call force_copy_elementor_kit(template, target)
ElementorKit->>WP: Switch to template blog
ElementorKit->>WP: Read active kit & _elementor_* meta
ElementorKit->>WP: Switch to target blog
ElementorKit->>WP: Overwrite kit _elementor_page_settings/_elementor_data
ElementorKit->>WP: Copy additional kit meta
ElementorKit->>Elementor: Regenerate Kit CSS
ElementorKit->>WP: Restore original blog context
ElementorKit-->>override_site: Return success/failure
end
rect rgba(176, 190, 197, 0.5)
Note over override_site,WP: Cleanup Phase
override_site->>WP: Restore blog context
override_site->>WP: Flush cache
override_site->>WP: Clear blog/user caches
override_site-->>Caller: Return
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
|
DISPATCH_CLAIM nonce=d21d83f921ac859e6a3d0c1d3df68626 runner=superdav42 ts=2026-05-04T06:17:28Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=698aea46bb6568a7cd49c491d5f1add2 runner=superdav42 ts=2026-05-04T06:18:06Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
superdav42
left a comment
There was a problem hiding this comment.
Auto-approved by pulse runner @superdav42 — author @kenedytorcatt confirmed collaborator, pre-merge gates passed.
superdav42
left a comment
There was a problem hiding this comment.
Auto-approved by pulse runner @superdav42 — author @kenedytorcatt confirmed collaborator, pre-merge gates passed.
|
DISPATCH_CLAIM nonce=510505c5c865ef2763e2faae78fddbdf runner=superdav42 ts=2026-05-04T06:21:37Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
inc/helpers/class-site-duplicator.php (1)
159-168:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun the context/cache reset on every exit path.
Both
return falsebranches skip the cleanup you added at Lines 208-216. Ifprocess_duplication()or$new_to_site->save()fails after the copy step, the request can still continue with the poisoned DB/cache state this patch is trying to prevent. Move that reset into a shared epilogue orfinally.Also applies to: 200-216
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@inc/helpers/class-site-duplicator.php` around lines 159 - 168, The cleanup that resets context/cache must run on every exit path; move the reset logic currently duplicated at the end of the function (the code you added around lines 208-216) into a single shared epilogue or a finally block so it always executes when leaving the function; ensure failures from self::process_duplication($args) (where $duplicate_site_id is WP_Error) and failures from $new_to_site->save() both trigger the same reset before returning (including the existing wu_log_add('site-duplication', ...) error paths) by invoking the shared reset routine rather than returning early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@inc/helpers/class-site-duplicator.php`:
- Around line 173-176: In the foreach over $identity_snapshot in
class-site-duplicator.php, stop using empty($opt_val) (which treats
intentionally blank strings as "empty") and instead guard only against a
missing/null value; change the condition to check $opt_val !== null (or !==
false if your source can return false for missing) before calling
update_blog_option($to_site_id, $opt_key, $opt_val) so intentionally blank
values like '' are preserved when updating options.
- Around line 108-110: The current loops that fully unwind blog switches by
repeatedly calling restore_current_blog() (the blocks using while (
function_exists('ms_is_switched') && ms_is_switched() ) {
restore_current_blog(); }) discard any switch_to_blog() state the caller
established; instead, at the start of the method capture the caller's switch
depth (or current blog ID) by checking ms_get_switched_blog() / ms_is_switched()
or storing get_current_blog_id() and ms_current_switched(); then after your
operation restore only back to that captured depth/ID (call
restore_current_blog() the exact number of times needed or switch_to_blog() back
to the original blog) rather than looping until ms_is_switched() is false; apply
this change to both occurrences (the loops at the shown blocks).
---
Outside diff comments:
In `@inc/helpers/class-site-duplicator.php`:
- Around line 159-168: The cleanup that resets context/cache must run on every
exit path; move the reset logic currently duplicated at the end of the function
(the code you added around lines 208-216) into a single shared epilogue or a
finally block so it always executes when leaving the function; ensure failures
from self::process_duplication($args) (where $duplicate_site_id is WP_Error) and
failures from $new_to_site->save() both trigger the same reset before returning
(including the existing wu_log_add('site-duplication', ...) error paths) by
invoking the shared reset routine rather than returning early.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3e983c39-87f5-4def-b504-1908aca816d3
📒 Files selected for processing (1)
inc/helpers/class-site-duplicator.php
| while ( function_exists('ms_is_switched') && ms_is_switched() ) { | ||
| restore_current_blog(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In WordPress multisite, does while ( ms_is_switched() ) { restore_current_blog(); }unwind the entireswitch_to_blog() stack, including contexts established by the caller?
💡 Result:
Yes, the loop while ( ms_is_switched ) { restore_current_blog; } unwinds the entire switch_to_blog stack, restoring the context to the original blog and state established before any switches by the caller.
Citations:
- 1: https://developer.wordpress.org/reference/functions/restore_current_blog
- 2: https://github.com/WordPress/WordPress/blob/master/wp-includes/ms-blogs.php
- 3: https://codex.wordpress.org/Function_Reference/ms_is_switched
- 4: http://codex.wordpress.org/WPMU_Functions/switch_to_blog
- 5: https://wordpress.stackexchange.com/questions/89113/restore-current-blog-vs-switch-to-blog
🏁 Script executed:
cat -n inc/helpers/class-site-duplicator.php | head -120Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 5129
🏁 Script executed:
sed -n '200,220p' inc/helpers/class-site-duplicator.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 1080
🏁 Script executed:
rg 'override_site\(' inc/ --type php -B 2 -A 2Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 777
🏁 Script executed:
rg -B 5 -A 10 'override_site\(' inc/ui/class-template-switching-element.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 716
🏁 Script executed:
sed -n '94,250p' inc/helpers/class-site-duplicator.php | grep -n 'switch_to_blog'Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 63
🏁 Script executed:
rg 'switch_to_blog' inc/helpers/class-site-duplicator.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 277
🏁 Script executed:
sed -n '94,180p' inc/helpers/class-site-duplicator.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 3520
🏁 Script executed:
sed -n '180,235p' inc/helpers/class-site-duplicator.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 2217
Preserve the caller's switched-blog context instead of unwinding it entirely.
Both while ( function_exists('ms_is_switched') && ms_is_switched() ) { restore_current_blog(); } loops (lines 108-110 and 209-211) restore all the way back to the network context, which discards any switch_to_blog() scope established by the caller and returns in the wrong blog context. Capture the initial switched depth or blog ID at the start of the method and restore only to that point instead of unwinding the entire stack.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/helpers/class-site-duplicator.php` around lines 108 - 110, The current
loops that fully unwind blog switches by repeatedly calling
restore_current_blog() (the blocks using while (
function_exists('ms_is_switched') && ms_is_switched() ) {
restore_current_blog(); }) discard any switch_to_blog() state the caller
established; instead, at the start of the method capture the caller's switch
depth (or current blog ID) by checking ms_get_switched_blog() / ms_is_switched()
or storing get_current_blog_id() and ms_current_switched(); then after your
operation restore only back to that captured depth/ID (call
restore_current_blog() the exact number of times needed or switch_to_blog() back
to the original blog) rather than looping until ms_is_switched() is false; apply
this change to both occurrences (the loops at the shown blocks).
| foreach ($identity_snapshot as $opt_key => $opt_val) { | ||
| if ( ! empty($opt_val) ) { | ||
| update_blog_option($to_site_id, $opt_key, $opt_val); | ||
| } |
There was a problem hiding this comment.
Restore intentionally blank identity fields too.
Using empty() here skips valid values like an intentionally blank blogdescription, so those fields stay overwritten by the template after the switch. Guard against a missing option value instead of a blank string.
Proposed fix
- foreach ($identity_snapshot as $opt_key => $opt_val) {
- if ( ! empty($opt_val) ) {
- update_blog_option($to_site_id, $opt_key, $opt_val);
- }
- }
+ foreach ($identity_snapshot as $opt_key => $opt_val) {
+ if ( false !== $opt_val ) {
+ update_blog_option($to_site_id, $opt_key, $opt_val);
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| foreach ($identity_snapshot as $opt_key => $opt_val) { | |
| if ( ! empty($opt_val) ) { | |
| update_blog_option($to_site_id, $opt_key, $opt_val); | |
| } | |
| foreach ($identity_snapshot as $opt_key => $opt_val) { | |
| if ( false !== $opt_val ) { | |
| update_blog_option($to_site_id, $opt_key, $opt_val); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/helpers/class-site-duplicator.php` around lines 173 - 176, In the foreach
over $identity_snapshot in class-site-duplicator.php, stop using empty($opt_val)
(which treats intentionally blank strings as "empty") and instead guard only
against a missing/null value; change the condition to check $opt_val !== null
(or !== false if your source can return false for missing) before calling
update_blog_option($to_site_id, $opt_key, $opt_val) so intentionally blank
values like '' are preserved when updating options.
|
DISPATCH_CLAIM nonce=6a68ca092551494b83aff79ccbfadd78 runner=superdav42 ts=2026-05-04T06:25:39Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=caa7e1268da1f628c3dff16b74ddfd7d runner=superdav42 ts=2026-05-04T06:29:36Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=9ce2b0f81cb19f7938d72908e3b62bc8 runner=superdav42 ts=2026-05-04T06:45:23Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=385e8a938c2c29f0aa5beab14c9d7b9a runner=superdav42 ts=2026-05-04T06:46:05Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=952bc694556d5324c6994384481c11cb runner=superdav42 ts=2026-05-04T06:48:31Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=689d51481c7baa05c825b8641c1cdf42 runner=superdav42 ts=2026-05-04T06:49:12Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=3691e933fd851205080d93da639247ee runner=superdav42 ts=2026-05-04T07:16:04Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=a9242462e4db5ee7a567a36146db7c06 runner=superdav42 ts=2026-05-04T07:16:46Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=c2c08d62dd8105a5ee07a2d0b8ae571e runner=superdav42 ts=2026-05-04T07:19:28Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=d87a482d193a00ca853b9e1e9baaf5b3 runner=superdav42 ts=2026-05-04T07:20:07Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=0b8be7f2cec762118bfe853ec8451f03 runner=superdav42 ts=2026-05-04T07:23:41Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=ac0b9b2a1b3e852767db69a9fdcc9c40 runner=superdav42 ts=2026-05-04T07:27:45Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=0338dee27892ddb99678ee22615c6ef1 runner=superdav42 ts=2026-05-04T07:31:52Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=bb03b498db02d8b1a56ff8123cec8999 runner=superdav42 ts=2026-05-04T07:46:20Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=41e18efd8dc6aebefe6b38ca4a83c152 runner=superdav42 ts=2026-05-04T07:47:03Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=b57cc5a25d7f827d6912368628f47fb4 runner=superdav42 ts=2026-05-04T07:49:27Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=29bf4686a15352667c02fb9f04490bd1 runner=superdav42 ts=2026-05-04T07:50:08Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=f43bcbf6472c136cf75185101487f450 runner=superdav42 ts=2026-05-04T08:17:22Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=371d5cc2e8067ad8b63f05ccb6811a8f runner=superdav42 ts=2026-05-04T08:18:02Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=71395cb9296eadc0d850e726498c41f8 runner=superdav42 ts=2026-05-04T08:20:41Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=4dbb1f637d81fca7425a951dab82369d runner=superdav42 ts=2026-05-04T08:21:22Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=add1c224047c8db440e1630cfcfa3d28 runner=superdav42 ts=2026-05-04T11:24:25Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=4bdae5313f7a0f26c85f61f7c4dfd0cc runner=superdav42 ts=2026-05-04T11:25:08Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=aeec8cc90de122e5580a9fec9f142b96 runner=superdav42 ts=2026-05-04T11:28:47Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=7c506d744fe5689033a743196e15e145 runner=superdav42 ts=2026-05-04T11:33:02Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=02629d6e2737200a6e2c643f37ec1859 runner=superdav42 ts=2026-05-04T11:37:14Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=3359064f30ffa5ee62d49398e54dd309 runner=superdav42 ts=2026-05-04T11:51:22Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=c7c4ddf6e1f3474e4329d3027e057424 runner=superdav42 ts=2026-05-04T11:52:02Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=bb5a97361efb91c774f39702cd347d9c runner=superdav42 ts=2026-05-04T11:54:28Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=675b48ddb6a75f0f51d289066c3265dd runner=superdav42 ts=2026-05-04T11:55:09Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=3fd4bf14276da27208ca4e797da9f550 runner=superdav42 ts=2026-05-04T12:21:22Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=4731d602e86239981419e489471c2854 runner=superdav42 ts=2026-05-04T12:22:02Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=055e322b2299cc025cf644c7d6e742c2 runner=superdav42 ts=2026-05-04T12:25:49Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=8d7e43a82e6937ec5cc76e139e58f965 runner=superdav42 ts=2026-05-04T12:26:33Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=da9bc537b8fdc0cf7515e0729523f224 runner=superdav42 ts=2026-05-04T12:30:25Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=1207d1747521146c3cdfcb2d6cb07fe8 runner=superdav42 ts=2026-05-04T12:34:38Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=ebd241635e688f9564b8c02316114ef4 runner=superdav42 ts=2026-05-04T12:38:50Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=cf950f1baaf480f05ccc8a33942e8641 runner=superdav42 ts=2026-05-04T12:52:17Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=8c3156599f0120bf9a43bf03ec504913 runner=superdav42 ts=2026-05-04T12:52:57Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=6562dbff728307cb9282aa4651475848 runner=superdav42 ts=2026-05-04T12:55:27Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=941a95700eff6c1c56a29ef1c987d63b runner=superdav42 ts=2026-05-04T12:56:07Z max_age_s=1800 version=3.14.39 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=b9e8411b98877a58fcf5628560c3ee31 runner=superdav42 ts=2026-05-04T13:23:14Z max_age_s=1800 version=3.14.40 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=8ba603e44d5c488c4c2745d794907121 runner=superdav42 ts=2026-05-04T13:23:58Z max_age_s=1800 version=3.14.40 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=9651ecdab13523c2a2cd3775e650a4cb runner=superdav42 ts=2026-05-04T13:26:31Z max_age_s=1800 version=3.14.40 opencode_version=1.14.33 |
|
DISPATCH_CLAIM nonce=53b4288a38d6c580745c6796cdf605bc runner=superdav42 ts=2026-05-04T13:27:13Z max_age_s=1800 version=3.14.40 opencode_version=1.14.33 |
Hi David — Critical PR for
|
| File | Lines added | Description |
|---|---|---|
inc/helpers/class-site-duplicator.php |
~140 | override_site() modifications + new force_copy_elementor_kit() |
Cheers,
Kenedy Torcatt (KursoPro)
kursopro7@gmail.com
P.S. — Thank you for building UM. We rely on it heavily and want to contribute back. Happy to iterate on this PR or split into smaller commits if preferred.
Merged via PR #1082 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).
aidevops.sh v3.14.41 spent 40s on this as a headless bash routine.
Rework the KursoPro template-switch fixes from PR #1082 to align with the current codebase and fix issues flagged during review: 1. Replace while(ms_is_switched) full-stack unwind with captured caller blog ID + restore-on-exit via cleanup_override_context(). Preserves the caller's switch_to_blog() context instead of unwinding to network root. 2. Change empty($opt_val) to false !== $opt_val on identity restore so intentionally blank values (e.g. empty blogdescription) are preserved instead of silently dropped. 3. Add cleanup_override_context() call to BOTH error return paths (process_duplication failure + save failure), not just the happy path. Prevents poisoned $wpdb/cache state from persisting after a failed override. 4. Remove duplicate force_copy_elementor_kit() public method — the existing backfill_kit_settings() + backfill_elementor_postmeta() + verify_kit_integrity() pipeline already handles Kit copying. Add active CSS regen via Elementor\Core\Files\CSS\Post::update() to backfill_kit_settings() instead. 5. Guard ini_set('memory_limit', '512M') against lowering an existing higher limit by checking wp_convert_hr_to_bytes() first. 6. Use proper @SInCE 2.4.0 version tags (replaces 2.x.x placeholder). Ref #1082
Hi David — Critical PR for
Site_Duplicator::override_site()TL;DR
13 production bugs found in
override_site()causing customer site corruption when switching templates. One real customer (abconline.kursopro.com) was left with broken site + 503 + corrupted blogmeta. Required manual SQL recovery.This PR ports the fix into UM core. Already running as a mu-plugin in our production for 24h with zero regressions. Validated with 18 consecutive switches across 6 templates, 3 RUNS, 100% PASS — including 36 BEFORE/AFTER screenshots and pixel-perfect match between customer site and source template.
Single file changed:
inc/helpers/class-site-duplicator.php. ~140 lines. No breaking changes. No schema changes. No deprecations.🚨 Requesting prioritized review — this affects every customer doing template switches across our 300+ subsites network.
Already validated and protected
We have already deployed our fix as a temporary KP mu-plugin so our customers are protected RIGHT NOW. We have these safety nets active in production:
wp kp-um-tpl restore <blog_id>to manually fix any affected customerwp kp-um-tpl auditscans all subsites for orphan blogmeta. Currently reports 0 orphans across our network.This PR is the same logic ported into UM core so we can remove the mu-plugin and rely on your native code.
Root causes (2 distinct issues)
Issue A:
MUCD_Data::copy_data()corrupts$wpdbcontextAfter
copy_data(), the global$wpdbis left pointing to the template blog's tables (norestore_current_blog()is called). On the nextoverride_site()call within the same request:email_exists()queries the template blog'swp_X_userstable → returnsfalsefor legitimate customer usersSite_Duplicator::create_admin()then trieswpmu_create_user($domain, ...)thinking the email is new → fails because the email actually exists at network levelprocess_duplication()returnsWP_Error('user_creation_error')→override_site()returnsfalseReal impact in our production: 2nd consecutive template switch fails 100% of the time without this fix.
Issue B: Elementor Kit
_elementor_page_settingsretains stale valuesMUCD copies all postmeta rows but Elementor maintains an internal serialize/cache layer. The Kit (post 3) keeps stale
system_colorsandsystem_typographyvalues even though DB is updated correctly. Customer's site renders with previous template's colors.Real example from our incident: Customer subscribed to "Plantilla Belleza" (rosa coral
#EAC7C7) but site rendered with "Plantilla Profesional" colors (azul#305778) for hours after the switch.Fix in this PR
override_site()modificationsrestore_current_blog()+wp_cache_flush())email_exists()returns correct valueprocess_duplication()New public helper method
_elementor_page_settingsand_elementor_datafrom source template's Kitupdate_post_meta()(forces Elementor cache to update)_elementor_*and_wp_*meta from source Kit\Elementor\Core\Files\CSS\Post::update()so frontend immediately reflects new colorsValidation (rigorous testing)
Iterative test in production-mirror staging
Test environment:
kpstage.com(cloned from production via our nightly tunnel)Test customer:
testcliente.kpstage.com(blog 311) with real customer data (membership 266, customer 251)3 RUNS x 6 template switches each = 18 total switches:
Each cycle verified 6 assertions:
blognamepreserved (customer's brand name)admin_emailpreservedwu_template_idcorrectly updatedwu_customer_idpreservedwu_type=customer_owned(not reverted tosite_template)Screenshots: 36 PNGs (BEFORE + AFTER for each switch) + JSON report with full assertion details available on request.
Pixel-perfect production validation
Captured screenshots of:
abconline.kursopro.com(real customer in production with Belleza template)plantilla2.kpstage.com(Belleza source template)Result: identical — same header rosa coral, same imagery, same buttons, same Kit colors. Confirms customer site renders exactly like the chosen template after our fix.
Real-world incident (the trigger for this work)
Customer: Ender Torres (endermaquilla@gmail.com, blog_id=291, abconline.kursopro.com)
Date: 2026-05-03
Symptoms:
blognamechanged to "Plantilla Belleza" (template's name)#305778instead of rosa#EAC7C7)falsewith "Could not create admin user"Recovery required: Manual SQL UPDATE on
wp_blogmetato restorewu_type='customer_owned',wu_membership_id,wu_customer_id, plus manual rerun ofoverride_site()via CLI.This PR ensures it never happens again to any customer.
What this PR does NOT touch
We had 19 bugs total in our investigation session, but only 13 are core UM bugs (this PR). The other 6 are KP-specific:
kp-change-payment-method.php— our own)kp-log-rotation.php— our own)kp-wc-orders-show-paid-date.php— our own)This PR is scoped strictly to UM core. No bloat, no scope creep.
Compatibility & Backwards Compatibility
override_site()override_site()signature unchanged, return value unchangedforce_copy_elementor_kit()returns false silently if\Elementor\Pluginclass doesn't exist$blog_id <= 1duplicate_site()(new site creation) — only modifiesoverride_site()(template switching path)Why we'd love a fast review
override_site(). Doesn't touch anything else.We'd love to remove our mu-plugin and rely on your native UM code as soon as possible.
Available on request
Files changed
inc/helpers/class-site-duplicator.phpoverride_site()modifications + newforce_copy_elementor_kit()Cheers,
Kenedy Torcatt (KursoPro)
kursopro7@gmail.com
P.S. — Thank you for building UM. We rely on it heavily and want to contribute back. Happy to iterate on this PR or split into smaller commits if preferred.
Summary by CodeRabbit