Skip to content

fix: preserve incomplete usermeta during duplication#1424

Merged
superdav42 merged 1 commit into
mainfrom
fix/checkout-usermeta-incomplete-object
Jun 13, 2026
Merged

fix: preserve incomplete usermeta during duplication#1424
superdav42 merged 1 commit into
mainfrom
fix/checkout-usermeta-incomplete-object

Conversation

@superdav42

@superdav42 superdav42 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Read duplicated usermeta rows directly from wp_usermeta so source values stay raw until we decide how to copy them.
  • Preserve serialized usermeta payloads containing __PHP_Incomplete_Class by writing the raw serialized value directly, avoiding WordPress wp_unslash() / map_deep() object mutation.
  • Add a Site Duplicator regression for incomplete serialized usermeta copied from a template-site user.

Production evidence

Controlled duplicate probe on the deployed build showed the remaining checkout failure path:

  • inc/duplication/duplicate.php:196 MUCD_Duplicate::copy_users()
  • update_user_meta() -> update_metadata() -> wp_unslash() -> stripslashes_deep() -> map_deep()
  • Error: The script tried to modify a property on an incomplete object for Yoast\WP\SEO\Presenters\Admin\Indexing_Notification_Presenter

Verification

  • php -l inc/duplication/duplicate.php
  • php -l tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php
  • vendor/bin/phpcs inc/duplication/duplicate.php tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php
  • git diff --check

Not Run

  • vendor/bin/phpunit --filter test_duplication_preserves_incomplete_serialized_user_meta is blocked locally because /tmp/wordpress-tests-lib/includes/functions.php is missing.
  • Local wp eval probe is blocked because this plugin checkout's configured ../wordpress path is not a WordPress install.

Summary by CodeRabbit

  • Bug Fixes

    • Improved site duplication to properly preserve incomplete serialized user metadata during the user copying process.
    • Enhanced user data handling in site duplication to use stricter validation checks for better accuracy.
  • Tests

    • Added test coverage for incomplete serialized user metadata preservation during site duplication.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR modifies site duplication to read user metadata directly from the database and preserve incomplete serialized object payloads that would be corrupted by normal unserialization and reserializing. It enforces strict type checking across the keep-users flow and adds test coverage for the incomplete object handling.

Changes

Usermeta duplication refactor with incomplete object handling

Layer / File(s) Summary
User copy decision with strict equality
inc/duplication/duplicate.php
The condition for keeping users and detecting admin user creation failure now use strict comparison ('yes' === $keep_users and false === $user_id) instead of loose equality.
Raw usermeta SQL reading and filtering
inc/duplication/duplicate.php
The usermeta copy loop now reads raw meta_key and meta_value pairs directly from the database via a prepared SQL query, applies prefix-based filtering, and uses strict in_array(..., true) checks for main-site-derived metadata keys.
Incomplete serialized object detection and preservation
inc/duplication/duplicate.php
New private helper methods detect whether unserialized values contain __PHP_Incomplete_Class anywhere (recursively), and if detected, write the original raw serialized payload directly to the database without reserializing; otherwise copy via normal update_user_meta.
Logging initialization and return value strict checks
inc/duplication/duplicate.php
Log initialization, log(), and log_error() now use strict equality and inequality checks ('yes' === $data['log'], false !== self::$log) for type safety.
Documentation and code quality directives
inc/duplication/duplicate.php
PHPCS ignore directives are expanded in the file header and memory limit bypass comments.
Test validation for incomplete serialized usermeta preservation
tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php
New test method inserts a raw serialized (incomplete) usermeta fixture directly via $wpdb into the template site, duplicates the site, asserts the duplicated site's usermeta contains the exact same raw meta_value under the target blog prefix, and performs cleanup.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant MUCD_Duplicate
  participant WordPress as WordPress Database
  participant MetaHelper as Incomplete Object Handler
  Caller->>MUCD_Duplicate: duplicate_site(keep_users='yes')
  MUCD_Duplicate->>MUCD_Duplicate: Init with strict 'yes' === keep_users check
  MUCD_Duplicate->>WordPress: SELECT meta_key, meta_value FROM usermeta
  WordPress-->>MUCD_Duplicate: Raw serialized metadata rows
  MUCD_Duplicate->>MetaHelper: copy_user_meta_with_preservation()
  MetaHelper->>MetaHelper: Unserialize and detect __PHP_Incomplete_Class
  alt Incomplete object detected
    MetaHelper->>WordPress: Direct SQL INSERT/UPDATE raw meta_value
  else Normal serialized data
    MetaHelper->>WordPress: update_user_meta() reserialize normally
  end
  WordPress-->>MetaHelper: Meta copied
  MetaHelper-->>MUCD_Duplicate: Completed
  MUCD_Duplicate-->>Caller: Duplication finished
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

