Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1fad3dffe | ||
|
|
1ac347e76f |
@@ -106,4 +106,6 @@ Hosts (z. B. mit und ohne `www`) bleiben die Dateien des anderen Hosts womöglic
|
|||||||
nicht betroffen). Ist der Cache dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
|
nicht betroffen). Ist der Cache dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
|
||||||
und in `lit-site-status` (`wpsc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht
|
und in `lit-site-status` (`wpsc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht
|
||||||
WP-Cron (oder einen System-Cron, der `wp-cron.php` aufruft). Auf Divi-Seiten leert jede Prüfung im Reiter den ganzen
|
WP-Cron (oder einen System-Cron, der `wp-cron.php` aufruft). Auf Divi-Seiten leert jede Prüfung im Reiter den ganzen
|
||||||
Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite.
|
Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite. Wurde etwas außerhalb von LIT MCP
|
||||||
|
geändert, leert `lit/cache-clear` den Cache auf Wunsch (`scope` `all` oder `post` mit `post_id`; erst Probelauf, dann
|
||||||
|
nach Rückfrage `confirm: true`).
|
||||||
|
|||||||
+5
-1
@@ -4,7 +4,7 @@ Tags: mcp, ai, divi, pods, oauth
|
|||||||
Requires at least: 6.9
|
Requires at least: 6.9
|
||||||
Tested up to: 7.1
|
Tested up to: 7.1
|
||||||
Requires PHP: 8.1
|
Requires PHP: 8.1
|
||||||
Stable tag: 1.2.0
|
Stable tag: 1.2.1
|
||||||
License: GPL-2.0-or-later
|
License: GPL-2.0-or-later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
|
||||||
@@ -209,6 +209,10 @@ LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft
|
|||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.1 =
|
||||||
|
* Neu: Tool lit/cache-clear leert den Cache von WP Super Cache gezielt – für die ganze Website (scope all, Recht manage_options) oder einen Beitrag (scope post, Recht edit_post). Ohne confirm nur ein Probelauf, ausgeführt wird erst mit confirm: true. Für Fälle, in denen etwas außerhalb von LIT MCP geändert wurde; Änderungen durch LIT MCP leeren den Cache weiterhin automatisch. Im Reiter „Tools“ erscheint dafür die Gruppe „WP Super Cache“, wenn das Plugin aktiv ist.
|
||||||
|
* Das Leeren der ganzen Website teilt sich die Drosselung mit dem automatischen Leeren (höchstens alle 30 Sekunden, mit Nachlauf). Ist WP Super Cache gesperrt (Reiter „Kompatibilität“), meldet das Tool das, statt still nichts zu tun.
|
||||||
|
|
||||||
= 1.2.0 =
|
= 1.2.0 =
|
||||||
* Neu: Reiter „Kompatibilität“ (ersetzt den Divi-Reiter) mit Übersicht aller angebundenen Plugins: Zustand, erkannte Version, Fähigkeiten, letzter Prüfbericht, Prüfen, Freigeben und Zurückziehen – auch auf dem Handy und barrierefrei. LIT MCP prüft fremde Plugins ohne offizielle Schnittstelle laufend und sperrt bei einem Bruch nur die betroffenen Werkzeuge.
|
* Neu: Reiter „Kompatibilität“ (ersetzt den Divi-Reiter) mit Übersicht aller angebundenen Plugins: Zustand, erkannte Version, Fähigkeiten, letzter Prüfbericht, Prüfen, Freigeben und Zurückziehen – auch auf dem Handy und barrierefrei. LIT MCP prüft fremde Plugins ohne offizielle Schnittstelle laufend und sperrt bei einem Bruch nur die betroffenen Werkzeuge.
|
||||||
* Neu: Pods läuft über diese Prüfung (ab Pods 2.8) und hat einen Lese- und Schreibtest an einem Wegwerf-Eintrag. Ein roter Test sperrt das Schreiben von Pods-Feldern, bis eine neue Prüfung grün ist.
|
* Neu: Pods läuft über diese Prüfung (ab Pods 2.8) und hat einen Lese- und Schreibtest an einem Wegwerf-Eintrag. Ein roter Test sperrt das Schreiben von Pods-Feldern, bis eine neue Prüfung grün ist.
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\SuperCache;
|
||||||
|
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use LIT\MCP\Abilities\FollowUp;
|
||||||
|
use LIT\MCP\Abilities\ToolResult;
|
||||||
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit/cache-clear: empties the WP Super Cache on request – for changes made outside this plugin (the plugin's own
|
||||||
|
* writes empty it anyway, SuperCacheListener). Dry run without confirm; with confirm: true the whole cache goes
|
||||||
|
* through the same throttle and trailing purge as the listener (SuperCachePurger), a post through wp_cache_post_edit,
|
||||||
|
* which skips every status but publish and private. The change log entry is ignored by the listener (NAME).
|
||||||
|
*/
|
||||||
|
final class CacheClearAbility implements Ability, DeclaresRequirements
|
||||||
|
{
|
||||||
|
public const NAME = 'lit/cache-clear';
|
||||||
|
|
||||||
|
private const OUTCOMES = ['purged', 'throttled', 'nothing', 'cache_off'];
|
||||||
|
|
||||||
|
public function __construct(private SuperCachePurger $purger, private ChangeLog $log)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return self::NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Seiten-Cache leeren';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Leert den Seiten-Cache von WP Super Cache: scope "all" den ganzen Cache, scope "post" nur die Seite '
|
||||||
|
. 'eines Beitrags (post_id). Änderungen durch LIT MCP leeren ihn ohnehin automatisch – das Tool ist für '
|
||||||
|
. 'Fälle, in denen etwas außerhalb von LIT MCP geändert wurde. Ohne confirm nur Probelauf (nichts wird '
|
||||||
|
. 'geleert); mit confirm: true nach Rückfrage beim Nutzer ausführen. Das Leeren des ganzen Caches ist auf '
|
||||||
|
. 'alle 30 Sekunden gedrosselt (ein Nachlauf folgt).';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'scope' => ['type' => 'string', 'enum' => ['all', 'post']],
|
||||||
|
'post_id' => ['type' => 'integer', 'minimum' => 1, 'description' => 'Pflicht bei scope "post".'],
|
||||||
|
'confirm' => ['type' => 'boolean', 'description' => 'true nach Bestätigung durch den Nutzer.'],
|
||||||
|
],
|
||||||
|
'required' => ['scope'],
|
||||||
|
'additionalProperties' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputSchema(): ?array
|
||||||
|
{
|
||||||
|
return ToolResult::schema(
|
||||||
|
[
|
||||||
|
'scope' => ['type' => 'string', 'enum' => ['all', 'post']],
|
||||||
|
'outcome' => ['type' => 'string', 'enum' => self::OUTCOMES],
|
||||||
|
],
|
||||||
|
['scope', 'outcome']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function annotations(): array
|
||||||
|
{
|
||||||
|
return Annotations::write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirect(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requirements(array $input): array
|
||||||
|
{
|
||||||
|
return match ($input['scope'] ?? null) {
|
||||||
|
'all' => [SuperCacheFeature::PurgeAll->id()],
|
||||||
|
'post' => [SuperCacheFeature::PurgePost->id()],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permission(array $input): bool|WP_Error
|
||||||
|
{
|
||||||
|
$target = $this->target($input);
|
||||||
|
|
||||||
|
return $target instanceof WP_Error ? $target : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(array $input): mixed
|
||||||
|
{
|
||||||
|
$target = $this->target($input);
|
||||||
|
if ($target instanceof WP_Error) {
|
||||||
|
return $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = $target instanceof WP_Post ? $target : null;
|
||||||
|
$scope = $post === null ? 'all' : 'post';
|
||||||
|
$what = $post === null
|
||||||
|
? 'den ganzen Seiten-Cache'
|
||||||
|
: sprintf('die Seite von "%s" (ID %d)', $post->post_title, $post->ID);
|
||||||
|
$enabled = $this->purger->cacheEnabled();
|
||||||
|
|
||||||
|
if (($input['confirm'] ?? false) !== true) {
|
||||||
|
return $this->dryRun($scope, $post, $what, $enabled);
|
||||||
|
}
|
||||||
|
if (! $enabled) {
|
||||||
|
return $this->result(
|
||||||
|
$scope,
|
||||||
|
'cache_off',
|
||||||
|
'Der Seiten-Cache von WP Super Cache ist ausgeschaltet; nichts zu leeren.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($post !== null) {
|
||||||
|
if (! $this->clearsStatus($post)) {
|
||||||
|
return $this->result($scope, 'nothing', sprintf(
|
||||||
|
'Nichts zu leeren: "%s" ist ein Entwurf bzw. nicht veröffentlicht (Status %s); WP Super Cache '
|
||||||
|
. 'cacht ihn nicht.',
|
||||||
|
$post->post_title,
|
||||||
|
$post->post_status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$outcome = $this->purger->post((int) $post->ID) ? SuperCachePurger::PURGED : SuperCachePurger::LOCKED;
|
||||||
|
} else {
|
||||||
|
$outcome = $this->purger->everything();
|
||||||
|
}
|
||||||
|
if ($outcome === SuperCachePurger::LOCKED) {
|
||||||
|
return Errors::make(
|
||||||
|
'compat_incompatible',
|
||||||
|
'WP Super Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
||||||
|
'Den Nutzer informieren, dass die Kompatibilität von WP Super Cache geprüft werden muss.',
|
||||||
|
423
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = match ($outcome) {
|
||||||
|
SuperCachePurger::THROTTLED => 'Der ganze Seiten-Cache wurde vor weniger als 30 Sekunden geleert; das '
|
||||||
|
. 'Leeren folgt als Nachlauf per WP-Cron.',
|
||||||
|
default => ucfirst($what) . ' wurde geleert.',
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this->result($scope, $outcome, $summary, $post);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dryRun(string $scope, ?WP_Post $post, string $what, bool $enabled): WP_Error
|
||||||
|
{
|
||||||
|
$throttled = $scope === 'all' && $this->purger->throttled();
|
||||||
|
$note = '';
|
||||||
|
if (! $enabled) {
|
||||||
|
$note = ' Der Cache ist ausgeschaltet; es würde nichts geleert.';
|
||||||
|
} elseif ($throttled) {
|
||||||
|
$note = ' Das Leeren wäre gerade gedrosselt und folgt als Nachlauf.';
|
||||||
|
} elseif ($post !== null && ! $this->clearsStatus($post)) {
|
||||||
|
$note = ' Der Beitrag ist nicht veröffentlicht; es gäbe nichts zu leeren.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors::confirmRequired(
|
||||||
|
sprintf('Probelauf: %s leeren?%s Nichts wurde geleert.', ucfirst($what), $note),
|
||||||
|
array_filter(
|
||||||
|
['scope' => $scope, 'post_id' => $post?->ID, 'cache_enabled' => $enabled, 'throttled' => $throttled],
|
||||||
|
static fn (mixed $value): bool => $value !== null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function result(string $scope, string $outcome, string $summary, ?WP_Post $post = null): array
|
||||||
|
{
|
||||||
|
$result = ToolResult::ok($summary)->with('scope', $scope)->with('outcome', $outcome);
|
||||||
|
if (in_array($outcome, ['purged', 'throttled'], true)) {
|
||||||
|
$warnings = [];
|
||||||
|
$logId = FollowUp::run(
|
||||||
|
fn () => $this->log->record(
|
||||||
|
self::NAME,
|
||||||
|
'wpsc',
|
||||||
|
$post === null ? 'all' : (string) $post->ID,
|
||||||
|
$summary
|
||||||
|
),
|
||||||
|
self::NAME . ' change log',
|
||||||
|
'Änderungsprotokoll konnte nicht geschrieben werden; der Cache wurde trotzdem behandelt.',
|
||||||
|
$warnings
|
||||||
|
);
|
||||||
|
$result->logId(is_int($logId) ? $logId : null)->warnings($warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WPSC skips everything but publish and private unless WPSCFORCEUPDATE is defined (wp_cache_post_edit). */
|
||||||
|
private function clearsStatus(WP_Post $post): bool
|
||||||
|
{
|
||||||
|
return defined('WPSCFORCEUPDATE') || in_array($post->post_status, ['publish', 'private'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The checked target: the WP_Post of scope "post", true for "all" – or the error. Input is not schema-validated
|
||||||
|
* yet when permission() runs.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
private function target(array $input): WP_Post|true|WP_Error
|
||||||
|
{
|
||||||
|
$scope = $input['scope'] ?? null;
|
||||||
|
if ($scope === 'all') {
|
||||||
|
return current_user_can('manage_options')
|
||||||
|
? true
|
||||||
|
: Errors::forbidden('Keine Berechtigung, den ganzen Seiten-Cache zu leeren (manage_options).');
|
||||||
|
}
|
||||||
|
if ($scope !== 'post') {
|
||||||
|
return Errors::invalidInput('scope fehlt oder ist ungültig.', ['scope' => '"all" oder "post"']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $input['post_id'] ?? null;
|
||||||
|
if (! is_int($id) || $id < 1) {
|
||||||
|
return Errors::invalidInput(
|
||||||
|
'post_id fehlt oder ist ungültig.',
|
||||||
|
['post_id' => 'positive Ganzzahl erwartet']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$post = get_post($id);
|
||||||
|
if (! $post instanceof WP_Post) {
|
||||||
|
return Errors::notFound(sprintf('Eintrag %d nicht gefunden.', $id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_user_can('edit_post', $id)
|
||||||
|
? $post
|
||||||
|
: Errors::forbidden(sprintf('Keine Berechtigung, Eintrag %d zu bearbeiten.', $id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\SuperCache;
|
||||||
|
|
||||||
use LIT\MCP\Admin\CompatibilityTab;
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||||
use LIT\MCP\Integrations\Bootable;
|
use LIT\MCP\Integrations\Bootable;
|
||||||
use LIT\MCP\Integrations\Integration;
|
use LIT\MCP\Integrations\Integration;
|
||||||
@@ -12,9 +13,9 @@ use LIT\MCP\Integrations\Integration;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WP Super Cache has no tools: the integration brings the compatibility profile (SuperCacheListener does the
|
* WP Super Cache: the compatibility profile (SuperCacheListener purges after writes, wired by Plugin), the cleanup of
|
||||||
* work, wired by Plugin) and the cleanup of probe pages a dead check left behind. Without abilities it is no tool
|
* probe pages a dead check left behind and the tool lit/cache-clear – a tool group only while WP Super Cache is
|
||||||
* group.
|
* loaded.
|
||||||
*/
|
*/
|
||||||
final class SuperCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
final class SuperCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
||||||
{
|
{
|
||||||
@@ -22,8 +23,11 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
|
|||||||
private $available;
|
private $available;
|
||||||
|
|
||||||
/** @param callable(): bool|null $available Whether WP Super Cache is loaded; null = WPCACHEHOME is defined. */
|
/** @param callable(): bool|null $available Whether WP Super Cache is loaded; null = WPCACHEHOME is defined. */
|
||||||
public function __construct(?callable $available = null)
|
public function __construct(
|
||||||
{
|
?callable $available,
|
||||||
|
private SuperCachePurger $purger,
|
||||||
|
private ChangeLog $log
|
||||||
|
) {
|
||||||
$this->available = $available ?? static fn (): bool => defined('WPCACHEHOME');
|
$this->available = $available ?? static fn (): bool => defined('WPCACHEHOME');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
|
|||||||
|
|
||||||
public function abilities(): array
|
public function abilities(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [new CacheClearAbility($this->purger, $this->log)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function compatibilities(): array
|
public function compatibilities(): array
|
||||||
|
|||||||
@@ -23,27 +23,21 @@ defined('ABSPATH') || exit;
|
|||||||
* honours the user's "clear all on edit" setting). More than one post, or anything else: the whole cache of this
|
* honours the user's "clear all on edit" setting). More than one post, or anything else: the whole cache of this
|
||||||
* site, once. The whole-cache purge runs at most every THROTTLE seconds (a client looping over media updates must
|
* site, once. The whole-cache purge runs at most every THROTTLE seconds (a client looping over media updates must
|
||||||
* not keep the cache empty): a purge inside the lock time schedules exactly one trailing purge (TRAILING_HOOK) at
|
* not keep the cache empty): a purge inside the lock time schedules exactly one trailing purge (TRAILING_HOOK) at
|
||||||
* its end. Single-post purges are never throttled. The release/withdraw entries of the check workflow change
|
* its end (SuperCachePurger, shared with lit/cache-clear). Single-post purges are never throttled. The release/withdraw
|
||||||
* nothing on the site and purge nothing.
|
* entries of the check workflow change nothing on the site and purge nothing; the entries of lit/cache-clear purged
|
||||||
|
* themselves.
|
||||||
*/
|
*/
|
||||||
final class SuperCacheListener
|
final class SuperCacheListener
|
||||||
{
|
{
|
||||||
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
|
public const TRAILING_HOOK = SuperCachePurger::TRAILING_HOOK;
|
||||||
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
public const PURGED_AT = SuperCachePurger::PURGED_AT;
|
||||||
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
|
|
||||||
/** Seconds between two whole-cache purges. */
|
|
||||||
private const THROTTLE = 30;
|
|
||||||
|
|
||||||
private Closure $usable;
|
private SuperCachePurger $purger;
|
||||||
private Closure $cacheEnabled;
|
|
||||||
private Closure $purgePost;
|
|
||||||
private Closure $purgeAll;
|
|
||||||
private Closure $postIdFor;
|
private Closure $postIdFor;
|
||||||
/** @var array<int, true> posts changed in this request */
|
/** @var array<int, true> posts changed in this request */
|
||||||
private array $posts = [];
|
private array $posts = [];
|
||||||
private bool $everything = false;
|
private bool $everything = false;
|
||||||
private bool $queued = false;
|
private bool $queued = false;
|
||||||
private bool $warned = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults are the real WPSC and WordPress functions; closures only for tests.
|
* Defaults are the real WPSC and WordPress functions; closures only for tests.
|
||||||
@@ -62,14 +56,7 @@ final class SuperCacheListener
|
|||||||
?Closure $purgeAll = null,
|
?Closure $purgeAll = null,
|
||||||
?Closure $postIdFor = null
|
?Closure $postIdFor = null
|
||||||
) {
|
) {
|
||||||
$this->usable = $usable;
|
$this->purger = new SuperCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
|
||||||
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
|
||||||
$this->purgePost = $purgePost ?? static function (int $id): void {
|
|
||||||
wp_cache_post_edit($id);
|
|
||||||
};
|
|
||||||
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
|
|
||||||
wp_cache_clear_cache($blog);
|
|
||||||
};
|
|
||||||
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +75,8 @@ final class SuperCacheListener
|
|||||||
public function onChange(string $ability, string $objectType, string $objectId, ?int $logId = null): void
|
public function onChange(string $ability, string $objectType, string $objectId, ?int $logId = null): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (CompatibilityTab::isReleaseAbility($ability) || ! ($this->cacheEnabled)()) {
|
$ignored = CompatibilityTab::isReleaseAbility($ability) || $ability === CacheClearAbility::NAME;
|
||||||
|
if ($ignored || ! $this->purger->cacheEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +105,9 @@ final class SuperCacheListener
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if ($everything || count($posts) > 1) {
|
if ($everything || count($posts) > 1) {
|
||||||
$this->purgeEverything();
|
$this->purger->everything();
|
||||||
} elseif ($posts !== []) {
|
} elseif ($posts !== []) {
|
||||||
$this->purge(SuperCacheFeature::PurgePost, fn () => ($this->purgePost)($posts[0]));
|
$this->purger->post($posts[0]);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::failure('WP Super Cache purge', $e);
|
Log::failure('WP Super Cache purge', $e);
|
||||||
@@ -130,53 +118,14 @@ final class SuperCacheListener
|
|||||||
public function trailingPurge(): void
|
public function trailingPurge(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (($this->cacheEnabled)()) {
|
if ($this->purger->cacheEnabled()) {
|
||||||
$this->purgeEverything();
|
$this->purger->everything();
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::failure('WP Super Cache trailing purge', $e);
|
Log::failure('WP Super Cache trailing purge', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
|
||||||
* are per site: the callback runs on this blog, no blog switch, wp_cache_clear_cache takes the blog ID).
|
|
||||||
*/
|
|
||||||
private function purgeEverything(): void
|
|
||||||
{
|
|
||||||
$this->purge(SuperCacheFeature::PurgeAll, function (): void {
|
|
||||||
$last = get_transient(self::PURGED_AT);
|
|
||||||
// Transients in the options table come back as strings.
|
|
||||||
if (is_numeric($last) && time() - (int) $last < self::THROTTLE) {
|
|
||||||
if (wp_next_scheduled(self::TRAILING_HOOK) === false) {
|
|
||||||
wp_schedule_single_event((int) $last + self::THROTTLE, self::TRAILING_HOOK);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Known and unavoidable on a multisite: WPSC also empties the shared blogs/ meta directory of all sites
|
|
||||||
// in its blog branch (wp-cache-phase2.php, wp_cache_clear_cache).
|
|
||||||
($this->purgeAll)(is_multisite() ? get_current_blog_id() : 0);
|
|
||||||
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param Closure(): void $purge */
|
|
||||||
private function purge(SuperCacheFeature $feature, Closure $purge): void
|
|
||||||
{
|
|
||||||
if (! ($this->usable)($feature)) {
|
|
||||||
if (! $this->warned) {
|
|
||||||
$this->warned = true;
|
|
||||||
Log::line(sprintf('WP Super Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$purge();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function publicPostId(string $objectType, string $objectId): ?int
|
private static function publicPostId(string $objectType, string $objectId): ?int
|
||||||
{
|
{
|
||||||
if ($objectType === 'attachment' || ! ctype_digit($objectId)) {
|
if ($objectType === 'attachment' || ! ctype_digit($objectId)) {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\SuperCache;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Log;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two purges of WP Super Cache, shared by SuperCacheListener (after every write) and lit/cache-clear: the
|
||||||
|
* capability check, the 30 s throttle of the whole-cache purge with its one trailing purge (TRAILING_HOOK) and the
|
||||||
|
* stamp (PURGED_AT). The state lives in the transient and the cron event, so any number of instances share it.
|
||||||
|
*/
|
||||||
|
final class SuperCachePurger
|
||||||
|
{
|
||||||
|
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
|
||||||
|
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
||||||
|
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
|
||||||
|
/** Seconds between two whole-cache purges. */
|
||||||
|
private const THROTTLE = 30;
|
||||||
|
|
||||||
|
public const PURGED = 'purged';
|
||||||
|
public const THROTTLED = 'throttled';
|
||||||
|
public const LOCKED = 'locked';
|
||||||
|
|
||||||
|
private Closure $usable;
|
||||||
|
private Closure $cacheEnabled;
|
||||||
|
private Closure $purgePost;
|
||||||
|
private Closure $purgeAll;
|
||||||
|
private bool $warned = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults are the real WPSC functions; closures only for tests.
|
||||||
|
*
|
||||||
|
* @param Closure(SuperCacheFeature): bool $usable Compatibility::usable().
|
||||||
|
* @param (Closure(): bool)|null $cacheEnabled WPSC caching switched on ($cache_enabled).
|
||||||
|
* @param (Closure(int): void)|null $purgePost wp_cache_post_edit($postId).
|
||||||
|
* @param (Closure(int): void)|null $purgeAll wp_cache_clear_cache($blogId), 0 = all sites.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Closure $usable,
|
||||||
|
?Closure $cacheEnabled = null,
|
||||||
|
?Closure $purgePost = null,
|
||||||
|
?Closure $purgeAll = null
|
||||||
|
) {
|
||||||
|
$this->usable = $usable;
|
||||||
|
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
||||||
|
$this->purgePost = $purgePost ?? static function (int $id): void {
|
||||||
|
wp_cache_post_edit($id);
|
||||||
|
};
|
||||||
|
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
|
||||||
|
wp_cache_clear_cache($blog);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cacheEnabled(): bool
|
||||||
|
{
|
||||||
|
return ($this->cacheEnabled)();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a whole-cache purge now would only be postponed. */
|
||||||
|
public function throttled(): bool
|
||||||
|
{
|
||||||
|
return self::within($this->lastPurge());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function within(?int $last): bool
|
||||||
|
{
|
||||||
|
return $last !== null && time() - $last < self::THROTTLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time of the last whole-cache purge; null = none within the lock time. Transients may come back as strings. */
|
||||||
|
private function lastPurge(): ?int
|
||||||
|
{
|
||||||
|
$last = get_transient(self::PURGED_AT);
|
||||||
|
|
||||||
|
return is_numeric($last) ? (int) $last : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** wp_cache_post_edit for one post; false = locked. Never throttled. */
|
||||||
|
public function post(int $postId): bool
|
||||||
|
{
|
||||||
|
if (! $this->allowed(SuperCacheFeature::PurgePost)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
($this->purgePost)($postId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
||||||
|
* are per site: the callback runs on this blog, no blog switch, wp_cache_clear_cache takes the blog ID).
|
||||||
|
*
|
||||||
|
* @return self::PURGED|self::THROTTLED|self::LOCKED
|
||||||
|
*/
|
||||||
|
public function everything(): string
|
||||||
|
{
|
||||||
|
if (! $this->allowed(SuperCacheFeature::PurgeAll)) {
|
||||||
|
return self::LOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
$last = $this->lastPurge();
|
||||||
|
if ($last !== null && self::within($last)) {
|
||||||
|
if (wp_next_scheduled(self::TRAILING_HOOK) === false) {
|
||||||
|
wp_schedule_single_event($last + self::THROTTLE, self::TRAILING_HOOK);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::THROTTLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known and unavoidable on a multisite: WPSC also empties the shared blogs/ meta directory of all sites
|
||||||
|
// in its blog branch (wp-cache-phase2.php, wp_cache_clear_cache).
|
||||||
|
($this->purgeAll)(is_multisite() ? get_current_blog_id() : 0);
|
||||||
|
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
||||||
|
|
||||||
|
return self::PURGED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function allowed(SuperCacheFeature $feature): bool
|
||||||
|
{
|
||||||
|
if (($this->usable)($feature)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (! $this->warned) {
|
||||||
|
$this->warned = true;
|
||||||
|
Log::line(sprintf('WP Super Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-2
@@ -37,7 +37,9 @@ use LIT\MCP\Integrations\Pods\PodsCompatibility;
|
|||||||
use LIT\MCP\Integrations\Pods\PodsFeature;
|
use LIT\MCP\Integrations\Pods\PodsFeature;
|
||||||
use LIT\MCP\Integrations\Pods\PodsFieldSource;
|
use LIT\MCP\Integrations\Pods\PodsFieldSource;
|
||||||
use LIT\MCP\Integrations\Pods\PodsIntegration;
|
use LIT\MCP\Integrations\Pods\PodsIntegration;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
||||||
@@ -59,7 +61,7 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
final class Plugin
|
final class Plugin
|
||||||
{
|
{
|
||||||
public const VERSION = '1.2.0';
|
public const VERSION = '1.2.1';
|
||||||
|
|
||||||
private static string $pluginFile = '';
|
private static string $pluginFile = '';
|
||||||
private static ?SettingsPage $settingsPage = null;
|
private static ?SettingsPage $settingsPage = null;
|
||||||
@@ -214,7 +216,16 @@ final class Plugin
|
|||||||
$divi->library(),
|
$divi->library(),
|
||||||
$divi->themeBuilder(),
|
$divi->themeBuilder(),
|
||||||
new PodsIntegration(),
|
new PodsIntegration(),
|
||||||
new SuperCacheIntegration()
|
new SuperCacheIntegration(
|
||||||
|
null,
|
||||||
|
// Asked at call time: the compatibility exists only once the registry does.
|
||||||
|
new SuperCachePurger(
|
||||||
|
static function (SuperCacheFeature $feature) use (&$registry): bool {
|
||||||
|
return $registry instanceof IntegrationRegistry && $registry->compatibility()->usable($feature);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
$changeLog
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return $registry;
|
return $registry;
|
||||||
|
|||||||
@@ -295,6 +295,47 @@ cache_file_gone || fail "the trailing purge did not empty the cache"
|
|||||||
[ "$(wp transient get "$PURGED_AT")" -gt "$LAST" ] || fail "the trailing purge must store a new purge time"
|
[ "$(wp transient get "$PURGED_AT")" -gt "$LAST" ] || fail "the trailing purge must store a new purge time"
|
||||||
pass "trailing purge by cron: cache emptied, event gone, purge time renewed"
|
pass "trailing purge by cron: cache emptied, event gone, purge time renewed"
|
||||||
|
|
||||||
|
# 8b. lit/cache-clear: the dry run changes nothing, confirm empties the post's page / the whole cache
|
||||||
|
fill_cache
|
||||||
|
for ARGS in "{\"scope\":\"post\",\"post_id\":$POST_ID}" '{"scope":"all"}'; do
|
||||||
|
DRY=$(ability lit/cache-clear "$ARGS")
|
||||||
|
[ "$(error_code "$DRY")" = confirm_required ] || fail "lit/cache-clear dry run $ARGS must ask for confirm: $DRY"
|
||||||
|
echo "$DRY" | jq -e '(.__error | fromjson | .cache_enabled) == true' >/dev/null || fail "dry run must report the cache as on: $DRY"
|
||||||
|
done
|
||||||
|
sleep 2
|
||||||
|
cache_file_exists || fail "the dry runs of lit/cache-clear emptied the cache"
|
||||||
|
[ "$(event_count)" = 0 ] || fail "a dry run must not schedule anything"
|
||||||
|
pass "lit/cache-clear without confirm: asks for confirmation, reports the cache as on, cache file stays"
|
||||||
|
|
||||||
|
CLEARED=$(ability lit/cache-clear "{\"scope\":\"post\",\"post_id\":$POST_ID,\"confirm\":true}")
|
||||||
|
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear post: $CLEARED"
|
||||||
|
wait_for "the cache file of the post is gone after lit/cache-clear scope post" cache_file_gone
|
||||||
|
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
|
||||||
|
pass "lit/cache-clear scope post with confirm: the page of the post is emptied"
|
||||||
|
|
||||||
|
# The whole-cache purge needs the lock time of the earlier steps to be over.
|
||||||
|
WAIT=$(($(wp transient get "$PURGED_AT") + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
||||||
|
fill_cache
|
||||||
|
CLEARED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
|
||||||
|
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear all: $CLEARED"
|
||||||
|
wait_for "the cache is empty after lit/cache-clear scope all" cache_file_gone
|
||||||
|
sleep 2
|
||||||
|
# Its own change log entry must not purge a second time (that would be throttled and leave a trailing event).
|
||||||
|
[ "$(event_count)" = 0 ] || fail "the change log entry of lit/cache-clear triggered a second purge"
|
||||||
|
LAST=$(wp transient get "$PURGED_AT")
|
||||||
|
pass "lit/cache-clear scope all with confirm: whole cache emptied once, own log entry purges nothing"
|
||||||
|
|
||||||
|
fill_cache
|
||||||
|
THROTTLED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
|
||||||
|
echo "$THROTTLED" | jq -e '.ok == true and .outcome == "throttled"' >/dev/null || fail "lit/cache-clear all inside the lock time: $THROTTLED"
|
||||||
|
[ "$(event_count)" = 1 ] || fail "a throttled lit/cache-clear must schedule exactly one trailing purge"
|
||||||
|
cache_file_exists || fail "throttled: the cache file must stay"
|
||||||
|
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "a throttled purge must not move the purge time"
|
||||||
|
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
||||||
|
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
|
||||||
|
cache_file_gone || fail "the trailing purge did not empty the cache"
|
||||||
|
pass "lit/cache-clear scope all inside 30 s: throttled, one trailing event, cron run empties the cache"
|
||||||
|
|
||||||
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
|
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
|
||||||
restore
|
restore
|
||||||
verify_restored || fail "the state before the run was not restored"
|
verify_restored || fail "the state before the run was not restored"
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\CacheClearAbility;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
||||||
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class CacheClearAbilityTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $purges = [];
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $scheduled = [];
|
||||||
|
private int|false $lastPurge = false;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->purges = [];
|
||||||
|
$this->scheduled = [];
|
||||||
|
$this->lastPurge = false;
|
||||||
|
Functions\when('is_multisite')->justReturn(false);
|
||||||
|
Functions\when('get_transient')->alias(fn (): int|false => $this->lastPurge);
|
||||||
|
Functions\when('set_transient')->justReturn(true);
|
||||||
|
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||||
|
Functions\when('wp_schedule_single_event')->alias(function (int $at, string $hook): bool {
|
||||||
|
$this->scheduled[] = $hook;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
Functions\when('get_post')->alias(
|
||||||
|
static fn (int $id): ?WP_Post => $id === 7
|
||||||
|
? new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_title' => 'Start', 'post_status' => 'publish'])
|
||||||
|
: ($id === 8
|
||||||
|
? new WP_Post(['ID' => 8, 'post_type' => 'page', 'post_title' => 'Idee', 'post_status' => 'draft'])
|
||||||
|
: null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
unset($GLOBALS['cache_enabled']);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
|
||||||
|
{
|
||||||
|
$purger = new SuperCachePurger(
|
||||||
|
static fn (SuperCacheFeature $feature): bool => $usable,
|
||||||
|
static fn (): bool => $cacheOn,
|
||||||
|
function (int $id): void {
|
||||||
|
$this->purges[] = 'post:' . $id;
|
||||||
|
},
|
||||||
|
function (int $blog): void {
|
||||||
|
$this->purges[] = 'all:' . $blog;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new CacheClearAbility($purger, $log ?? FakeChangeLog::forbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescribesItselfLikeAGuardedWriteTool(): void
|
||||||
|
{
|
||||||
|
$ability = $this->ability();
|
||||||
|
|
||||||
|
self::assertInstanceOf(Ability::class, $ability);
|
||||||
|
self::assertSame('lit/cache-clear', $ability->name());
|
||||||
|
self::assertFalse($ability->isDirect());
|
||||||
|
self::assertSame(Annotations::write(true), $ability->annotations());
|
||||||
|
self::assertStringContainsString('confirm: true', $ability->description());
|
||||||
|
self::assertStringContainsString('WP Super Cache', $ability->description());
|
||||||
|
|
||||||
|
$schema = $ability->inputSchema();
|
||||||
|
self::assertSame(['scope'], $schema['required']);
|
||||||
|
self::assertFalse($schema['additionalProperties']);
|
||||||
|
self::assertSame(['all', 'post'], $schema['properties']['scope']['enum']);
|
||||||
|
self::assertSame(['scope', 'post_id', 'confirm'], array_keys($schema['properties']));
|
||||||
|
self::assertArrayHasKey('outcome', $ability->outputSchema()['properties']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeclaresTheCapabilityOfTheScope(): void
|
||||||
|
{
|
||||||
|
$ability = $this->ability();
|
||||||
|
|
||||||
|
self::assertInstanceOf(DeclaresRequirements::class, $ability);
|
||||||
|
self::assertSame(['wpsc-purge-all'], $ability->requirements(['scope' => 'all']));
|
||||||
|
self::assertSame(['wpsc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
|
||||||
|
self::assertSame([], $ability->requirements(['scope' => 'x']));
|
||||||
|
self::assertSame([], $ability->requirements([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllNeedsManageOptions(): void
|
||||||
|
{
|
||||||
|
$asked = [];
|
||||||
|
Functions\when('current_user_can')->alias(function (string $cap) use (&$asked): bool {
|
||||||
|
$asked[] = $cap;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertToolError($this->ability()->permission(['scope' => 'all']), 'forbidden');
|
||||||
|
self::assertSame(['manage_options'], $asked);
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
self::assertTrue($this->ability()->permission(['scope' => 'all']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostNeedsEditPostForThatId(): void
|
||||||
|
{
|
||||||
|
$asked = [];
|
||||||
|
Functions\when('current_user_can')->alias(function (string $cap, int $id) use (&$asked): bool {
|
||||||
|
$asked[] = [$cap, $id];
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 7]), 'forbidden');
|
||||||
|
self::assertSame([['edit_post', 7]], $asked);
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
self::assertTrue($this->ability()->permission(['scope' => 'post', 'post_id' => 7]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnUnknownPostIsNotFound(): void
|
||||||
|
{
|
||||||
|
self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 99]), 'not_found');
|
||||||
|
self::assertToolError($this->ability()->execute(['scope' => 'post', 'post_id' => 99, 'confirm' => true]), 'not_found');
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{array<string, mixed>}> */
|
||||||
|
public static function invalidInputs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no scope' => [[]],
|
||||||
|
'unknown scope' => [['scope' => 'site']],
|
||||||
|
'post without id' => [['scope' => 'post']],
|
||||||
|
'post with zero' => [['scope' => 'post', 'post_id' => 0]],
|
||||||
|
'post with text' => [['scope' => 'post', 'post_id' => 'abc']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider invalidInputs
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
public function testInvalidInputIsRefusedWithoutEffect(array $input): void
|
||||||
|
{
|
||||||
|
self::assertToolError($this->ability()->permission($input), 'validation_failed');
|
||||||
|
self::assertToolError($this->ability()->execute($input + ['confirm' => true]), 'validation_failed');
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDryRunClearsNothingAndReportsTheState(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['cache_enabled'] = true;
|
||||||
|
$this->lastPurge = time() - 5;
|
||||||
|
|
||||||
|
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertSame([], $this->scheduled);
|
||||||
|
self::assertSame('all', $payload['scope']);
|
||||||
|
self::assertTrue($payload['cache_enabled']);
|
||||||
|
self::assertTrue($payload['throttled']);
|
||||||
|
self::assertStringContainsString('Probelauf', $payload['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDryRunOfAPostNamesThePostAndIgnoresAConfirmThatIsNotTrue(): void
|
||||||
|
{
|
||||||
|
$payload = self::assertToolError(
|
||||||
|
$this->ability(cacheOn: false)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => 'true']),
|
||||||
|
'confirm_required'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertSame(7, $payload['post_id']);
|
||||||
|
self::assertFalse($payload['cache_enabled']);
|
||||||
|
self::assertStringContainsString('Start', $payload['message']);
|
||||||
|
self::assertStringContainsString('ausgeschaltet', $payload['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConfirmClearsThePageOfThePostAndLogsIt(): void
|
||||||
|
{
|
||||||
|
$log = new FakeChangeLog(4);
|
||||||
|
|
||||||
|
$result = $this->ability($log)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame(['post:7'], $this->purges);
|
||||||
|
self::assertTrue($result['ok']);
|
||||||
|
self::assertSame('purged', $result['outcome']);
|
||||||
|
self::assertSame(4, $result['log_id']);
|
||||||
|
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => '7']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testADraftHasNothingToClear(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['cache_enabled'] = true;
|
||||||
|
$result = $this->ability()->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertSame('nothing', $result['outcome']);
|
||||||
|
self::assertStringContainsString('Entwurf', $result['summary']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConfirmClearsTheWholeCacheAtOnce(): void
|
||||||
|
{
|
||||||
|
$log = new FakeChangeLog(1);
|
||||||
|
|
||||||
|
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame(['all:0'], $this->purges);
|
||||||
|
self::assertSame('purged', $result['outcome']);
|
||||||
|
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => 'all']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
|
||||||
|
{
|
||||||
|
$this->lastPurge = time() - 5;
|
||||||
|
|
||||||
|
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertSame([SuperCachePurger::TRAILING_HOOK], $this->scheduled);
|
||||||
|
self::assertSame('throttled', $result['outcome']);
|
||||||
|
self::assertStringContainsString('Nachlauf', $result['summary']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{array<string, mixed>}> */
|
||||||
|
public static function scopes(): array
|
||||||
|
{
|
||||||
|
return ['all' => [['scope' => 'all']], 'post' => [['scope' => 'post', 'post_id' => 7]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider scopes
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
public function testWithTheCacheOffNothingHappensAndNothingIsLogged(array $input): void
|
||||||
|
{
|
||||||
|
$result = $this->ability(FakeChangeLog::forbidden(), cacheOn: false)->execute($input + ['confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertSame('cache_off', $result['outcome']);
|
||||||
|
self::assertNull($result['log_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider confirmedScopes
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
public function testALockedPurgeIsReportedAsAnErrorAndNeitherPurgesNorLogs(array $input): void
|
||||||
|
{
|
||||||
|
$previous = ini_set('error_log', '/dev/null');
|
||||||
|
|
||||||
|
$result = $this->ability(FakeChangeLog::forbidden(), usable: false)->execute($input);
|
||||||
|
|
||||||
|
ini_set('error_log', (string) $previous);
|
||||||
|
self::assertInstanceOf(WP_Error::class, $result);
|
||||||
|
self::assertSame('lit_compat_incompatible', $result->get_error_code());
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'WP Super Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
||||||
|
$result->get_error_message()
|
||||||
|
);
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{array<string, mixed>}> */
|
||||||
|
public static function confirmedScopes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'all' => [['scope' => 'all', 'confirm' => true]],
|
||||||
|
'post' => [['scope' => 'post', 'post_id' => 7, 'confirm' => true]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,30 +11,37 @@ use LIT\MCP\Integrations\Bootable;
|
|||||||
use LIT\MCP\Integrations\Integration;
|
use LIT\MCP\Integrations\Integration;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
||||||
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
final class SuperCacheIntegrationTest extends TestCase
|
final class SuperCacheIntegrationTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testIsAnIntegrationWithoutAbilitiesThatProvidesItsProfile(): void
|
private static function purger(): SuperCachePurger
|
||||||
{
|
{
|
||||||
$integration = new SuperCacheIntegration();
|
return new SuperCachePurger(static fn (): bool => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
|
||||||
|
{
|
||||||
|
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
||||||
|
|
||||||
self::assertInstanceOf(Integration::class, $integration);
|
self::assertInstanceOf(Integration::class, $integration);
|
||||||
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
|
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
|
||||||
self::assertSame('wpsc', $integration->id());
|
self::assertSame('wpsc', $integration->id());
|
||||||
self::assertSame('WP Super Cache', $integration->label());
|
self::assertSame('WP Super Cache', $integration->label());
|
||||||
self::assertSame([], $integration->abilities());
|
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
|
||||||
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
|
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsAvailableOnlyWithSuperCache(): void
|
public function testIsAvailableOnlyWithSuperCache(): void
|
||||||
{
|
{
|
||||||
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration())->isAvailable());
|
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden()))->isAvailable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
|
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
|
||||||
{
|
{
|
||||||
$integration = new SuperCacheIntegration();
|
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
||||||
$integration->boot();
|
$integration->boot();
|
||||||
|
|
||||||
self::assertInstanceOf(Bootable::class, $integration);
|
self::assertInstanceOf(Bootable::class, $integration);
|
||||||
@@ -45,11 +52,11 @@ final class SuperCacheIntegrationTest extends TestCase
|
|||||||
{
|
{
|
||||||
Functions\expect('get_posts')->never();
|
Functions\expect('get_posts')->never();
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
(new SuperCacheIntegration(static fn (): bool => true))->removeOrphans('pods');
|
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
|
||||||
(new SuperCacheIntegration(static fn (): bool => false))->removeOrphans('wpsc');
|
(new SuperCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
||||||
|
|
||||||
Functions\when('current_user_can')->justReturn(false);
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
(new SuperCacheIntegration(static fn (): bool => true))->removeOrphans('wpsc');
|
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
||||||
$this->addToAssertionCount(1);
|
$this->addToAssertionCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +70,6 @@ final class SuperCacheIntegrationTest extends TestCase
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
(new SuperCacheIntegration(static fn (): bool => true))->removeOrphans('wpsc');
|
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,18 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** lit/cache-clear purges itself; its change log entry must not trigger a second purge. */
|
||||||
|
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
|
||||||
|
{
|
||||||
|
$listener = $this->listener();
|
||||||
|
$listener->onChange('lit/cache-clear', 'wpsc', 'all', 1);
|
||||||
|
$listener->onChange('lit/cache-clear', 'wpsc', '12', 2);
|
||||||
|
$listener->flush();
|
||||||
|
|
||||||
|
self::assertSame([], $this->purges);
|
||||||
|
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
|
||||||
|
}
|
||||||
|
|
||||||
public function testNothingHappensWhileTheCacheIsOff(): void
|
public function testNothingHappensWhileTheCacheIsOff(): void
|
||||||
{
|
{
|
||||||
$listener = $this->listener(cacheOn: false);
|
$listener = $this->listener(cacheOn: false);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
||||||
|
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class SuperCachePurgerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testALockedWholePurgeDoesNothingAndSaysSo(): void
|
||||||
|
{
|
||||||
|
$previous = ini_set('error_log', '/dev/null');
|
||||||
|
Functions\expect('get_transient')->never();
|
||||||
|
$purger = new SuperCachePurger(
|
||||||
|
static fn (SuperCacheFeature $feature): bool => false,
|
||||||
|
static fn (): bool => true,
|
||||||
|
static fn (int $id) => self::fail('no post purge'),
|
||||||
|
static fn (int $blog) => self::fail('no whole purge')
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(SuperCachePurger::LOCKED, $purger->everything());
|
||||||
|
self::assertFalse($purger->post(3));
|
||||||
|
ini_set('error_log', (string) $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrottledReadsTheStamp(): void
|
||||||
|
{
|
||||||
|
$stamp = false;
|
||||||
|
Functions\when('get_transient')->alias(static function () use (&$stamp) {
|
||||||
|
return $stamp;
|
||||||
|
});
|
||||||
|
$purger = new SuperCachePurger(static fn (): bool => true);
|
||||||
|
|
||||||
|
self::assertFalse($purger->throttled());
|
||||||
|
$stamp = (string) (time() - 5);
|
||||||
|
self::assertTrue($purger->throttled());
|
||||||
|
$stamp = time() - 31;
|
||||||
|
self::assertFalse($purger->throttled());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The stamp is read once: a transient expiring between two reads must not schedule the trailing purge in 1970. */
|
||||||
|
public function testEverythingReadsTheStampOnceAndSchedulesTheTrailingPurgeInTheFuture(): void
|
||||||
|
{
|
||||||
|
$reads = 0;
|
||||||
|
$scheduledAt = [];
|
||||||
|
Functions\when('get_transient')->alias(static function () use (&$reads) {
|
||||||
|
return ++$reads === 1 ? time() - 5 : false;
|
||||||
|
});
|
||||||
|
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||||
|
Functions\when('wp_schedule_single_event')->alias(static function (int $at) use (&$scheduledAt): bool {
|
||||||
|
$scheduledAt[] = $at;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$purger = new SuperCachePurger(static fn (): bool => true);
|
||||||
|
|
||||||
|
self::assertSame(SuperCachePurger::THROTTLED, $purger->everything());
|
||||||
|
|
||||||
|
self::assertSame(1, $reads);
|
||||||
|
self::assertCount(1, $scheduledAt);
|
||||||
|
self::assertGreaterThanOrEqual(time(), $scheduledAt[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,11 +34,13 @@ use Mockery;
|
|||||||
/** The wiring of the composition root: Plugin::integrations() and the steps Plugin::boot() takes with it. */
|
/** The wiring of the composition root: Plugin::integrations() and the steps Plugin::boot() takes with it. */
|
||||||
final class PluginTest extends TestCase
|
final class PluginTest extends TestCase
|
||||||
{
|
{
|
||||||
private static function registry(ToolAvailability $tools = new ToolAvailability()): IntegrationRegistry
|
private static function registry(
|
||||||
{
|
ToolAvailability $tools = new ToolAvailability(),
|
||||||
|
?FakeChangeLog $log = null
|
||||||
|
): IntegrationRegistry {
|
||||||
return Plugin::integrations(
|
return Plugin::integrations(
|
||||||
Mockery::mock(ContentSchema::class),
|
Mockery::mock(ContentSchema::class),
|
||||||
FakeChangeLog::forbidden(),
|
$log ?? FakeChangeLog::forbidden(),
|
||||||
new Limits(),
|
new Limits(),
|
||||||
$tools
|
$tools
|
||||||
);
|
);
|
||||||
@@ -95,8 +97,8 @@ final class PluginTest extends TestCase
|
|||||||
self::assertArrayNotHasKey('pods', $registry->grouped());
|
self::assertArrayNotHasKey('pods', $registry->grouped());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WP Super Cache has no tools either: registered for its profile only. */
|
/** WP Super Cache is registered with its profile; its tool group appears only while the plugin is loaded. */
|
||||||
public function testSuperCacheIsRegisteredWithoutBecomingAToolGroup(): void
|
public function testSuperCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void
|
||||||
{
|
{
|
||||||
$registry = self::registry();
|
$registry = self::registry();
|
||||||
|
|
||||||
@@ -104,6 +106,40 @@ final class PluginTest extends TestCase
|
|||||||
self::assertArrayNotHasKey('wpsc', $registry->grouped());
|
self::assertArrayNotHasKey('wpsc', $registry->grouped());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purger of the real wiring asks the registry's Compatibility at call time: with WP Super Cache loaded and
|
||||||
|
* its manifest met, the whole-cache purge runs (a registry captured by value would still be null: always locked).
|
||||||
|
*
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
*/
|
||||||
|
public function testTheWiredPurgerAsksTheCompatibilityOfTheRegistry(): void
|
||||||
|
{
|
||||||
|
define('WPCACHEHOME', '/plugins/wp-super-cache/');
|
||||||
|
eval('function wp_cache_post_edit($id) { $GLOBALS["wpsc_calls"][] = "post:" . $id; }'
|
||||||
|
. 'function wp_cache_clear_cache($blog = 0) { $GLOBALS["wpsc_calls"][] = "all:" . $blog; }');
|
||||||
|
$GLOBALS['wpsc_calls'] = [];
|
||||||
|
$GLOBALS['cache_path'] = '/cache/';
|
||||||
|
$GLOBALS['blog_cache_dir'] = '/cache/blogs/';
|
||||||
|
$GLOBALS['cache_enabled'] = true;
|
||||||
|
Functions\when('get_file_data')->justReturn(['Version' => '3.1.5']);
|
||||||
|
Functions\when('get_option')->justReturn(false);
|
||||||
|
Functions\when('get_transient')->justReturn(false);
|
||||||
|
Functions\when('set_transient')->justReturn(true);
|
||||||
|
Functions\when('is_multisite')->justReturn(false);
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
$integrations = (new ReflectionProperty(IntegrationRegistry::class, 'integrations'))->getValue(self::registry(log: new FakeChangeLog(1)));
|
||||||
|
$wpsc = array_values(array_filter(
|
||||||
|
$integrations,
|
||||||
|
static fn ($integration): bool => $integration->id() === 'wpsc'
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
$result = $wpsc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame('purged', $result['outcome']);
|
||||||
|
self::assertSame(['all:0'], $GLOBALS['wpsc_calls']);
|
||||||
|
}
|
||||||
|
|
||||||
/** lit-site-status keeps its order: Divi, Pods, WP Super Cache, The Events Calendar. */
|
/** lit-site-status keeps its order: Divi, Pods, WP Super Cache, The Events Calendar. */
|
||||||
public function testSiteStatusListsDiviPodsSuperCacheAndTheEventsCalendarInThatOrder(): void
|
public function testSiteStatusListsDiviPodsSuperCacheAndTheEventsCalendarInThatOrder(): void
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Plugin Name: WP-LIT-MCP
|
* Plugin Name: WP-LIT-MCP
|
||||||
* Plugin URI: https://git.loosen-it.de/Loosen-IT/WP-LIT-MCP
|
* Plugin URI: https://git.loosen-it.de/Loosen-IT/WP-LIT-MCP
|
||||||
* Description: MCP-Server für WordPress, Divi 5 und Pods. Pflegt Inhalte, Medien und Divi-Seiten mit OAuth für KI-Assistenten.
|
* Description: MCP-Server für WordPress, Divi 5 und Pods. Pflegt Inhalte, Medien und Divi-Seiten mit OAuth für KI-Assistenten.
|
||||||
* Version: 1.2.0
|
* Version: 1.2.1
|
||||||
* Update URI: https://git.loosen-it.de/Loosen-IT/WP-LIT-MCP
|
* Update URI: https://git.loosen-it.de/Loosen-IT/WP-LIT-MCP
|
||||||
* Requires at least: 6.9
|
* Requires at least: 6.9
|
||||||
* Requires PHP: 8.1
|
* Requires PHP: 8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user