Skip to content

Comments

fix(domain): prevent multiple primary domains per site#278

Merged
superdav42 merged 2 commits intoUltimate-Multisite:mainfrom
mahyarrezghi:fix/prevent-multiple-primary-domains
Dec 23, 2025
Merged

fix(domain): prevent multiple primary domains per site#278
superdav42 merged 2 commits intoUltimate-Multisite:mainfrom
mahyarrezghi:fix/prevent-multiple-primary-domains

Conversation

@mahyarrezghi
Copy link
Contributor

@mahyarrezghi mahyarrezghi commented Dec 1, 2025

  • Track original primary state in Domain model to detect changes
  • Unset other primary domains when setting a new primary
  • Ensure consistency across API, admin, and creation flows

Summary by CodeRabbit

  • New Features

    • Automatic cleanup of previous primary domain assignments when a new primary is set.
  • Behavior Changes

    • Improved detection of when a domain becomes primary to ensure updates run only when needed.
    • Cleanup now consistently runs as part of the domain save process (removed redundant immediate triggers elsewhere), reducing duplicate background tasks.

✏️ Tip: You can customize this high-level summary in your review settings.

- Track original primary state in Domain model to detect changes
- Unset other primary domains when setting a new primary
- Ensure consistency across API, admin, and creation flows
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

📝 Walkthrough

Walkthrough

Move async cleanup of old primary domains from UI/admin handlers into the Domain model save flow. The model now computes pre-save "was new" status and, after persisting, dispatches wu_async_remove_old_primary_domains when a domain becomes primary. UI/admin code removed their in-place cleanup calls.

Changes

Cohort / File(s) Summary
Domain model updates
inc/models/class-domain.php
Compute was_new = ! $this->exists() before save; detect promotion to primary after successful save and dispatch wu_async_remove_old_primary_domains for same-site old primaries; added use stdClass import (unused); minor formatting tweak.
Admin page removal of duplicate cleanup
inc/admin-pages/class-domain-list-admin-page.php
Removed logic that collected old primary domains and dispatched the async cleanup when adding a new primary domain; still enqueues domain processing and returns redirect.
UI domain mapping removal of duplicate cleanup
inc/ui/class-domain-mapping-element.php
Removed immediate async cleanup calls in handle_user_add_new_domain_modal and handle_user_make_domain_primary_modal; comment left noting cleanup now occurs in model save.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI/Admin
    participant DomainModel as "Domain Model"
    participant DB as "Database"
    participant AsyncQueue as "Async Queue"
    participant Cleanup as "Cleanup Handler"

    User->>UI/Admin: create or promote domain (mark primary)
    UI/Admin->>DomainModel: set attributes and call save()
    DomainModel->>DomainModel: compute was_new = !exists()
    DomainModel->>DB: persist domain record
    DB-->>DomainModel: success

    alt domain is primary AND (was_new OR promoted from non-primary)
        DomainModel->>AsyncQueue: dispatch wu_async_remove_old_primary_domains(site_id, new_primary_id)
        AsyncQueue-->>Cleanup: run async action
        Cleanup->>DB: query old primary domains for site
        DB-->>Cleanup: return old primary ids
        Cleanup->>DB: unset primary flag on old domains
        DB-->>Cleanup: success
    end

    Cleanup-->>UI/Admin: (async) cleanup completed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Fix sso #257 — Related changes to inc/ui/class-domain-mapping-element.php around make-domain-primary handling; likely touches the same UI flow altered here.

Poem

🐰 I hopped through code to mark one true domain,
Saved its state, then nudged old crowns down the lane,
Async brooms sweep tidy while modals stay still,
A rabbit's small patch of logic, neat and chill.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preventing multiple primary domains per site, which is the core objective across all three modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
inc/models/class-domain.php (1)

494-499: Consider clarifying variable naming for readability.

The naming of $new_domain (line 494) and $was_new (line 498) have opposite semantics:

  • $new_domain = $this->exists() — true when domain exists
  • $was_new = ! $this->exists() — true when domain does not exist

This can be confusing for future maintainers. Consider renaming $new_domain to $existed_before_save for consistency with $was_new.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 165ab6e and 0ec8334.

📒 Files selected for processing (1)
  • inc/models/class-domain.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