origin:interactive, status:in-review

Poem

🐰 Raw serialized dreams once broken, now preserved with SQL spoken,
When objects fail to wake from sleep, their payloads we safely keep.
Strict type checks guard every door, duplicating sites forevermore! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly summarizes the main change: preserving incomplete usermeta during duplication, which is the core issue addressed in both the code changes and test additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/checkout-usermeta-incomplete-object

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
inc/duplication/duplicate.php (1)

316-346: 💤 Low value

Consider logging or returning failure status from raw meta operations.

$wpdb->update() and $wpdb->insert() return false on failure, but this is currently ignored. For this hotfix, silent failure is acceptable (better than a checkout crash), but a follow-up could add error logging to surface database write issues during duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/duplication/duplicate.php` around lines 316 - 346, The code calls
$wpdb->update and $wpdb->insert and currently ignores their return values;
capture each call's return (e.g. $res = $wpdb->update(...) / $res =
$wpdb->insert(...)) and if the result === false, either log the failure
(error_log or a logger) including context ($meta_id, $user_id, $meta_key) or
return a failure status from the enclosing function so callers can handle it;
update both branches (the $meta_id update branch and the insert branch using
$raw_meta_value) to perform this check and take the chosen action.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php`:
- Around line 192-194: The finally block currently uses a truthy check on
$result before calling wpmu_delete_blog($result, true), which can pass a
WP_Error object and crash cleanup; change the guard to ensure $result is a valid
integer site ID (e.g., use is_int($result) or ctype_digit/is_numeric + cast and
> 0) before calling wpmu_delete_blog so only numeric site IDs are deleted and
WP_Error objects are ignored.

---

Nitpick comments:
In `@inc/duplication/duplicate.php`:
- Around line 316-346: The code calls $wpdb->update and $wpdb->insert and
currently ignores their return values; capture each call's return (e.g. $res =
$wpdb->update(...) / $res = $wpdb->insert(...)) and if the result === false,
either log the failure (error_log or a logger) including context ($meta_id,
$user_id, $meta_key) or return a failure status from the enclosing function so
callers can handle it; update both branches (the $meta_id update branch and the
insert branch using $raw_meta_value) to perform this check and take the chosen
action.
🪄 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: 1ebec419-70b6-4265-9ec4-0964690ab798

📥 Commits

Reviewing files that changed from the base of the PR and between dc5a204 and 601da37.

📒 Files selected for processing (2)
  • inc/duplication/duplicate.php
  • tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php

Comment on lines +192 to +194
if ($result) {
wpmu_delete_blog($result, true);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard cleanup with a strict site-ID check

Line 192 uses a truthy check on $result; if duplication returns WP_Error, the finally block can call wpmu_delete_blog() with an object and crash cleanup. Use an integer check before deleting the blog.

Suggested fix
-		} finally {
-			if ($result) {
-				wpmu_delete_blog($result, true);
-			}
+		} finally {
+			if (is_int($result) && $result > 0) {
+				wpmu_delete_blog($result, true);
+			}
 
 			wp_delete_user($user_id);
 		}
📝 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.

Suggested change
if ($result) {
wpmu_delete_blog($result, true);
}
if (is_int($result) && $result > 0) {
wpmu_delete_blog($result, true);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WP_Ultimo/Helpers/Site_Duplicator_Test.php` around lines 192 - 194, The
finally block currently uses a truthy check on $result before calling
wpmu_delete_blog($result, true), which can pass a WP_Error object and crash
cleanup; change the guard to ensure $result is a valid integer site ID (e.g.,
use is_int($result) or ctype_digit/is_numeric + cast and > 0) before calling
wpmu_delete_blog so only numeric site IDs are deleted and WP_Error objects are
ignored.

@superdav42 superdav42 merged commit 160a348 into main Jun 13, 2026
12 checks passed
@superdav42 superdav42 deleted the fix/checkout-usermeta-incomplete-object branch June 13, 2026 18:17
@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant