Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ seams a host may rebind:
| `Notices\Contracts\Writer_Interface` | `Notices\Writer` | what each notice says |
| `Conflict\Contracts\Resolver_Interface` | `Conflict\Resolver` | one method: which policy branch a conflict takes |
| `Plugin\Contracts\Deactivator_Interface` | `Plugin\Deactivator` | deactivates the standalone, network-aware |
| `Plugin\Contracts\Checker_Interface` | `Plugin\Checker` | answers whether a plugin is active |
| `Plugin\Contracts\Checker_Interface` | `Plugin\Checker` | answers whether a plugin is active (either scope), and whether it is network-active |
| `Contracts\Activator_Interface` | `Activator` | run-once activation-callback tracking |

The rest — `Boot\Scheduler`, `Loader`, `Registry\Reader`, `Conflict\Detector`, `Conflict\Gatekeeper`,
Expand Down Expand Up @@ -195,7 +195,7 @@ that drives the whole of it against a real WordPress is `tests/unit/Scenario/`.
| `src/Loader.php` | The load pass: the gate chain, the `require_once`, the activation callback. |
| `src/Sub_Plugin.php` | Value object; validates config and answers what it can without a container-bound collaborator. |
| `src/Conflict_Policy.php` | The three policy constants, `default()`, `is_valid()`. |
| `src/Plugin/` | `Deactivator` (turns the standalone off), `Checker` (answers whether a plugin is active), `Loads_Plugin_Functions` (pulls in `wp-admin/includes/plugin.php`), `Contracts\Deactivator_Interface`, `Contracts\Checker_Interface`. The only files that touch WordPress plugin functions. |
| `src/Plugin/` | `Deactivator` (turns the standalone off), `Checker` (answers whether a plugin is active in either scope, and whether it is network-active — `is_plugin_active_for_network()`, which is `false` off a network so no caller needs an `is_multisite()` guard), `Loads_Plugin_Functions` (pulls in `wp-admin/includes/plugin.php`), `Contracts\Deactivator_Interface`, `Contracts\Checker_Interface`. The only files that touch WordPress plugin functions. |
| `src/Registry/` | `Registrar` (holds registered `Sub_Plugin` objects), `Reader` (the registration buffer, drained into the registrar on the way past; the object every pass reads the registry through), `Contracts\Registrar_Interface`. |
| `src/Activator.php` | Runs a sub-plugin's activation callback once ever, recorded in one option. |
| `src/Conflict/` | `Detector` (whether a standalone is in the way), `Resolver` (which policy branch to take), `Gatekeeper` (which requests, and which users, may have one resolved), `Redirector` (where the user lands afterwards), `Rewriter` (rewrites the activation-error screen for a registered standalone), `Contracts\Resolver_Interface`. |
Expand Down
15 changes: 15 additions & 0 deletions src/Plugin/Checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,19 @@ public function is_active( string $basename ): bool {
// sub-plugin per request.
return is_plugin_active( $basename );
}

/**
* @since 1.0.0
*
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool {
$this->load_plugin_functions();

// is_plugin_active_for_network() checks is_multisite() itself and returns false off a
// network, so the stranding guard that reads this needs no is_multisite() test of its own.
return is_plugin_active_for_network( $basename );
}
}
18 changes: 18 additions & 0 deletions src/Plugin/Contracts/Checker_Interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,22 @@ interface Checker_Interface {
* @return bool
*/
public function is_active( string $basename ): bool;

/**
* Whether the plugin is active across the whole network.
*
* Network scope only, unlike `is_active()`, which counts either scope. The two are asked for
* different reasons: `is_active()` answers "the standalone's code is going to run this request",
* while this answers "deactivating it reaches every site" -- the question the conflict resolver's
* stranding guard turns on. Returns `false` whenever the site is not multisite, because
* WordPress's own `is_plugin_active_for_network()` does, so a caller needs no `is_multisite()`
* guard of its own and the stranding guard is a no-op off a network.
*
* @since 1.0.0
*
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool;
}
9 changes: 9 additions & 0 deletions tests/unit/Boot/SchedulerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,15 @@ static function (): Checker_Interface {
public function is_active( string $basename ): bool {
return true;
}

/**
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool {
return false;
}
};
}
);
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/Conflict/DetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,15 @@ public function is_active( string $basename ): bool {

return $this->answer;
}

/**
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool {
return false;
}
};
}

Expand Down Expand Up @@ -531,6 +540,15 @@ public function is_active( string $basename ): bool {

return in_array( $basename, $this->active, true );
}

/**
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool {
return false;
}
};
}

Expand Down
53 changes: 53 additions & 0 deletions tests/unit/Plugin/CheckerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,59 @@ static function () {
$this->assertSame( 1, $calls );
}

public function test_it_reports_a_network_active_plugin(): void {
$this->setFunctionReturn( 'is_plugin_active_for_network', true );

$this->assertTrue( $this->checker->is_network_active( 'give-recurring/give-recurring.php' ) );
}

public function test_it_reports_a_plugin_that_is_not_network_active(): void {
$this->setFunctionReturn( 'is_plugin_active_for_network', false );

$this->assertFalse( $this->checker->is_network_active( 'give-recurring/give-recurring.php' ) );
}

/**
* The basename reaches the stranding guard's comparison next, so asserting only the return value
* would let the wrong plugin decide whether a network-wide deactivation is safe.
*/
public function test_it_passes_the_basename_through_to_the_network_check(): void {
$received = null;

$this->setFunctionReturn(
'is_plugin_active_for_network',
static function ( $basename ) use ( &$received ) {
$received = $basename;

return true;
},
true
);

$this->checker->is_network_active( 'give-recurring/give-recurring.php' );

$this->assertSame( 'give-recurring/give-recurring.php', $received );
}

/**
* The mirror of the single-scope check: is_network_active() asks the network function and only
* that one. is_plugin_active() ORs the network check into its answer, so reaching for it here
* would report a merely site-active standalone as network-active and wave through a network-wide
* deactivation the stranding guard exists to refuse.
*/
public function test_the_network_check_asks_only_the_network_function(): void {
$this->setFunctionReturn(
'is_plugin_active',
static function () {
throw new LogicException( 'is_network_active() must not ask the site-scope function.' );
},
true
);
$this->setFunctionReturn( 'is_plugin_active_for_network', true );

$this->assertTrue( $this->checker->is_network_active( 'give-recurring/give-recurring.php' ) );
}

/**
* `Plugin\Loads_Plugin_Functions` guards on `deactivate_plugins()`, and it has to keep doing so.
* `is_plugin_active()` is a common third-party shim: guarded on that name, something else defining
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/Scenario/HostTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ public function is_active( string $basename ): bool {

return in_array( $basename, $this->active, true );
}

/**
* @param string $basename Plugin basename.
*
* @return bool
*/
public function is_network_active( string $basename ): bool {
return false;
}
};

$deactivator = new class() implements Deactivator_Interface {
Expand Down
Loading