inc/models/class-domain.php (4)
inc/models/class-site.php (1)
  • exists (1243-1246)
inc/database/engine/class-table.php (1)
  • exists (65-72)
inc/objects/class-billing-address.php (1)
  • exists (95-98)
inc/functions/domain.php (1)
  • wu_get_domains (35-38)
🪛 GitHub Check: Code Quality Checks
inc/models/class-domain.php

[failure] 307-307:
Expected 1 space after "!"; 0 found


[failure] 534-534:
Whitespace found at end of line

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cypress (8.1, chrome)
  • GitHub Check: cypress (8.2, chrome)
🔇 Additional comments (2)
inc/models/class-domain.php (2)

57-64: LGTM!

The new $original_primary property is well-documented and appropriately typed. It correctly enables change tracking for the primary domain state.


542-557: Verify the async action handler exists and handles empty arrays gracefully.

The logic for detecting when a domain becomes primary is correct. However, ensure:

  1. The wu_async_remove_old_primary_domains action handler is registered elsewhere.
  2. The handler gracefully handles an empty array (when no other primaries exist).

Comment on lines 307 to 309
if ($this->id > 0 && !isset($this->original_primary)) {
$this->original_primary = $this->primary_domain;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix code style: add space after negation operator.

Static analysis flagged a missing space after !.

Apply this diff:

-		if ($this->id > 0 && !isset($this->original_primary)) {
+		if ($this->id > 0 && ! isset($this->original_primary)) {
🧰 Tools
🪛 GitHub Check: Code Quality Checks

[failure] 307-307:
Expected 1 space after "!"; 0 found

🤖 Prompt for AI Agents
In inc/models/class-domain.php around lines 307 to 309, the negation operator is
missing a space; change the conditional to add a space after the '!' so it reads
with '! isset(...)' (i.e. ensure there is a space between the '!' and the
following token) to satisfy the code style/static analysis.

@superdav42
Copy link
Collaborator

@mahyarrezghi Thanks for the PR. I simplified the implementation using $this->_original['primary_domain'] instead of a new original property. Probably it wasn't obvious that the _original property existed but it's for this type a thing. I also removed the other calls to wu_async_remove_old_primary_domains to avoid duplicate work. Updating in the save() method is the way to go here!

@superdav42 superdav42 merged commit 7007986 into Ultimate-Multisite:main Dec 23, 2025
10 of 12 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
inc/ui/class-domain-mapping-element.php (1)

607-616: Remove unnecessary query for old primary domains.

Lines 607-615 query old primary domains but the result is no longer used for cleanup (as noted in the comment at line 616). This creates unnecessary database overhead. Since the cleanup now happens in Domain::save(), this query serves no purpose except populating the filter parameter at line 637.

Consider whether the $old_primary_domains parameter in the wu_make_primary_domain_redirect_url filter is actually needed by consumers. If not, remove lines 607-615 entirely.

🔎 Verify if any code consumes the $old_primary_domains filter parameter
#!/bin/bash
# Search for usages of wu_make_primary_domain_redirect_url filter to see if the 4th parameter is used
rg -n -A 10 "apply_filters.*wu_make_primary_domain_redirect_url|add_filter.*wu_make_primary_domain_redirect_url"
inc/models/class-domain.php (1)

12-12: Remove unused import.

The use stdClass; import is not used anywhere in this file. The reference to stdClass at line 591 is only in a PHPDoc comment, which doesn't require an import.

🔎 Proposed fix
-use stdClass;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec8334 and 8380070.

📒 Files selected for processing (3)
  • inc/admin-pages/class-domain-list-admin-page.php
  • inc/models/class-domain.php
  • inc/ui/class-domain-mapping-element.php
💤 Files with no reviewable changes (1)
  • inc/admin-pages/class-domain-list-admin-page.php
🧰 Additional context used
🧬 Code graph analysis (1)
inc/models/class-domain.php (4)
inc/models/class-site.php (1)
  • exists (1243-1246)
inc/database/engine/class-table.php (1)
  • exists (65-72)
inc/objects/class-billing-address.php (1)
  • exists (95-98)
inc/functions/domain.php (1)
  • wu_get_domains (35-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: PHP 7.4
  • GitHub Check: cypress (8.2, chrome)
  • GitHub Check: Build Plugin for Testing
  • GitHub Check: cypress (8.1, chrome)
🔇 Additional comments (2)
inc/ui/class-domain-mapping-element.php (1)

620-637: Well-documented filter addition.

The new wu_make_primary_domain_redirect_url filter is well-documented and allows customization of the redirect behavior after making a domain primary. Good addition for extensibility.

inc/models/class-domain.php (1)

486-486: Correct implementation of new/existing domain tracking.

The $was_new flag correctly captures whether the domain is being created (before the save occurs). This is the right approach for detecting state changes.

Comment on lines +531 to +545
/*
* If this domain was just set as primary, unset other primaries for this site.
*/
if ($this->primary_domain && ($was_new || (isset($this->_original['primary_domain']) && ! $this->_original['primary_domain']))) {
$old_primary_domains = wu_get_domains(
[
'primary_domain' => true,
'blog_id' => $this->blog_id,
'id__not_in' => [$this->id],
'fields' => 'ids',
]
);

do_action('wu_async_remove_old_primary_domains', $old_primary_domains);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual code at the specified lines to understand the current state
sed -n '531,545p' inc/models/class-domain.php | cat -n

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 706


🏁 Script executed:

# Check line 429 for the wu_enqueue_async_action reference mentioned in the review
sed -n '425,435p' inc/models/class-domain.php | cat -n

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 503


🏁 Script executed:

# Search for listeners that handle wu_async_remove_old_primary_domains action
rg -n "add_action.*wu_async_remove_old_primary_domains|wu_async_remove_old_primary_domains" --type php -B 2 -A 10

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 1732


🏁 Script executed:

# Check if wu_enqueue_async_action is used with this action elsewhere
rg -n "wu_enqueue_async_action.*wu_async_remove_old_primary_domains|wu_async_remove_old_primary_domains.*wu_enqueue_async_action" --type php

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 63


🏁 Script executed:

# Search for how _original array is populated in base model
rg -n "class Domain|_original|populate.*original" inc/models/class-domain.php -B 2 -A 5 | head -50

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 659


🏁 Script executed:

# Check the parent class/base model to understand _original initialization
find . -name "*.php" -type f | xargs grep -l "class.*Domain.*extends" | grep -v node_modules

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 828


🏁 Script executed:

# Find the async_remove_old_primary_domains method implementation
rg -n "function async_remove_old_primary_domains" --type php -A 20

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 1338


🏁 Script executed:

# Search for Base_Model class to understand _original initialization
fd "class-base-model.php" --type f

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 112


🏁 Script executed:

# Check if there's any initialization of _original in Domain model
rg -n "class Domain|_original\s*=" inc/models/class-domain.php -B 2 -A 3 | head -40

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 211


🏁 Script executed:

# Look for where wu_enqueue_async_action is actually used in the codebase for comparison
rg -n "wu_enqueue_async_action" --type php -B 2 -A 5 | head -40

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 2433


🏁 Script executed:

# Examine the actual line 534 more carefully for whitespace/formatting
sed -n '534p' inc/models/class-domain.php | cat -A

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 217


🏁 Script executed:

# Check Base_Model to understand how _original is populated during initialization
head -100 inc/models/class-base-model.php | cat -n

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 2350


🏁 Script executed:

# Look for _original initialization in Base_Model
rg -n "_original\s*=" inc/models/class-base-model.php -B 3 -A 3

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 319


🏁 Script executed:

# Check how _original is set when a model is created/loaded
rg -n "public function __construct|function get_original|_original" inc/models/class-base-model.php -B 2 -A 5 | head -80

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 1622


🏁 Script executed:

# Check if the diff provided in review makes sense by comparing whitespace carefully
sed -n '534p' inc/models/class-domain.php | od -c | head -30

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 121


Consider renaming the action or implementing true async queueing for primary domain updates.

The wu_async_remove_old_primary_domains action at line 544 executes synchronously via do_action(), but its name suggests asynchronous processing. The registered listener in inc/managers/class-domain-manager.php (line 895) directly updates domains without queueing async work. To match the naming convention used elsewhere (e.g., wu_enqueue_async_action() at line 210), either rename the action to reflect synchronous execution or use wu_enqueue_async_action() for truly deferred processing.

@mahyarrezghi mahyarrezghi deleted the fix/prevent-multiple-primary-domains branch December 24, 2025 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants