3 Commits
Author SHA1 Message Date
Jan-Niclas Loosen 3b3c0df1f1 Merge branch 'feat/fastest-cache' 2026-11-06 19:57:55 +01:00
Jan-Niclas LoosenandClaude Opus 5.5 6147ba8087 chore(release): Version 1.2.2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:57:55 +01:00
Jan-Niclas LoosenandClaude Opus 5.5 bee9da3a86 feat(cache): WP Fastest Cache statt WP Super Cache
Die Cache-Integration war für WP Super Cache gebaut, eingesetzt wird aber
WP Fastest Cache. Profil (wpfc), Listener, Drosselung, lit/cache-clear und
Leeren-Probe laufen jetzt über die öffentlichen WPFC-Funktionen
wpfc_clear_post_cache_by_id/wpfc_clear_all_cache; die Probe leert nur die
Wegwerf-Seite (singleDeleteCache ohne Elternseiten) und prüft, dass WPFC
den Hook noch verarbeitet. Smoke fastest-cache-smoke.sh ersetzt den WPSC-Smoke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:57:55 +01:00
35 changed files with 1238 additions and 1444 deletions
+4 -5
View File
@@ -100,11 +100,10 @@ nichts geändert. Vorgehen:
`compat_incompatible`; der Stand steht im selben Reiter „Kompatibilität“.
Seitenweite Änderungen (Optionen, Menüs und Menü-Positionen, Medien, Divi-Design, Theme Builder, globale
Bibliotheksmodule) leeren den Cache von WP Super Cache, Änderungen an einem Beitrag nur dessen Seite. Dabei leert der
Purge eines einzelnen Beitrags die Cache-Dateien unter dem Host, über den der MCP-Aufruf eingeht; bei verschiedenen
Hosts (z. B. mit und ohne `www`) bleiben die Dateien des anderen Hosts womöglich ungeleert (der Komplett-Purge ist davon
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
Bibliotheksmodule) leeren den Cache von WP Fastest Cache, Änderungen an einem Beitrag dessen Seite samt Startseite,
Feeds, Sitemap, Nachbarbeiträgen und Archiven seiner Begriffe (so leert WP Fastest Cache einen Beitrag). Ist der Cache
dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
und in `lit-site-status` (`wpfc`). 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
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
+5 -1
View File
@@ -4,7 +4,7 @@ Tags: mcp, ai, divi, pods, oauth
Requires at least: 6.9
Tested up to: 7.1
Requires PHP: 8.1
Stable tag: 1.2.1
Stable tag: 1.2.2
License: GPL-2.0-or-later
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 ==
= 1.2.2 =
* Geändert: Die Cache-Anbindung arbeitet jetzt mit WP Fastest Cache statt WP Super Cache. Änderungen durch LIT MCP leeren den Cache von WP Fastest Cache automatisch (ein Beitrag samt Startseite, Feeds und Archiven, seitenweite Änderungen als Ganzes, höchstens alle 30 Sekunden mit Nachlauf). lit/cache-clear leert ihn auf Wunsch, auch für Entwürfe und gelöschte Beiträge. Im Reiter „Kompatibilität“ erscheint WP Fastest Cache mit einem Leeren-Test an einer Wegwerf-Seite.
* Entfernt: Die Anbindung an WP Super Cache.
= 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 -1
View File
@@ -8,7 +8,7 @@ use Closure;
defined('ABSPATH') || exit;
/** The one check step a profile's behaviour probe makes (Profile::$extraSteps), shared by Pods and WP Super Cache. */
/** The one check step a profile's behaviour probe makes (Profile::$extraSteps), shared by Pods and WP Fastest Cache. */
final class ProbeStep
{
/**
+1 -1
View File
@@ -22,7 +22,7 @@ defined('ABSPATH') || exit;
*/
final class ThrowawayPost
{
/** Meta key and title prefix the Pods and WP Super Cache probes give their entries (the Divi write probe has its own). */
/** Meta key and title prefix the Pods and WP Fastest Cache probes give their entries (the Divi write probe has its own). */
public const MARKER = '_lit_mcp_check_entry';
public const TITLE = '[LIT MCP Prüfung]';
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
@@ -17,18 +17,19 @@ 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).
* lit/cache-clear: empties the WP Fastest Cache on request – for changes made outside this plugin (the plugin's own
* writes empty it anyway, FastestCacheListener). Dry run without confirm; with confirm: true the whole cache goes
* through the same throttle and trailing purge as the listener (FastestCachePurger), a post through
* wpfc_clear_post_cache_by_id (any status: WPFC also finds the address of a trashed post). 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'];
private const OUTCOMES = ['purged', 'throttled', 'cache_off'];
public function __construct(private SuperCachePurger $purger, private ChangeLog $log)
public function __construct(private FastestCachePurger $purger, private ChangeLog $log)
{
}
@@ -44,7 +45,7 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
public function description(): string
{
return 'Leert den Seiten-Cache von WP Super Cache: scope "all" den ganzen Cache, scope "post" nur die Seite '
return 'Leert den Seiten-Cache von WP Fastest 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 '
@@ -89,8 +90,8 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
public function requirements(array $input): array
{
return match ($input['scope'] ?? null) {
'all' => [SuperCacheFeature::PurgeAll->id()],
'post' => [SuperCacheFeature::PurgePost->id()],
'all' => [FastestCacheFeature::PurgeAll->id()],
'post' => [FastestCacheFeature::PurgePost->id()],
default => [],
};
}
@@ -123,34 +124,26 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
return $this->result(
$scope,
'cache_off',
'Der Seiten-Cache von WP Super Cache ist ausgeschaltet; nichts zu leeren.'
'Der Seiten-Cache von WP Fastest 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;
$outcome = $this->purger->post((int) $post->ID) ? FastestCachePurger::PURGED : FastestCachePurger::LOCKED;
} else {
$outcome = $this->purger->everything();
}
if ($outcome === SuperCachePurger::LOCKED) {
if ($outcome === FastestCachePurger::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.',
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
'Den Nutzer informieren, dass die Kompatibilität von WP Fastest Cache geprüft werden muss.',
423
);
}
$summary = match ($outcome) {
SuperCachePurger::THROTTLED => 'Der ganze Seiten-Cache wurde vor weniger als 30 Sekunden geleert; das '
FastestCachePurger::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.',
};
@@ -166,8 +159,6 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
$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(
@@ -188,7 +179,7 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
$logId = FollowUp::run(
fn () => $this->log->record(
self::NAME,
'wpsc',
'wpfc',
$post === null ? 'all' : (string) $post->ID,
$summary
),
@@ -202,12 +193,6 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
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.
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use Closure;
use LIT\MCP\Compatibility\CheckStep;
@@ -10,8 +10,8 @@ use LIT\MCP\Compatibility\ProbeStep;
defined('ABSPATH') || exit;
/** The check step only WP Super Cache has (the purge probe, SuperCacheProbe) and its labels. */
final class SuperCacheCheckSteps
/** The check step only WP Fastest Cache has (the purge probe, FastestCacheProbe) and its labels. */
final class FastestCacheCheckSteps
{
public const PROBE = 'probe';
@@ -19,10 +19,10 @@ final class SuperCacheCheckSteps
private const LABEL_CACHE_OFF = self::LABEL . ' (Cache ist ausgeschaltet, nicht geprüft)';
/**
* The Profile::$extraSteps of WP Super Cache. A switched-off cache is a green step that says so: a skipped step
* The Profile::$extraSteps of WP Fastest Cache. A switched-off cache is a green step that says so: a skipped step
* would lock the purge on a site that caches nothing.
*
* @param Closure(): (list<string>|null) $probe SuperCacheProbe::run().
* @param Closure(): (list<string>|null) $probe FastestCacheProbe::run().
* @return Closure(bool): list<CheckStep>
*/
public static function extra(Closure $probe): Closure
@@ -35,7 +35,7 @@ final class SuperCacheCheckSteps
// Reports saved before labels were stored have none.
return $step->label
?? ($step->key === self::PROBE ? self::LABEL : null)
?? CheckStep::genericLabel($step->key, SuperCacheCompatibility::LABEL)
?? CheckStep::genericLabel($step->key, FastestCacheCompatibility::LABEL)
?? $step->key;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\FastestCache;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\ConstantRequirement;
use LIT\MCP\Compatibility\GlobalRequirement;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
defined('ABSPATH') || exit;
/**
* WP Fastest Cache as a compatibility profile: the two public purge functions FastestCacheListener calls, checked
* against 1.5.5 (wpFastestCache.php). Both only fire an action the WpFastestCache instance hooks in its constructor
* (singleDeleteCache, deleteCache); the probe exercises singleDeleteCache and checks that hook (FastestCacheProbe).
* The cache switch lives in the global $wp_fastest_cache_options (the decoded option WpFastestCache).
*/
final class FastestCacheCompatibility
{
public const LABEL = 'WP Fastest Cache';
public static function profile(): Profile
{
$all = FastestCacheFeature::cases();
return new Profile(
id: 'wpfc',
label: self::LABEL,
tier: Tier::LockOnFailure,
capabilities: $all,
manifests: [new Manifest(
'WP Fastest Cache',
calls: [
new Call('wpfc_clear_post_cache_by_id', 1, [FastestCacheFeature::PurgePost]),
new Call('wpfc_clear_all_cache', 0, [FastestCacheFeature::PurgeAll]),
new Call('WpFastestCache::singleDeleteCache', 3, [FastestCacheFeature::PurgePost]),
new Call('WpFastestCache::getWpContentDir', 1, [FastestCacheFeature::PurgePost]),
],
constants: [new ConstantRequirement('WPFC_MAIN_PATH', $all)],
globals: [
new GlobalRequirement('wp_fastest_cache', $all),
new GlobalRequirement('wp_fastest_cache_options', $all),
]
)],
active: static fn (): bool => defined('WPFC_MAIN_PATH'),
version: static function (): ?string {
if (! defined('WPFC_MAIN_PATH')) {
return null;
}
$header = get_file_data(WPFC_MAIN_PATH . 'wpFastestCache.php', ['Version' => 'Version']);
$version = $header['Version'] ?? '';
return $version === '' ? null : $version;
},
testedVersions: ['1.5.5'],
extraSteps: FastestCacheCheckSteps::extra(static fn (): ?array => (new FastestCacheProbe())->run()),
stepLabel: FastestCacheCheckSteps::label(...)
);
}
/** WPFC caches only while its switch is on (wpFastestCacheStatus in the decoded option, an object or []). */
public static function cacheEnabled(): bool
{
$options = $GLOBALS['wp_fastest_cache_options'] ?? null;
return is_object($options) && ! empty($options->wpFastestCacheStatus);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use LIT\MCP\Compatibility\Capability;
@@ -11,11 +11,11 @@ defined('ABSPATH') || exit;
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
/** What the plugin does to WP Super Cache: empty its cache for one post or entirely. */
enum SuperCacheFeature: string implements Capability
/** What the plugin does to WP Fastest Cache: empty its cache for one post or entirely. */
enum FastestCacheFeature: string implements Capability
{
case PurgePost = 'wpsc-purge-post';
case PurgeAll = 'wpsc-purge-all';
case PurgePost = 'wpfc-purge-post';
case PurgeAll = 'wpfc-purge-all';
public function id(): string
{
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\ChangeLog\ChangeLog;
@@ -13,22 +13,22 @@ use LIT\MCP\Integrations\Integration;
defined('ABSPATH') || exit;
/**
* WP Super Cache: the compatibility profile (SuperCacheListener purges after writes, wired by Plugin), the cleanup of
* probe pages a dead check left behind and the tool lit/cache-clear – a tool group only while WP Super Cache is
* loaded.
* WP Fastest Cache: the compatibility profile (FastestCacheListener purges after writes, wired by Plugin), the
* cleanup of probe pages a dead check left behind and the tool lit/cache-clear – a tool group only while WP Fastest
* Cache is loaded.
*/
final class SuperCacheIntegration implements Integration, ProvidesCompatibility, Bootable
final class FastestCacheIntegration implements Integration, ProvidesCompatibility, Bootable
{
/** @var callable(): bool */
private $available;
/** @param callable(): bool|null $available Whether WP Super Cache is loaded; null = WPCACHEHOME is defined. */
/** @param callable(): bool|null $available Whether WP Fastest Cache is loaded; null = WPFC_MAIN_PATH is defined. */
public function __construct(
?callable $available,
private SuperCachePurger $purger,
private FastestCachePurger $purger,
private ChangeLog $log
) {
$this->available = $available ?? static fn (): bool => defined('WPCACHEHOME');
$this->available = $available ?? static fn (): bool => defined('WPFC_MAIN_PATH');
}
public function boot(): void
@@ -37,24 +37,25 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
}
/**
* lit_mcp_compat_profile_load('wpsc'), before any output: deletes probe pages a dead check left behind
* (SuperCacheProbe::removeOrphans()). A GET without nonce on purpose: only the plugin's own leftovers, idempotent.
* lit_mcp_compat_profile_load('wpfc'), before any output: deletes probe pages a dead check left behind
* (FastestCacheProbe::removeOrphans()). A GET without nonce on purpose: only the plugin's own leftovers,
* idempotent.
*/
public function removeOrphans(string $profileId): void
{
if ($profileId === 'wpsc' && $this->isAvailable() && current_user_can('manage_options')) {
SuperCacheProbe::removeOrphans();
if ($profileId === 'wpfc' && $this->isAvailable() && current_user_can('manage_options')) {
FastestCacheProbe::removeOrphans();
}
}
public function id(): string
{
return 'wpsc';
return 'wpfc';
}
public function label(): string
{
return SuperCacheCompatibility::LABEL;
return FastestCacheCompatibility::LABEL;
}
public function isAvailable(): bool
@@ -69,6 +70,6 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
public function compatibilities(): array
{
return [SuperCacheCompatibility::profile()];
return [FastestCacheCompatibility::profile()];
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use Closure;
use LIT\MCP\Admin\CompatibilityTab;
@@ -14,25 +14,26 @@ use Throwable;
defined('ABSPATH') || exit;
/**
* Empties the WP Super Cache after a write of this plugin (hook lit_mcp_changed). WPSC purges on its own only what
* goes through wp_update_post: field, term and featured-image updates, options, menu locations, Divi design,
* theme builder and global library modules it never sees.
* Empties the WP Fastest Cache after a write of this plugin (hook lit_mcp_changed). WPFC purges on its own only on
* post status transitions (and only with its "new post"/"update post" settings on) and term edits: field, term and
* featured-image updates, options, menu locations, Divi design, theme builder and global library modules it never
* sees.
*
* Purge with trailing run: the listener only collects, one purge runs at the end of the request (shutdown). A post
* of a public post type (not attachment) that still exists: that post (wp_cache_post_edit, which skips drafts and
* 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
* not keep the cache empty): a purge inside the lock time schedules exactly one trailing purge (TRAILING_HOOK) at
* its end (SuperCachePurger, shared with lit/cache-clear). Single-post purges are never throttled. The release/withdraw
* entries of the check workflow change nothing on the site and purge nothing; the entries of lit/cache-clear purged
* themselves.
* of a public post type (not attachment) that still exists: that post (wpfc_clear_post_cache_by_id, which also
* clears the home page, feeds, sitemap, neighbours and term archives of the post). 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 not keep the cache empty): a purge inside the lock time schedules exactly one
* trailing purge (TRAILING_HOOK) at its end (FastestCachePurger, shared with lit/cache-clear). Single-post purges are
* never throttled. The release/withdraw 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 FastestCacheListener
{
public const TRAILING_HOOK = SuperCachePurger::TRAILING_HOOK;
public const PURGED_AT = SuperCachePurger::PURGED_AT;
public const TRAILING_HOOK = FastestCachePurger::TRAILING_HOOK;
public const PURGED_AT = FastestCachePurger::PURGED_AT;
private SuperCachePurger $purger;
private FastestCachePurger $purger;
private Closure $postIdFor;
/** @var array<int, true> posts changed in this request */
private array $posts = [];
@@ -40,12 +41,12 @@ final class SuperCacheListener
private bool $queued = false;
/**
* Defaults are the real WPSC and WordPress functions; closures only for tests.
* Defaults are the real WPFC and WordPress 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.
* @param Closure(FastestCacheFeature): bool $usable Compatibility::usable().
* @param (Closure(): bool)|null $cacheEnabled WPFC caching switched on.
* @param (Closure(int): void)|null $purgePost wpfc_clear_post_cache_by_id($postId).
* @param (Closure(): void)|null $purgeAll wpfc_clear_all_cache().
* @param (Closure(string, string): ?int)|null $postIdFor Post ID if objectType/objectId name an existing
* post of a public type but attachment.
*/
@@ -56,7 +57,7 @@ final class SuperCacheListener
?Closure $purgeAll = null,
?Closure $postIdFor = null
) {
$this->purger = new SuperCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
$this->purger = new FastestCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
}
@@ -91,7 +92,7 @@ final class SuperCacheListener
add_action('shutdown', [$this, 'flush']);
}
} catch (Throwable $e) {
Log::failure('WP Super Cache purge', $e);
Log::failure('WP Fastest Cache purge', $e);
}
}
@@ -110,7 +111,7 @@ final class SuperCacheListener
$this->purger->post($posts[0]);
}
} catch (Throwable $e) {
Log::failure('WP Super Cache purge', $e);
Log::failure('WP Fastest Cache purge', $e);
}
}
@@ -122,7 +123,7 @@ final class SuperCacheListener
$this->purger->everything();
}
} catch (Throwable $e) {
Log::failure('WP Super Cache trailing purge', $e);
Log::failure('WP Fastest Cache trailing purge', $e);
}
}
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\FastestCache;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Log;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Purge probe of the WP Fastest Cache check: a marked throwaway page gets a cache file where WPFC keeps the cache of
* that page (cache/all/<path of its address>/index.html, WPFC's own getWpContentDir), WpFastestCache::
* singleDeleteCache – the method behind wpfc_clear_post_cache_by_id, which FastestCacheListener calls – runs for it,
* and the file must be gone (WPFC moves the directory to cache/tmpWpfc, which it empties itself). The probe also
* checks that WPFC still hooks wpfc_clear_post_cache_by_id to that method: without the hook the public function does
* nothing, silently. The page and the cache directory are always removed (ThrowawayPost); entries a dead check left
* behind are deleted by removeOrphans(). The whole-cache purge (wpfc_clear_all_cache) is not probed: it would empty
* the live cache of the site on every check. Runs only from the check route (Compatibility::check).
*
* The purge is narrowed to the page ($to_clear_parents = false; otherwise WPFC also empties the home page, author
* page, sitemap, neighbours and term archives). NOT avoidable: WPFC still empties the two feed caches and purges the
* external caches configured in WPFC (Varnish, Cloudflare) on every call; creating and deleting the page fire the
* hooks of every other plugin (Divi 5 empties caches on insert and delete of a page).
*
* The page stays a draft; WPFC takes the address a draft would have once published. The draft gets a fixed slug
* straight in the database, without hooks: an empty slug would make its address the home page.
*
* shortcut: needs a pretty permalink structure (a "?page_id=" address has no cache directory, nothing to probe then).
*/
final class FastestCacheProbe
{
private const SLUG_PREFIX = 'lit-mcp-check-';
private const PURGE_HOOK = 'wpfc_clear_post_cache_by_id';
private const ABORTED = 'Fastest-Cache-Probe abgebrochen (Details im PHP-Error-Log).';
private const NOT_HOOKED = 'WP Fastest Cache verarbeitet wpfc_clear_post_cache_by_id nicht mehr; das Leeren eines '
. 'Beitrags bliebe wirkungslos.';
private const NOT_PURGED = 'Die Testdatei im Cache blieb nach dem Leeren durch WP Fastest Cache bestehen.';
private const NO_SLUG = 'Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).';
private const NOT_WRITTEN = 'Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden '
. '(Details im PHP-Error-Log).';
private Closure $enabled;
private Closure $hooked;
private Closure $cacheDir;
private Closure $purge;
private Closure $setSlug;
/**
* Defaults are the real WPFC and WordPress calls; closures only for tests.
*
* @param (Closure(): bool)|null $enabled WPFC caching switched on.
* @param (Closure(): bool)|null $hooked WPFC hooks wpfc_clear_post_cache_by_id to singleDeleteCache.
* @param (Closure(int): ?string)|null $cacheDir Cache directory (with trailing slash) of a page; null = none.
* @param (Closure(int): void)|null $purge singleDeleteCache for this page only.
* @param (Closure(int): bool)|null $setSlug Sets the probe slug without hooks; false = not changed.
*/
public function __construct(
?Closure $enabled = null,
?Closure $hooked = null,
?Closure $cacheDir = null,
?Closure $purge = null,
?Closure $setSlug = null
) {
$this->enabled = $enabled ?? FastestCacheCompatibility::cacheEnabled(...);
$this->hooked = $hooked ?? static fn (): bool
=> has_action(self::PURGE_HOOK, [$GLOBALS['wp_fastest_cache'] ?? null, 'singleDeleteCache']) !== false;
$this->cacheDir = $cacheDir ?? self::cacheDirFor(...);
$this->purge = $purge ?? static function (int $id): void {
$GLOBALS['wp_fastest_cache']->singleDeleteCache(false, $id, false);
};
$this->setSlug = $setSlug ?? self::writeSlug(...);
}
/** @return list<string>|null German problems, [] = green; null = nothing to probe (cache off, no usable path). */
public function run(): ?array
{
if (! ($this->enabled)()) {
return null;
}
if (! ($this->hooked)()) {
return [self::NOT_HOOKED];
}
$unchecked = false;
$problems = self::throwaway()->run('page', function (int $id) use (&$unchecked): array {
if (! ($this->setSlug)($id)) {
Log::line('wpfc probe: the database did not set the slug of page ' . $id);
return [self::NO_SLUG];
}
$dir = ($this->cacheDir)($id);
if ($dir === null) {
$unchecked = true;
return [];
}
return $this->exercise($id, $dir);
});
return $unchecked && $problems === [] ? null : $problems;
}
/** @return list<int> IDs of probe pages a dead check left behind, deleted now. */
public static function removeOrphans(): array
{
return self::throwaway()->removeOrphans('page');
}
private static function throwaway(): ThrowawayPost
{
return new ThrowawayPost(
ThrowawayPost::MARKER,
ThrowawayPost::TITLE,
self::ABORTED,
'Die Probe-Seite konnte nicht angelegt werden (Details im PHP-Error-Log).',
'Die Probe-Seite %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „'
. ThrowawayPost::TITLE . ' …“).',
'wpfc probe'
);
}
/**
* WPFC's cache directory of the probe page, computed the way singleDeleteCache does (the path of the published
* address), or null when it is not exactly <path>/<probe slug>/: a plain "?page_id=" address, a path with "..".
*/
public static function cacheDirFor(int $postId): ?string
{
$post = get_post($postId);
if (! $post instanceof WP_Post) {
return null;
}
$published = clone $post;
$published->post_status = 'publish';
$address = urldecode((string) get_permalink($published));
if (preg_match('#^https?://[^/]+/(.+)$#', $address, $match) !== 1) {
return null;
}
$path = trim($match[1], '/');
$slug = self::SLUG_PREFIX . $postId;
if (str_contains($path, '..') || ($path !== $slug && ! str_ends_with($path, '/' . $slug))) {
return null;
}
return $GLOBALS['wp_fastest_cache']->getWpContentDir('/cache/all/') . $path . '/';
}
// The probe's own file in WPFC's own cache directory, written the way WPFC does it (plain PHP, no WP_Filesystem).
// phpcs:disable WordPress.WP.AlternativeFunctions
/** @return list<string> */
private function exercise(int $id, string $dir): array
{
$file = $dir . 'index.html';
try {
if (! wp_mkdir_p($dir) || file_put_contents($file, '<!-- lit-mcp purge probe -->') === false) {
Log::line('wpfc probe: could not write the test file ' . $file);
return [self::NOT_WRITTEN];
}
($this->purge)($id);
clearstatcache(true, $file);
if (! file_exists($file)) {
return [];
}
Log::line('wpfc probe: singleDeleteCache left the test file ' . $file . ' in place.');
return [self::NOT_PURGED];
} finally {
@unlink($file);
@rmdir($dir);
}
}
// phpcs:enable WordPress.WP.AlternativeFunctions
/** A fixed slug (an empty one would make the page's address the home page) – no hooks, no clean_post_cache. */
private static function writeSlug(int $id): bool
{
global $wpdb;
$rows = $wpdb->update($wpdb->posts, ['post_name' => self::SLUG_PREFIX . $id], ['ID' => $id]);
wp_cache_delete($id, 'posts');
return $rows !== false && (int) $rows > 0;
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
namespace LIT\MCP\Integrations\FastestCache;
use Closure;
use LIT\MCP\Log;
@@ -10,15 +10,15 @@ 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
* The two purges of WP Fastest Cache, shared by FastestCacheListener (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
final class FastestCachePurger
{
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
public const TRAILING_HOOK = 'lit_mcp_wpfc_trailing_purge';
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
public const PURGED_AT = 'lit_mcp_wpfc_purged_at';
/** Seconds between two whole-cache purges. */
private const THROTTLE = 30;
@@ -33,12 +33,12 @@ final class SuperCachePurger
private bool $warned = false;
/**
* Defaults are the real WPSC functions; closures only for tests.
* Defaults are the real WPFC 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.
* @param Closure(FastestCacheFeature): bool $usable Compatibility::usable().
* @param (Closure(): bool)|null $cacheEnabled WPFC caching switched on.
* @param (Closure(int): void)|null $purgePost wpfc_clear_post_cache_by_id($postId).
* @param (Closure(): void)|null $purgeAll wpfc_clear_all_cache().
*/
public function __construct(
Closure $usable,
@@ -47,12 +47,12 @@ final class SuperCachePurger
?Closure $purgeAll = null
) {
$this->usable = $usable;
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
$this->cacheEnabled = $cacheEnabled ?? FastestCacheCompatibility::cacheEnabled(...);
$this->purgePost = $purgePost ?? static function (int $id): void {
wp_cache_post_edit($id);
wpfc_clear_post_cache_by_id($id);
};
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
wp_cache_clear_cache($blog);
$this->purgeAll = $purgeAll ?? static function (): void {
wpfc_clear_all_cache();
};
}
@@ -80,10 +80,10 @@ final class SuperCachePurger
return is_numeric($last) ? (int) $last : null;
}
/** wp_cache_post_edit for one post; false = locked. Never throttled. */
/** wpfc_clear_post_cache_by_id for one post; false = locked. Never throttled. */
public function post(int $postId): bool
{
if (! $this->allowed(SuperCacheFeature::PurgePost)) {
if (! $this->allowed(FastestCacheFeature::PurgePost)) {
return false;
}
($this->purgePost)($postId);
@@ -93,13 +93,13 @@ final class SuperCachePurger
/**
* 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).
* are per site: the callback runs on this blog).
*
* @return self::PURGED|self::THROTTLED|self::LOCKED
*/
public function everything(): string
{
if (! $this->allowed(SuperCacheFeature::PurgeAll)) {
if (! $this->allowed(FastestCacheFeature::PurgeAll)) {
return self::LOCKED;
}
@@ -112,22 +112,20 @@ final class SuperCachePurger
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);
($this->purgeAll)();
set_transient(self::PURGED_AT, time(), self::THROTTLE);
return self::PURGED;
}
private function allowed(SuperCacheFeature $feature): bool
private function allowed(FastestCacheFeature $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()));
Log::line(sprintf('WP Fastest Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
}
return false;
@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\ConstantRequirement;
use LIT\MCP\Compatibility\GlobalRequirement;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\Tier;
defined('ABSPATH') || exit;
/**
* WP Super Cache as a compatibility profile: the two purge functions SuperCacheListener calls, checked against
* 3.1.5 (wp-cache-phase2.php). wp_cache_post_edit is no documented API: if a release drops it, only the purge is
* locked (visible in the tab and in lit-site-status). Both functions read the global $cache_path, which wp-cache.php
* sets on load from wp-cache-config.php or its sample; wp_cache_post_edit also reads $blog_cache_dir
* (wp-cache-base.php). The check adds a purge probe (SuperCacheProbe).
*/
final class SuperCacheCompatibility
{
public const LABEL = 'WP Super Cache';
public static function profile(): Profile
{
$all = SuperCacheFeature::cases();
return new Profile(
id: 'wpsc',
label: self::LABEL,
tier: Tier::LockOnFailure,
capabilities: $all,
manifests: [new Manifest(
'WP Super Cache',
calls: [
new Call('wp_cache_post_edit', 1, [SuperCacheFeature::PurgePost]),
new Call('wp_cache_clear_cache', 1, [SuperCacheFeature::PurgeAll]),
],
constants: [new ConstantRequirement('WPCACHEHOME', $all)],
globals: [
new GlobalRequirement('cache_path', $all),
new GlobalRequirement('blog_cache_dir', [SuperCacheFeature::PurgePost]),
]
)],
active: static fn (): bool => defined('WPCACHEHOME'),
version: static function (): ?string {
if (! defined('WPCACHEHOME')) {
return null;
}
// WPSC_VERSION_ID is a build counter, not the plugin version: read the plugin header.
$version = get_file_data(WPCACHEHOME . 'wp-cache.php', ['Version' => 'Version'])['Version'] ?? '';
return $version === '' ? null : $version;
},
testedVersions: ['3.1.5'],
extraSteps: SuperCacheCheckSteps::extra(static fn (): ?array => (new SuperCacheProbe())->run()),
stepLabel: SuperCacheCheckSteps::label(...)
);
}
}
@@ -1,230 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\SuperCache;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Log;
use Throwable;
defined('ABSPATH') || exit;
/**
* Purge probe of the WP Super Cache check: a marked throwaway page gets a cache file at the exact place WP Super
* Cache keeps the cache of that page (its own path logic), wp_cache_post_edit() – what SuperCacheListener calls –
* runs, and the file must be gone (renamed to ".needs-rebuild" counts as gone: that is how WPSC marks it). The
* file, its directory and the page are always removed (ThrowawayPost). Entries a dead check left behind are
* deleted by removeOrphans(). The whole-cache purge (wp_cache_clear_cache) is not probed: it would empty the live
* cache of the site on every check. Runs only from the check route (Compatibility::check).
*
* The purge of the live site is narrowed for the call: $wp_cache_clear_on_post_edit is 0 (otherwise WPSC empties
* the whole cache) and the filter wpsc_delete_related_pages_on_edit returns false (otherwise wp_cache_post_change
* clears the home page and page/ as well); both are put back exactly, also when WPSC throws. What WPSC still does
* for a page then: it empties the cache directory of that page (wp_cache_post_id_gc, $all = false: no page/, no
* home page), deletes the legacy wp-cache meta files of that page and any unreadable legacy meta file of this site
* (stray files, unusable anyway), fires the action gc_cache ("prune"/"rebuild" with the page's address – other
* plugins hooking it, e.g. preloaders, see the probe page) and, for each public taxonomy term of the page,
* wpsc_delete_post_archives() clears the term archive's cache. A fresh page has no terms unless another plugin
* gives pages a default term; then that one archive loses its cache file on every check.
*
* NOT avoidable: creating, changing and deleting the page are real WordPress calls, and other plugins react to them
* like to any editor's page. Divi 5 (Loop module, LoopHooks → et_core_clear_wp_cache → wp_cache_clear_cache) empties
* the whole WPSC cache on every insert and delete of a page – seen on the DDEV site, so on a Divi site every check
* empties the live cache regardless of what the probe does itself.
*
* The page is a draft; wp_cache_post_edit skips drafts, so for the call it is a private page – its status is set
* straight in the database, without hooks: WPSC itself hooks clean_post_cache/edit_post to wp_cache_post_edit and
* remembers the last post per request, so a real status change would use up the call before the probe makes it.
*
* The cache directory is WPSC's own (get_current_url_supercache_dir), which takes the host from the request. It is
* used only when it is exactly <home host directory>/<probe slug>/; any other host (www or not) makes the probe
* "not checked" instead of looking in the wrong place.
*
* shortcut: needs a pretty permalink structure with WP in the home address' directory (WPSC does not purge a "?p="
* address either, nothing to probe then).
*/
final class SuperCacheProbe
{
private const SLUG_PREFIX = 'lit-mcp-check-';
private const CLEAR_ON_EDIT = 'wp_cache_clear_on_post_edit';
private const RELATED_PAGES_FILTER = 'wpsc_delete_related_pages_on_edit';
/** Files WPSC writes per page and protocol, and what a purge renames them to. */
private const CACHE_FILES = ['index.html', 'index-https.html'];
private const REBUILD_SUFFIX = '.needs-rebuild';
private const ABORTED = 'Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).';
private const NOT_PURGED = 'Die Testdatei im Cache blieb nach dem Leeren durch WP Super Cache bestehen.';
private const NOT_PRIVATE = 'Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).';
private const NOT_WRITTEN = 'Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden '
. '(Details im PHP-Error-Log).';
private Closure $enabled;
private Closure $cacheDir;
private Closure $purge;
private Closure $setStatus;
/**
* Defaults are the real WPSC and WordPress calls; closures only for tests.
*
* @param (Closure(): bool)|null $enabled WPSC caching switched on ($cache_enabled).
* @param (Closure(int): ?string)|null $cacheDir Cache directory (with trailing slash) of a page; null = none.
* @param (Closure(int): void)|null $purge wp_cache_post_edit($postId).
* @param (Closure(int, string): bool)|null $setStatus Sets status (and the slug) of the page without hooks;
* false = the database did not change it.
*/
public function __construct(
?Closure $enabled = null,
?Closure $cacheDir = null,
?Closure $purge = null,
?Closure $setStatus = null
) {
$this->enabled = $enabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
$this->cacheDir = $cacheDir ?? self::cacheDirFor(...);
$this->purge = $purge ?? self::purgeThisPageOnly(...);
$this->setStatus = $setStatus ?? self::writeStatus(...);
}
/** @return list<string>|null German problems, [] = green; null = nothing to probe (cache off, no usable path). */
public function run(): ?array
{
if (! ($this->enabled)()) {
return null;
}
$unchecked = false;
$problems = self::throwaway()->run('page', function (int $id) use (&$unchecked): array {
try {
if (! ($this->setStatus)($id, 'private')) {
Log::line('wpsc probe: the database did not set page ' . $id . ' to private.');
return [self::NOT_PRIVATE];
}
$dir = ($this->cacheDir)($id);
if ($dir === null) {
$unchecked = true;
return [];
}
return $this->exercise($id, $dir);
} finally {
$this->resetToDraft($id);
}
});
return $unchecked && $problems === [] ? null : $problems;
}
/** @return list<int> IDs of probe pages a dead check left behind (draft or private), deleted now. */
public static function removeOrphans(): array
{
return self::throwaway()->removeOrphans('page');
}
private static function throwaway(): ThrowawayPost
{
return new ThrowawayPost(
ThrowawayPost::MARKER,
ThrowawayPost::TITLE,
self::ABORTED,
'Die Probe-Seite konnte nicht angelegt werden (Details im PHP-Error-Log).',
'Die Probe-Seite %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „'
. ThrowawayPost::TITLE . ' …“).',
'wpsc probe'
);
}
/** Must not hide the result of the probe or end the check: the page is deleted right after anyway. */
private function resetToDraft(int $id): void
{
try {
if (! ($this->setStatus)($id, 'draft')) {
Log::line('wpsc probe: the database did not set page ' . $id . ' back to draft.');
}
} catch (Throwable $e) {
Log::failure('wpsc probe', $e);
}
}
/**
* WPSC's own cache directory of the probe page, or null when the probe must not use it: anything but exactly
* <home host directory>/<probe slug>/ – another host than the home URL's (WPSC takes it from the request), a
* plain "?p=" address or an empty slug (both resolve to the home page's directory), a path with "..".
*/
public static function cacheDirFor(int $postId): ?string
{
$home = (string) get_supercache_dir();
$dir = (string) get_current_url_supercache_dir($postId);
return $home !== '' && $dir === $home . self::SLUG_PREFIX . $postId . '/' ? $dir : null;
}
/** wp_cache_post_edit for the probe page only: see the class comment. Global and filter are put back exactly. */
private static function purgeThisPageOnly(int $id): void
{
$hadGlobal = array_key_exists(self::CLEAR_ON_EDIT, $GLOBALS);
$before = $GLOBALS[self::CLEAR_ON_EDIT] ?? null;
// A closure of its own: remove_filter must not take away a "__return_false" another plugin added.
$off = static fn (): bool => false;
try {
$GLOBALS[self::CLEAR_ON_EDIT] = 0;
add_filter(self::RELATED_PAGES_FILTER, $off, PHP_INT_MAX);
wp_cache_post_edit($id);
} finally {
remove_filter(self::RELATED_PAGES_FILTER, $off, PHP_INT_MAX);
if ($hadGlobal) {
$GLOBALS[self::CLEAR_ON_EDIT] = $before;
} else {
unset($GLOBALS[self::CLEAR_ON_EDIT]);
}
}
}
// The probe's own file in WPSC's own cache directory, written the way WPSC does it (plain PHP, no WP_Filesystem).
// phpcs:disable WordPress.WP.AlternativeFunctions
/** @return list<string> */
private function exercise(int $id, string $dir): array
{
$file = $dir . 'index.html';
try {
if (! wp_mkdir_p($dir) || file_put_contents($file, '<!-- lit-mcp purge probe -->') === false) {
Log::line('wpsc probe: could not write the test file ' . $file);
return [self::NOT_WRITTEN];
}
($this->purge)($id);
clearstatcache(true, $file);
if (! file_exists($file)) {
return [];
}
Log::line('wpsc probe: wp_cache_post_edit left the test file ' . $file . ' in place.');
return [self::NOT_PURGED];
} finally {
foreach (self::CACHE_FILES as $name) {
@unlink($dir . $name);
@unlink($dir . $name . self::REBUILD_SUFFIX);
}
@rmdir($dir);
}
}
// phpcs:enable WordPress.WP.AlternativeFunctions
/** Status and a fixed slug (an empty one would make the page's address the home page) – no hooks, no clean_post_cache. */
private static function writeStatus(int $id, string $status): bool
{
global $wpdb;
$columns = ['post_status' => $status, 'post_name' => self::SLUG_PREFIX . $id];
$rows = $wpdb->update($wpdb->posts, $columns, ['ID' => $id]);
wp_cache_delete($id, 'posts');
return $rows !== false && (int) $rows > 0;
}
}
+10 -10
View File
@@ -37,10 +37,10 @@ use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Integrations\Pods\PodsFeature;
use LIT\MCP\Integrations\Pods\PodsFieldSource;
use LIT\MCP\Integrations\Pods\PodsIntegration;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCacheIntegration;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
@@ -61,7 +61,7 @@ defined('ABSPATH') || exit;
*/
final class Plugin
{
public const VERSION = '1.2.1';
public const VERSION = '1.2.2';
private static string $pluginFile = '';
private static ?SettingsPage $settingsPage = null;
@@ -100,8 +100,8 @@ final class Plugin
// Integration-owned hooks (Divi: upgrader hook, check route, CLI command, tab "Divi") – before the settings
// page reads its tabs.
$registry->boot();
// After every write of the plugin: empty the WP Super Cache where WordPress itself would not.
SuperCacheListener::create($registry->compatibility())->register();
// After every write of the plugin: empty the WP Fastest Cache where WordPress itself would not.
FastestCacheListener::create($registry->compatibility())->register();
// The admin check of every compatibility profile (POST lit-mcp/v1/<id>-check), REST like the MCP calls.
(new CheckRoute($registry->compatibility()))->register();
@@ -216,11 +216,11 @@ final class Plugin
$divi->library(),
$divi->themeBuilder(),
new PodsIntegration(),
new SuperCacheIntegration(
new FastestCacheIntegration(
null,
// Asked at call time: the compatibility exists only once the registry does.
new SuperCachePurger(
static function (SuperCacheFeature $feature) use (&$registry): bool {
new FastestCachePurger(
static function (FastestCacheFeature $feature) use (&$registry): bool {
return $registry instanceof IntegrationRegistry && $registry->compatibility()->usable($feature);
}
),
+6 -6
View File
@@ -19,11 +19,11 @@ defined('ABSPATH') || exit;
* Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Integrations\Divi\Release\
* lit_mcp_<profile>_releases (Compatibility\ReleaseStore), lit_mcp_<profile>_check_report
* (Compatibility\CheckReportStore) and lit_mcp_<profile>_check_lock (Admin\CheckRoute) for the
* profiles divi, pods and wpsc (an explicit list: a LIKE pattern could hit foreign options),
* profiles divi, pods and wpfc (an explicit list: a LIKE pattern could hit foreign options),
* Integrations\Divi\Design\DesignLock::option(), Admin\ConnectionStatus::ENDPOINT_TRANSIENT,
* Integrations\Divi\Release\ReleaseResolver::CACHE,
* Integrations\SuperCache\SuperCacheListener::PURGED_AT (transient),
* ChangeLog\ChangeLogCleanup::HOOK, Integrations\SuperCache\SuperCacheListener::TRAILING_HOOK (cron),
* Integrations\FastestCache\FastestCacheListener::PURGED_AT (transient),
* ChangeLog\ChangeLogCleanup::HOOK, Integrations\FastestCache\FastestCacheListener::TRAILING_HOOK (cron),
* ChangeLog\ChangeLog::TABLE, Admin\LogTab::PER_PAGE_OPTION and
* Updater\RecheckOnInstall::OPTION on purpose;
* tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in sync with
@@ -40,7 +40,7 @@ final class Uninstaller
'lit_mcp_allowed_roles', 'lit_mcp_exposure', 'lit_mcp_limits', 'lit_mcp_db_version',
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
'lit_mcp_lock_divi_options', 'lit_mcp_lock_variables', 'lit_mcp_lock_presets', 'lit_mcp_lock_custom_css',
@@ -48,8 +48,8 @@ final class Uninstaller
'lit_mcp_lock_colors', 'lit_mcp_lock_theme_options',
'lit_mcp_installed_version',
];
private const TRANSIENTS = ['lit_mcp_endpoint_check', 'lit_mcp_divi_state', 'lit_mcp_wpsc_purged_at'];
private const CRON_HOOKS = ['lit_mcp_log_cleanup', 'lit_mcp_wpsc_trailing_purge'];
private const TRANSIENTS = ['lit_mcp_endpoint_check', 'lit_mcp_divi_state', 'lit_mcp_wpfc_purged_at'];
private const CRON_HOOKS = ['lit_mcp_log_cleanup', 'lit_mcp_wpfc_trailing_purge'];
private const TABLE = 'lit_mcp_log';
/** Screen option "Einträge pro Seite" of the Protokoll tab (Admin\LogTab::PER_PAGE_OPTION): user meta. */
@@ -1,56 +1,38 @@
#!/usr/bin/env bash
# End-to-end checks of the WP Super Cache profile of the compatibility layer (plan 2026-11-06-kompatibilitaetsschicht,
# 2.5 and 2.6): status, check route /wpsc-check with the purge probe (cache off and on), the purge after a write of
# this plugin without wp_update_post (terms), the whole-cache purge after a setting, its throttle and the trailing
# purge by cron. Runs INSIDE the DDEV web container:
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/super-cache-smoke.sh
# End-to-end checks of the WP Fastest Cache profile of the compatibility layer: status, check route /wpfc-check with
# the purge probe (cache off and on), the purge after a write of this plugin without wp_update_post (terms), the
# whole-cache purge after a setting, its throttle, the trailing purge by cron and lit/cache-clear. Runs INSIDE the
# DDEV web container:
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/fastest-cache-smoke.sh
#
# SIDE EFFECTS, all undone by the EXIT trap and proven at the end (verify_restored): activating WP Super Cache edits
# wp-config.php (WP_CACHE, WPCACHEHOME), creates wp-content/advanced-cache.php and wp-content/wp-cache-config.php,
# fills wp-content/cache/ and schedules wp_cache_* cron events and options. The script saves wp-config.php (and the
# files named in SAVED_FILES, if they exist) first, deactivates the plugin and writes the saved state back, byte for byte (sha256).
# It uses the plain PHP cache mode ($wp_cache_mod_rewrite = 0): no .htaccess rewrite rules are ever written; the run
# refuses to start if the cache directory is not empty, WP Super Cache is already active or WP_CACHE is already set.
# SIDE EFFECTS, all undone by the EXIT trap and proven at the end (verify_restored): WP Fastest Cache is activated if
# it was not, its option WpFastestCache is switched (only wpFastestCacheStatus, nothing that writes .htaccess) and set
# back byte for byte, a test post and two categories are created and deleted. Cache files of the site are emptied by
# the whole-cache purges – that is what the run checks; WPFC rebuilds them on the next requests.
set -euo pipefail
cd /var/www/html
# shellcheck source=lib.sh
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
ROOT=/var/www/html
CACHE_DIR=$ROOT/wp-content/cache
SUPERCACHE_DIR=$CACHE_DIR/supercache
WP_CONFIG=$ROOT/wp-config.php
CACHE_ALL=$ROOT/wp-content/cache/all
HTACCESS=$ROOT/.htaccess
SAVED_FILES=(wp-content/advanced-cache.php wp-content/wp-cache-config.php .htaccess)
CRON_PATTERN='wp_cache|wpsc|lit_mcp_wpsc'
OPTION_PATTERN='wpsc|wp_super_cache|wp_cache|super_cache|lit_mcp_wpsc'
TRAILING_HOOK=lit_mcp_wpsc_trailing_purge
PURGED_AT=lit_mcp_wpsc_purged_at
CRON_PATTERN='lit_mcp_wpfc'
OPTION_PATTERN='lit_mcp_wpfc'
TRAILING_HOOK=lit_mcp_wpfc_trailing_purge
PURGED_AT=lit_mcp_wpfc_purged_at
BASE=http://localhost
CACHE_ON='{"wpFastestCacheStatus":"on"}'
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
PREFIX=$(wp db prefix)
# --- Guards: nothing is changed before all of them pass ------------------------------------------------------------
wp plugin is-active wp-super-cache 2>/dev/null && fail "WP Super Cache is already active; this smoke expects it inactive"
grep -Eq "^\s*(define\s*\(\s*['\"]WP_CACHE['\"]|const\s+WP_CACHE\s*=)" "$WP_CONFIG" && fail "wp-config.php already sets WP_CACHE"
[ -d "$CACHE_DIR" ] && [ -n "$(find "$CACHE_DIR" -mindepth 1 -print -quit)" ] && fail "$CACHE_DIR is not empty; refusing to touch a cache"
# --- Snapshot of everything the run can change ---------------------------------------------------------------------
BACKUP=$(mktemp -d)
# --- Guards and snapshot of everything the run can change ----------------------------------------------------------
wp plugin is-installed wp-fastest-cache || fail "WP Fastest Cache is not installed"
sums() { sha256sum "$@" 2>/dev/null | awk '{print $1}'; }
CONFIG_SUM_BEFORE=$(sums "$WP_CONFIG")
CONFIG_STAT_BEFORE=$(stat -c '%a %U:%G' "$WP_CONFIG")
# cp -a, not -p: wp-config.php carries an ACL entry; WP Super Cache replaces the file (new inode) and loses it, and
# only cp -a writes it back with the content.
cp -a "$WP_CONFIG" "$BACKUP/wp-config.php"
[ "$(sums "$BACKUP/wp-config.php")" = "$CONFIG_SUM_BEFORE" ] || fail "could not save wp-config.php"
for f in "${SAVED_FILES[@]}"; do
if [ -e "$ROOT/$f" ]; then cp -a "$ROOT/$f" "$BACKUP/$(basename "$f")"; echo "$(sums "$ROOT/$f")" > "$BACKUP/$(basename "$f").sum"; fi
done
WPFC_WAS_ACTIVE=no; wp plugin is-active wp-fastest-cache 2>/dev/null && WPFC_WAS_ACTIVE=yes
WPFC_OPTION_BEFORE=$(wp option get WpFastestCache 2>/dev/null || echo __absent__)
HTACCESS_BEFORE=absent; [ -e "$HTACCESS" ] && HTACCESS_BEFORE=$(sums "$HTACCESS")
CACHE_DIR_EXISTED=no; CACHE_DIR_MODE=775
if [ -d "$CACHE_DIR" ]; then CACHE_DIR_EXISTED=yes; CACHE_DIR_MODE=$(stat -c '%a' "$CACHE_DIR"); fi
list_hooks() { wp cron event list --fields=hook --format=csv | tail -n +2 | grep -E "$CRON_PATTERN" | sort -u || true; }
list_options() { wp db query "SELECT option_name FROM ${PREFIX}options WHERE option_name REGEXP '$OPTION_PATTERN' ORDER BY option_name" --skip-column-names; }
HOOKS_BEFORE=$(list_hooks)
@@ -60,41 +42,20 @@ ORIGINAL_DESCRIPTION=$(wp option get blogdescription)
CREATED_POSTS=()
CREATED_TERMS=()
# --- Restore ---------------------------------------------------------------------------------------------------------
# A saved file is written back atomically: a copy next to the target (same file system), then mv – a run killed in
# the middle never leaves a half-written wp-config.php.
restore_file() { # $1 = path relative to ROOT
local name tmp; name=$(basename "$1")
if [ -e "$BACKUP/$name" ]; then
tmp=$(mktemp "$ROOT/$(dirname "$1")/.$name.restore.XXXXXX") \
&& cp -a "$BACKUP/$name" "$tmp" && mv -f "$tmp" "$ROOT/$1" || { rm -f "$tmp"; return 1; }
else
rm -f "$ROOT/$1"
fi
}
restore() {
set +e
local id term hook option
for id in "${CREATED_POSTS[@]}"; do wp post delete "$id" --force >/dev/null 2>&1; done
for term in "${CREATED_TERMS[@]}"; do wp term delete category "$term" >/dev/null 2>&1; done
wp option update blogdescription "$ORIGINAL_DESCRIPTION" >/dev/null 2>&1
wp option delete lit_mcp_wpsc_check_report lit_mcp_wpsc_releases lit_mcp_wpsc_check_lock >/dev/null 2>&1
wp transient delete "$PURGED_AT" >/dev/null 2>&1
cleanup_app_passwords_for "$ADMIN_LOGIN"
# Deactivation lets WP Super Cache undo its own edits (WP_CACHE, WPCACHEHOME, advanced-cache.php, cache files) ...
wp plugin is-active wp-super-cache >/dev/null 2>&1 && wp plugin deactivate wp-super-cache >/dev/null 2>&1
# ... and what it leaves, or does differently, is set back from the saved state.
restore_file wp-config.php
for id in "${SAVED_FILES[@]}"; do restore_file "$id"; done
[ -d "$CACHE_DIR" ] && find "$CACHE_DIR" -mindepth 1 -delete
if [ "$CACHE_DIR_EXISTED" = no ]; then
rmdir "$CACHE_DIR" 2>/dev/null
if [ "$WPFC_OPTION_BEFORE" = __absent__ ]; then
wp option delete WpFastestCache >/dev/null 2>&1
else
# Deactivation removes the whole cache directory; mkdir -m keeps the inherited ACL of its parent.
[ -d "$CACHE_DIR" ] || mkdir -m "$CACHE_DIR_MODE" "$CACHE_DIR"
wp option update WpFastestCache "$WPFC_OPTION_BEFORE" >/dev/null 2>&1
fi
# The profile's cron event and every option or event the plugin added.
[ "$WPFC_WAS_ACTIVE" = no ] && wp plugin deactivate wp-fastest-cache >/dev/null 2>&1
for hook in $(list_hooks); do
grep -qx "$hook" <<<"$HOOKS_BEFORE" || wp cron event delete "$hook" >/dev/null 2>&1
done
@@ -106,30 +67,11 @@ restore() {
# Prints the proof; returns non-zero on any difference (used in the trap, so it must not exit).
verify_restored() {
local ok=0 name after
after=$(sums "$WP_CONFIG")
if [ "$after" = "$CONFIG_SUM_BEFORE" ]; then pass "restored: wp-config.php sha256 $CONFIG_SUM_BEFORE (before) == $after (after)"
else echo "FAIL: wp-config.php sha256 $CONFIG_SUM_BEFORE (before) != $after (after)"; ok=1; fi
[ "$(stat -c '%a %U:%G' "$WP_CONFIG")" = "$CONFIG_STAT_BEFORE" ] \
|| { echo "FAIL: wp-config.php mode/owner changed: $CONFIG_STAT_BEFORE -> $(stat -c '%a %U:%G' "$WP_CONFIG")"; ok=1; }
for f in "${SAVED_FILES[@]}"; do
name=$(basename "$f")
if [ -e "$BACKUP/$name" ]; then
[ "$(sums "$ROOT/$f")" = "$(cat "$BACKUP/$name.sum")" ] || { echo "FAIL: $f differs from the saved one"; ok=1; }
else
[ ! -e "$ROOT/$f" ] || { echo "FAIL: $f was absent before and exists now"; ok=1; }
fi
done
[ "$ok" = 0 ] && pass "restored: advanced-cache.php and wp-cache-config.php as before (absent: $([ -e "$BACKUP/advanced-cache.php" ] && echo no || echo yes))"
local htaccess_after=absent; [ -e "$HTACCESS" ] && htaccess_after=$(sums "$HTACCESS")
local ok=0 htaccess_after=absent
[ "$(wp option get WpFastestCache 2>/dev/null || echo __absent__)" = "$WPFC_OPTION_BEFORE" ] \
|| { echo "FAIL: option WpFastestCache differs"; ok=1; }
[ -e "$HTACCESS" ] && htaccess_after=$(sums "$HTACCESS")
[ "$htaccess_after" = "$HTACCESS_BEFORE" ] || { echo "FAIL: .htaccess changed ($HTACCESS_BEFORE -> $htaccess_after)"; ok=1; }
if [ "$CACHE_DIR_EXISTED" = yes ]; then
[ -d "$CACHE_DIR" ] && [ -z "$(find "$CACHE_DIR" -mindepth 1 -print -quit)" ] && [ "$(stat -c '%a' "$CACHE_DIR")" = "$CACHE_DIR_MODE" ] \
|| { echo "FAIL: cache directory not empty, gone or its mode changed"; ok=1; }
else
[ ! -e "$CACHE_DIR" ] || { echo "FAIL: cache directory was absent before and exists now"; ok=1; }
fi
wp plugin is-active wp-super-cache 2>/dev/null && { echo "FAIL: WP Super Cache is still active"; ok=1; }
[ "$(wp option get active_plugins --format=json)" = "$ACTIVE_PLUGINS_BEFORE" ] || { echo "FAIL: active_plugins differs"; ok=1; }
[ "$(list_hooks)" = "$HOOKS_BEFORE" ] || { echo "FAIL: cron events differ: $(list_hooks | tr '\n' ' ')"; ok=1; }
[ "$(list_options)" = "$OPTIONS_BEFORE" ] || { echo "FAIL: options differ: $(list_options | tr '\n' ' ')"; ok=1; }
@@ -137,8 +79,9 @@ verify_restored() {
[ "$(wp option get blogdescription)" = "$ORIGINAL_DESCRIPTION" ] || { echo "FAIL: blogdescription not restored"; ok=1; }
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] \
|| { echo "FAIL: a probe page is left"; ok=1; }
[ "$(wp post list --post_type=post --post_status=any --name=lit-wpsc-smoke --format=count)" = 0 ] \
[ "$(wp post list --post_type=post --post_status=any --name=lit-wpfc-smoke --format=count)" = 0 ] \
|| { echo "FAIL: the test post is left"; ok=1; }
[ "$ok" = 0 ] && pass "restored: WP Fastest Cache active=$WPFC_WAS_ACTIVE with its option as before, .htaccess untouched"
return "$ok"
}
@@ -146,7 +89,6 @@ on_exit() {
local rc=$1
restore
verify_restored || rc=1
rm -rf "$BACKUP"
guard_exit "$rc" || exit $?
exit "$rc"
}
@@ -154,11 +96,11 @@ trap 'on_exit $?' EXIT
trap 'exit 130' INT TERM HUP
# --- Helpers ---------------------------------------------------------------------------------------------------------
wpsc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpsc")'; }
wpfc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpfc")'; }
stored_option() { wp option get "$1" --format=json 2>/dev/null || true; }
event_count() { wp cron event list --hook="$TRAILING_HOOK" --format=count 2>/dev/null || echo 0; }
# The cache file of the test post as WP Super Cache keeps it (index.html; a ".needs-rebuild" copy is a purged one).
cache_file() { find "$SUPERCACHE_DIR" -type f -path '*lit-wpsc-smoke*' -name index.html 2>/dev/null | head -1; }
# The cache file of the test post where WP Fastest Cache keeps it (a purge moves the directory to cache/tmpWpfc).
cache_file() { local f="$CACHE_ALL${POST_PATH%/}/index.html"; [ -f "$f" ] && echo "$f" || true; }
wait_for() { # $1 = description, rest = command; polls up to 15 s
local what=$1 i; shift
for i in $(seq 1 15); do "$@" && return 0; sleep 1; done
@@ -166,97 +108,78 @@ wait_for() { # $1 = description, rest = command; polls up to 15 s
}
cache_file_exists() { [ -n "$(cache_file)" ]; }
cache_file_gone() { [ -z "$(cache_file)" ]; }
front_page_by_wpsc() { [[ "$(curl -s -H "Host: $HOST" "$BASE/")" == *WP-Super-Cache* ]]; }
# One public request for the test post; the cache file appears at the end of the request.
fetch_post() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $HOST" "$BASE$POST_PATH")
[ "$code" = 200 ] || fail "GET $POST_PATH answered $code"
}
# Requests the test post until WP Super Cache has written its file (the first request after a purge or a new post
# is not always the one that is stored).
# Requests the test post until WP Fastest Cache has written its file.
fill_cache() {
local i
for i in $(seq 1 10); do
fetch_post
sleep 1
cache_file_exists && return 0
sleep 1
done
echo "--- response of $POST_PATH (selected headers) and the cache tree:"
curl -s -i -H "Host: $HOST" "$BASE$POST_PATH" | grep -iE '^(HTTP|cache-control|set-cookie)|WP Super Cache|Page not cached' | head -10
find "$CACHE_DIR" | head -20
echo "--- end of the response of $POST_PATH and the cache tree:"
curl -s -H "Host: $HOST" "$BASE$POST_PATH" | tail -c 300
find "$CACHE_ALL" -maxdepth 4 | head -20
fail "timeout: cache file of the test post"
}
config_value() { grep -E "^\s*\\\$$1\s*=" "$ROOT/wp-content/wp-cache-config.php" | head -1; }
# --- 0. Activate and look at what WP Super Cache did ------------------------------------------------------------------
wp plugin activate wp-super-cache >/dev/null 2>&1 || fail "could not activate WP Super Cache"
grep -q "define( *'WP_CACHE', *true *)" "$WP_CONFIG" || fail "activation did not set WP_CACHE (the run cannot cache)"
[ -e "$ROOT/wp-content/advanced-cache.php" ] || fail "activation did not create advanced-cache.php"
# WP Super Cache keys its files by the host of the request: MCP calls must carry the host the page was cached under.
MCP_HOST=$HOST
# --- 0. Active, cache switched off -----------------------------------------------------------------------------------
[ "$WPFC_WAS_ACTIVE" = yes ] || wp plugin activate wp-fastest-cache >/dev/null 2>&1 || fail "could not activate WP Fastest Cache"
wp option update WpFastestCache '{}' >/dev/null
connect "$ADMIN_LOGIN"
# 1. Status
wpsc_component | jq -e '.active == true and .version == "3.1.5" and .problems == []' >/dev/null \
|| fail "site status must show WP Super Cache 3.1.5 active without problems: $(wpsc_component)"
pass "lit-site-status: WP Super Cache 3.1.5 active, no problems"
wpfc_component | jq -e '.active == true and .version == "1.5.5" and .problems == []' >/dev/null \
|| fail "site status must show WP Fastest Cache 1.5.5 active without problems: $(wpfc_component)"
pass "lit-site-status: WP Fastest Cache 1.5.5 active, no problems"
# 2. Check route with the cache still switched off: the probe step is green and says "not checked"
# 2. Check route with the cache switched off: the probe step is green and says "not checked"
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
run_check() {
local headers
headers=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/wpsc-check")
headers=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/wpfc-check")
echo "$headers" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $headers"
echo "$headers" | grep -qi '^location:.*tab=compatibility.*lit_mcp_notice=compat_checked' || fail "redirect to the tab: $headers"
}
[ "$(wp eval 'echo (int) ! empty($GLOBALS["cache_enabled"]);')" = 0 ] || fail "the cache must be off after activation"
run_check
REPORT=$(stored_option lit_mcp_wpsc_check_report)
REPORT=$(stored_option lit_mcp_wpfc_check_report)
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("Cache ist ausgeschaltet, nicht geprüft")))' >/dev/null \
|| fail "cache off: the report must be green and the probe must say it did not check: $REPORT"
pass "/wpsc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
pass "/wpfc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
# 3. Switch the cache on in the plain PHP mode (no mod_rewrite, no .htaccess)
wp eval 'wp_cache_enable();' >/dev/null 2>&1 || fail "wp_cache_enable failed"
# PHP-FPM's opcache re-reads a changed wp-cache-config.php at most every 2 s: wait until a public request is served
# by WP Super Cache (its HTML comment), the state the checks below need.
wait_for "WP Super Cache serves public requests" front_page_by_wpsc
config_value cache_enabled | grep -q 'true' || fail "wp-cache-config.php: cache_enabled is not true: $(config_value cache_enabled)"
config_value wp_cache_mod_rewrite | grep -Eq '= *0;' || fail "mod_rewrite mode is on: $(config_value wp_cache_mod_rewrite)"
config_value super_cache_enabled | grep -q 'true' || fail "super_cache_enabled is not true: $(config_value super_cache_enabled)"
[ "$(config_value wp_cache_clear_on_post_edit | grep -Ec '= *0;')" = 1 ] || fail "clear_on_post_edit must be 0: $(config_value wp_cache_clear_on_post_edit)"
[ ! -e "$HTACCESS" ] || [ "$(sums "$HTACCESS")" = "$HTACCESS_BEFORE" ] || fail ".htaccess changed"
pass "cache on in the plain PHP mode (mod_rewrite 0, clear_on_post_edit 0), .htaccess untouched"
# 4. Check route with the cache on: the purge probe really runs
# 3. Cache on; the check route runs the real purge probe
wp option update WpFastestCache "$CACHE_ON" >/dev/null
run_check
REPORT=$(stored_option lit_mcp_wpsc_check_report)
REPORT=$(stored_option lit_mcp_wpfc_check_report)
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("nicht geprüft") | not))
and .version == "3.1.5"' >/dev/null || fail "cache on: the report must be green with a real probe step: $REPORT"
[ -z "$(stored_option lit_mcp_wpsc_check_lock)" ] || fail "the check lock must be released"
and .version == "1.5.5"' >/dev/null || fail "cache on: the report must be green with a real probe step: $REPORT"
[ -z "$(stored_option lit_mcp_wpfc_check_lock)" ] || fail "the check lock must be released"
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] || fail "the probe left a page behind"
[ "$(find "$SUPERCACHE_DIR" -path '*lit-mcp-check-*' -print -quit 2>/dev/null)" = "" ] || fail "the probe left a cache directory behind"
UNAUTH=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "$BASE/wp-json/lit-mcp/v1/wpsc-check")
[ "$(find "$CACHE_ALL" -maxdepth 3 -path '*lit-mcp-check-*' -print -quit 2>/dev/null)" = "" ] || fail "the probe left a cache directory behind"
UNAUTH=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "$BASE/wp-json/lit-mcp/v1/wpfc-check")
[ "$UNAUTH" = 401 ] || [ "$UNAUTH" = 403 ] || fail "anonymous check must be refused, got $UNAUTH"
pass "/wpsc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
pass "/wpfc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
# 5. A published test post, fetched until it is in the cache
CAT_A=$(wp term create category "LIT WPSC Smoke A" --porcelain); CREATED_TERMS+=("$CAT_A")
CAT_B=$(wp term create category "LIT WPSC Smoke B" --porcelain); CREATED_TERMS+=("$CAT_B")
POST_ID=$(wp post create --post_type=post --post_status=publish --post_title="LIT WPSC Smoke" --post_name=lit-wpsc-smoke \
--post_content="Testbeitrag fuer den Super-Cache-Smoke." --porcelain)
# 4. A published test post, fetched until it is in the cache
CAT_A=$(wp term create category "LIT WPFC Smoke A" --porcelain); CREATED_TERMS+=("$CAT_A")
CAT_B=$(wp term create category "LIT WPFC Smoke B" --porcelain); CREATED_TERMS+=("$CAT_B")
POST_ID=$(wp post create --post_type=post --post_status=publish --post_title="LIT WPFC Smoke" --post_name=lit-wpfc-smoke \
--post_content="Testbeitrag fuer den Fastest-Cache-Smoke." --porcelain)
CREATED_POSTS+=("$POST_ID")
POST_PATH=$(wp eval "echo wp_parse_url(get_permalink($POST_ID), PHP_URL_PATH);")
fill_cache
pass "test post $POST_ID cached: $(cache_file | sed "s#$CACHE_DIR/##")"
pass "test post $POST_ID cached: $(cache_file | sed "s#$ROOT/##")"
# 6. Control: a term change outside this plugin does not touch the cache (WP Super Cache never sees it) ...
# 5. Control: a term change outside this plugin does not touch the cache (WP Fastest Cache never sees it) ...
wp post term set "$POST_ID" category "$CAT_A" >/dev/null
sleep 2
cache_file_exists || fail "control: WP Super Cache itself purged on a plain term change; the next check proves nothing"
cache_file_exists || fail "control: WP Fastest Cache itself purged on a plain term change; the next check proves nothing"
# ... but the same kind of change through lit-content-update (no wp_update_post) empties the cache of the post.
REV=$(tool lit-content-get "{\"post_id\":$POST_ID}" | jq -r '.rev')
UPDATED=$(tool lit-content-update "{\"post_id\":$POST_ID,\"rev\":\"$REV\",\"terms\":{\"category\":[$CAT_B]}}")
@@ -265,10 +188,10 @@ wait_for "the cache file of the post is gone after lit-content-update" cache_fil
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
pass "terms-only lit-content-update empties the cache of the post (a plain term change does not), no trailing event"
# 7. Whole-cache purge after a setting; the second one inside 30 s is throttled and leaves exactly one cron event
# 6. Whole-cache purge after a setting; the second one inside 30 s is throttled and leaves exactly one cron event
fill_cache
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || fail "transient $PURGED_AT set before the first whole purge"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 1"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 1"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 1"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 1"
wait_for "the cache is empty after a setting change" cache_file_gone
wait_for "the purge time is stored" bash -c "[ -n \"\$(wp transient get $PURGED_AT 2>/dev/null)\" ]"
LAST=$(wp transient get "$PURGED_AT")
@@ -276,18 +199,18 @@ LAST=$(wp transient get "$PURGED_AT")
pass "setting change empties the whole cache (purge time stored, no trailing event)"
fill_cache
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 2"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 2"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 2"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 2"
wait_for "one trailing purge is scheduled" bash -c "[ \"\$(wp cron event list --hook=$TRAILING_HOOK --format=count)\" = 1 ]"
sleep 2
cache_file_exists || fail "throttled: the second whole purge ran inside the lock time"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 3"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 3"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 3"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 3"
sleep 3
[ "$(event_count)" = 1 ] || fail "exactly one trailing event must stay scheduled, found $(event_count)"
cache_file_exists || fail "throttled: the third whole purge ran inside the lock time"
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "throttled purges must not move the purge time"
pass "second and third whole purge inside 30 s: throttled (file stays, purge time unchanged), exactly one $TRAILING_HOOK event"
# 8. Run the trailing purge once the lock is over: the cache is emptied and nothing is scheduled any more
# 7. Run the trailing purge once the lock is over: the cache is emptied and nothing is scheduled any more
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"
@@ -295,7 +218,7 @@ 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"
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
# 8. 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")
@@ -339,6 +262,6 @@ pass "lit/cache-clear scope all inside 30 s: throttled, one trailing event, cron
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
restore
verify_restored || fail "the state before the run was not restored"
pass "cleanup: plugin inactive, test post, categories, options, transient and cron events gone"
pass "cleanup: test post, categories, options, transient and cron events gone"
finish
+2 -2
View File
@@ -6,7 +6,7 @@
# Requires jq and curl (both present in the DDEV web image).
# Mutual exclusion of ALL smokes (every one sources this file inside the same web container): two at once would
# share one site state – WP Super Cache edits wp-config.php, every smoke empties the test application passwords of the
# share one site state – WP Fastest Cache edits wp-config.php, every smoke empties the test application passwords of the
# admin (cleanup_app_passwords_for, harmless only because of this lock), Pods/Divi release options. The lock lives as
# long as the script (fd 9 closes with it); a smoke that runs a second one itself must not source this file twice.
exec 9>/tmp/lit-mcp-smoke.lock
@@ -22,7 +22,7 @@ ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
# Optional: the Host header MCP requests carry (default: curl's own, "localhost"). A cache plugin that keys its files
# by the request's host (WP Super Cache) needs the site's real host here (super-cache-smoke.sh sets it).
# by the request's host (WP Fastest Cache) needs the site's real host here (none of the current smokes needs it).
MCP_HOST=${MCP_HOST:-}
TOKEN=""
+2 -2
View File
@@ -173,11 +173,11 @@ final class CompatibilityTabTest extends TestCase
return (string) ob_get_clean();
}
/** One source for the ability name of the release and withdraw entries (SuperCacheListener reads it too). */
/** One source for the ability name of the release and withdraw entries (FastestCacheListener reads it too). */
public function testTheReleaseAbilityNameHasOneSource(): void
{
self::assertSame('lit/divi-release', CompatibilityTab::releaseAbility('divi'));
self::assertTrue(CompatibilityTab::isReleaseAbility(CompatibilityTab::releaseAbility('wpsc')));
self::assertTrue(CompatibilityTab::isReleaseAbility(CompatibilityTab::releaseAbility('wpfc')));
foreach (['lit/content-update', 'lit/-release', 'lit/release', 'lit/divi-releases'] as $other) {
self::assertFalse(CompatibilityTab::isReleaseAbility($other), $other);
}
@@ -245,7 +245,7 @@ final class ThrowawayPostTest extends TestCase
self::assertSame([], $this->throwaway()->removeOrphans('page'));
}
/** A probe killed while its page was private (WP Super Cache) leaves a private page, not a draft. */
/** Private entries count too (the old WP Super Cache probe passed through private). */
public function testShortStatusListCoversAProbeKilledWhilePrivate(): void
{
$query = null;
@@ -2,15 +2,15 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
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\Integrations\FastestCache\CacheClearAbility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Error;
@@ -51,20 +51,20 @@ final class CacheClearAbilityTest extends TestCase
protected function tearDown(): void
{
unset($GLOBALS['cache_enabled']);
unset($GLOBALS['wp_fastest_cache_options']);
parent::tearDown();
}
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
{
$purger = new SuperCachePurger(
static fn (SuperCacheFeature $feature): bool => $usable,
$purger = new FastestCachePurger(
static fn (FastestCacheFeature $feature): bool => $usable,
static fn (): bool => $cacheOn,
function (int $id): void {
$this->purges[] = 'post:' . $id;
},
function (int $blog): void {
$this->purges[] = 'all:' . $blog;
function (): void {
$this->purges[] = 'all';
}
);
@@ -80,7 +80,7 @@ final class CacheClearAbilityTest extends TestCase
self::assertFalse($ability->isDirect());
self::assertSame(Annotations::write(true), $ability->annotations());
self::assertStringContainsString('confirm: true', $ability->description());
self::assertStringContainsString('WP Super Cache', $ability->description());
self::assertStringContainsString('WP Fastest Cache', $ability->description());
$schema = $ability->inputSchema();
self::assertSame(['scope'], $schema['required']);
@@ -95,8 +95,8 @@ final class CacheClearAbilityTest extends TestCase
$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(['wpfc-purge-all'], $ability->requirements(['scope' => 'all']));
self::assertSame(['wpfc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
self::assertSame([], $ability->requirements(['scope' => 'x']));
self::assertSame([], $ability->requirements([]));
}
@@ -165,7 +165,7 @@ final class CacheClearAbilityTest extends TestCase
public function testTheDryRunClearsNothingAndReportsTheState(): void
{
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
$this->lastPurge = time() - 5;
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
@@ -202,17 +202,16 @@ final class CacheClearAbilityTest extends TestCase
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']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => '7']);
}
public function testADraftHasNothingToClear(): void
/** WPFC finds the address of any post (a trashed one too), so no status is skipped. */
public function testADraftIsClearedToo(): void
{
$GLOBALS['cache_enabled'] = true;
$result = $this->ability()->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
$result = $this->ability(new FakeChangeLog(2))->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
self::assertSame([], $this->purges);
self::assertSame('nothing', $result['outcome']);
self::assertStringContainsString('Entwurf', $result['summary']);
self::assertSame(['post:8'], $this->purges);
self::assertSame('purged', $result['outcome']);
}
public function testConfirmClearsTheWholeCacheAtOnce(): void
@@ -221,9 +220,9 @@ final class CacheClearAbilityTest extends TestCase
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
self::assertSame('purged', $result['outcome']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => 'all']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => 'all']);
}
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
@@ -233,7 +232,7 @@ final class CacheClearAbilityTest extends TestCase
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
self::assertSame([], $this->purges);
self::assertSame([SuperCachePurger::TRAILING_HOOK], $this->scheduled);
self::assertSame([FastestCachePurger::TRAILING_HOOK], $this->scheduled);
self::assertSame('throttled', $result['outcome']);
self::assertStringContainsString('Nachlauf', $result['summary']);
}
@@ -271,7 +270,7 @@ final class CacheClearAbilityTest extends TestCase
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.',
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
$result->get_error_message()
);
self::assertSame([], $this->purges);
@@ -2,33 +2,33 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheCheckStepsTest extends TestCase
final class FastestCacheCheckStepsTest extends TestCase
{
public function testTheProbeIsSkippedWithARedManifestAndNotRun(): void
{
$ran = false;
$steps = SuperCacheCheckSteps::extra(static function () use (&$ran): array {
$steps = FastestCacheCheckSteps::extra(static function () use (&$ran): array {
$ran = true;
return [];
})(false);
self::assertCount(1, $steps);
self::assertSame(SuperCacheCheckSteps::PROBE, $steps[0]->key);
self::assertSame(FastestCacheCheckSteps::PROBE, $steps[0]->key);
self::assertTrue($steps[0]->skipped);
self::assertFalse($ran);
}
public function testTheStepCarriesTheProbeProblems(): void
{
$steps = SuperCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
$steps = FastestCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
self::assertSame(['rot'], $steps[0]->problems);
self::assertFalse($steps[0]->skipped);
@@ -38,7 +38,7 @@ final class SuperCacheCheckStepsTest extends TestCase
/** A switched-off cache is no red step: a skipped one would lock the purge of a site that simply caches nothing. */
public function testWithoutAnActiveCacheTheStepIsGreenAndSaysSo(): void
{
$steps = SuperCacheCheckSteps::extra(static fn (): ?array => null)(true);
$steps = FastestCacheCheckSteps::extra(static fn (): ?array => null)(true);
self::assertTrue($steps[0]->green());
self::assertStringContainsString('Cache ist ausgeschaltet, nicht geprüft', $steps[0]->label());
@@ -46,11 +46,11 @@ final class SuperCacheCheckStepsTest extends TestCase
public function testTheLabelOfAnOldStepComesFromTheKey(): void
{
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', SuperCacheCheckSteps::label(new CheckStep(SuperCacheCheckSteps::PROBE, [])));
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', FastestCacheCheckSteps::label(new CheckStep(FastestCacheCheckSteps::PROBE, [])));
self::assertSame(
SuperCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
SuperCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
FastestCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
FastestCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
);
self::assertSame('other', SuperCacheCheckSteps::label(new CheckStep('other', [])));
self::assertSame('other', FastestCacheCheckSteps::label(new CheckStep('other', [])));
}
}
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class FastestCacheCompatibilityTest extends TestCase
{
public function testDescribesFastestCacheAsALockOnFailurePlugin(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertSame('wpfc', $profile->id);
self::assertSame('WP Fastest Cache', $profile->label);
self::assertSame(Tier::LockOnFailure, $profile->tier);
self::assertSame(['1.5.5'], $profile->testedVersions);
self::assertNull($profile->minVersion);
self::assertSame(FastestCacheFeature::cases(), $profile->capabilities);
}
/** Argument counts are those the purger and the probe pass. */
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
{
$manifest = FastestCacheCompatibility::profile()->manifests[0];
$calls = [];
foreach ($manifest->calls as $call) {
$calls[$call->symbol] = [$call->args, $call->features()];
}
self::assertSame([
'wpfc_clear_post_cache_by_id' => [1, [FastestCacheFeature::PurgePost]],
'wpfc_clear_all_cache' => [0, [FastestCacheFeature::PurgeAll]],
'WpFastestCache::singleDeleteCache' => [3, [FastestCacheFeature::PurgePost]],
'WpFastestCache::getWpContentDir' => [1, [FastestCacheFeature::PurgePost]],
], $calls);
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
}
/** The instance that hooks the purge actions and the decoded options with the cache switch. */
public function testTheGlobalsTheFunctionsReadAreRequired(): void
{
$manifest = FastestCacheCompatibility::profile()->manifests[0];
self::assertSame(['WPFC_MAIN_PATH'], array_map(static fn ($c) => $c->name, $manifest->constants));
$globals = [];
foreach ($manifest->globals as $global) {
$globals[$global->name] = $global->features();
}
self::assertSame(
['wp_fastest_cache' => FastestCacheFeature::cases(), 'wp_fastest_cache_options' => FastestCacheFeature::cases()],
$globals
);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testActiveAndVersionFollowTheMainPathConstantAndThePluginHeader(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertFalse(($profile->active)());
self::assertNull(($profile->version)());
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
Functions\expect('get_file_data')
->once()
->with('/plugins/wp-fastest-cache/wpFastestCache.php', ['Version' => 'Version'])
->andReturn(['Version' => '1.5.5']);
self::assertTrue(($profile->active)());
self::assertSame('1.5.5', ($profile->version)());
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
{
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
Functions\when('get_file_data')->justReturn(['Version' => '']);
self::assertNull((FastestCacheCompatibility::profile()->version)());
}
public function testTheCheckHasTheProbeAsExtraStep(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertNotNull($profile->extraSteps);
$steps = ($profile->extraSteps)(false);
self::assertSame([FastestCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
self::assertTrue($steps[0]->skipped);
self::assertNotNull($profile->stepLabel);
self::assertSame(
'Leeren-Test an einer Wegwerf-Seite im Cache',
($profile->stepLabel)(new CheckStep(FastestCacheCheckSteps::PROBE, []))
);
}
public function testTheCacheIsOnlyEnabledWhileTheSwitchInTheDecodedOptionsIsOn(): void
{
unset($GLOBALS['wp_fastest_cache_options']);
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
$GLOBALS['wp_fastest_cache_options'] = [];
self::assertFalse(FastestCacheCompatibility::cacheEnabled(), 'WPFC stores [] when the option is missing');
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheMinifyHtml' => 'on'];
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
self::assertTrue(FastestCacheCompatibility::cacheEnabled());
unset($GLOBALS['wp_fastest_cache_options']);
}
public function testAllProfilesCoexistWithoutIdCollision(): void
{
$compatibility = new Compatibility(
[DiviProfile::profile(), PodsCompatibility::profile(), FastestCacheCompatibility::profile()]
);
self::assertSame(['divi', 'pods', 'wpfc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
self::assertSame(FastestCacheFeature::PurgeAll, $compatibility->capability('wpfc-purge-all'));
}
}
@@ -2,25 +2,25 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheFeatureTest extends TestCase
final class FastestCacheFeatureTest extends TestCase
{
public function testEveryFeatureIsAWritingCapabilityWithAPrefixedId(): void
{
foreach (SuperCacheFeature::cases() as $feature) {
foreach (FastestCacheFeature::cases() as $feature) {
self::assertInstanceOf(Capability::class, $feature);
self::assertSame($feature->value, $feature->id());
self::assertNotSame('', $feature->label());
self::assertTrue($feature->isWrite());
}
self::assertSame(
['wpsc-purge-post', 'wpsc-purge-all'],
array_map(static fn (SuperCacheFeature $f): string => $f->id(), SuperCacheFeature::cases())
['wpfc-purge-post', 'wpfc-purge-all'],
array_map(static fn (FastestCacheFeature $f): string => $f->id(), FastestCacheFeature::cases())
);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheIntegration;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
final class FastestCacheIntegrationTest extends TestCase
{
private static function purger(): FastestCachePurger
{
return new FastestCachePurger(static fn (): bool => true);
}
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
self::assertInstanceOf(Integration::class, $integration);
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
self::assertSame('wpfc', $integration->id());
self::assertSame('WP Fastest Cache', $integration->label());
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
self::assertEquals([FastestCacheCompatibility::profile()], $integration->compatibilities());
}
/**
* The same constant the profile's active() reads: WP Fastest Cache defines it when its plugin file loads.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testIsAvailableOnlyWithFastestCache(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
self::assertFalse($integration->isAvailable());
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
self::assertTrue($integration->isAvailable());
self::assertTrue((FastestCacheCompatibility::profile()->active)());
}
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
$integration->boot();
self::assertInstanceOf(Bootable::class, $integration);
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
}
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
{
Functions\expect('get_posts')->never();
Functions\when('current_user_can')->justReturn(true);
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
(new FastestCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
Functions\when('current_user_can')->justReturn(false);
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
$this->addToAssertionCount(1);
}
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
{
Functions\when('current_user_can')->justReturn(true);
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
self::assertSame('page', $args['post_type']);
self::assertSame(['draft', 'private'], $args['post_status']);
return [];
});
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
}
}
@@ -2,19 +2,19 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Post;
final class SuperCacheListenerTest extends TestCase
final class FastestCacheListenerTest extends TestCase
{
/** @var list<string> */
private array $purges = [];
@@ -56,7 +56,7 @@ final class SuperCacheListenerTest extends TestCase
{
ini_set('error_log', (string) $this->previousErrorLog);
unlink($this->errorLog);
unset($GLOBALS['cache_enabled']);
unset($GLOBALS['wp_fastest_cache_options']);
parent::tearDown();
}
@@ -66,17 +66,17 @@ final class SuperCacheListenerTest extends TestCase
bool $usablePost = true,
bool $usableAll = true,
?Closure $postIdFor = null
): SuperCacheListener {
return new SuperCacheListener(
static fn (SuperCacheFeature $feature): bool => $feature === SuperCacheFeature::PurgePost
): FastestCacheListener {
return new FastestCacheListener(
static fn (FastestCacheFeature $feature): bool => $feature === FastestCacheFeature::PurgePost
? $usablePost
: $usableAll,
static fn (): bool => $cacheOn,
function (int $id): void {
$this->purges[] = 'post:' . $id;
},
function (int $blog): void {
$this->purges[] = 'all:' . $blog;
function (): void {
$this->purges[] = 'all';
},
$postIdFor ?? static fn (string $type, string $id): ?int => in_array($type, ['page', 'post'], true)
? (int) $id
@@ -120,7 +120,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/content-update', 'post', '14', 7);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
public function testThePurgeIsDoneOncePerFlush(): void
@@ -143,7 +143,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
$listener->onChange('lit/content-update', 'page', '14', 7);
$listener->flush();
self::assertSame(['all:0', 'post:14'], $this->purges);
self::assertSame(['all', 'post:14'], $this->purges);
}
/** @dataProvider siteWideChanges */
@@ -153,7 +153,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange($ability, $type, $id, null);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** @return array<string, array{string, string, string}> */
@@ -175,7 +175,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/post-trash', 'post', '99', 1);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** One whole-cache change among single posts: still one purge, no single ones on top. */
@@ -186,7 +186,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/wp-settings-update', 'option', 'a', 2);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** Release and withdraw entries of the check workflow change nothing on the site (shared ability name). */
@@ -204,8 +204,8 @@ final class SuperCacheListenerTest extends TestCase
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
{
$listener = $this->listener();
$listener->onChange('lit/cache-clear', 'wpsc', 'all', 1);
$listener->onChange('lit/cache-clear', 'wpsc', '12', 2);
$listener->onChange('lit/cache-clear', 'wpfc', 'all', 1);
$listener->onChange('lit/cache-clear', 'wpfc', '12', 2);
$listener->flush();
self::assertSame([], $this->purges);
@@ -233,7 +233,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
self::assertSame([], $this->purges);
self::assertSame(1, substr_count($this->logged(), 'wpsc-purge-post'));
self::assertSame(1, substr_count($this->logged(), 'wpfc-purge-post'));
}
/** The capabilities lock separately: a gone purge-post does not stop the whole-cache purge. */
@@ -244,7 +244,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
public function testALockedWholeCachePurgeSchedulesNothing(): void
@@ -258,18 +258,6 @@ final class SuperCacheListenerTest extends TestCase
self::assertSame([], $this->scheduled);
}
public function testOnAMultisiteTheCurrentBlogIsPurged(): void
{
Functions\when('is_multisite')->justReturn(true);
Functions\when('get_current_blog_id')->justReturn(3);
$listener = $this->listener();
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame(['all:3'], $this->purges);
}
public function testAWholeCachePurgeStampsTheTimeForThirtySeconds(): void
{
$listener = $this->listener();
@@ -277,7 +265,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
self::assertCount(1, $this->stamped);
self::assertSame('lit_mcp_wpsc_purged_at', $this->stamped[0][0]);
self::assertSame('lit_mcp_wpfc_purged_at', $this->stamped[0][0]);
self::assertEqualsWithDelta(time(), $this->stamped[0][1], 2);
self::assertSame(30, $this->stamped[0][2]);
}
@@ -305,7 +293,7 @@ final class SuperCacheListenerTest extends TestCase
self::assertSame([], $this->purges);
self::assertSame([], $this->stamped);
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpsc_trailing_purge']], $this->scheduled);
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpfc_trailing_purge']], $this->scheduled);
}
/** Options-table transients come back as strings. */
@@ -337,7 +325,7 @@ final class SuperCacheListenerTest extends TestCase
$listener = $this->listener();
$listener->trailingPurge();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
self::assertCount(1, $this->stamped);
}
@@ -351,7 +339,7 @@ final class SuperCacheListenerTest extends TestCase
public function testAThrowingTrailingPurgeIsLogged(): void
{
$listener = new SuperCacheListener(
$listener = new FastestCacheListener(
static fn (): bool => true,
static fn (): bool => true,
null,
@@ -367,13 +355,13 @@ final class SuperCacheListenerTest extends TestCase
public function testAThrowingPurgeIsLoggedAndNotRethrown(): void
{
$listener = new SuperCacheListener(
$listener = new FastestCacheListener(
static fn (): bool => true,
static fn (): bool => true,
static function (): void {
throw new \RuntimeException('disk full');
},
static fn (int $blog): null => null,
static fn (): null => null,
static fn (): int => 12
);
@@ -389,37 +377,37 @@ final class SuperCacheListenerTest extends TestCase
$listener->register();
self::assertSame(10, has_action('lit_mcp_changed', [$listener, 'onChange']));
self::assertSame(10, has_action('lit_mcp_wpsc_trailing_purge', [$listener, 'trailingPurge']));
self::assertSame(10, has_action('lit_mcp_wpfc_trailing_purge', [$listener, 'trailingPurge']));
}
/** Plugin wires create(): without WP Super Cache nothing is usable, and with the cache off nothing is asked. */
/** Plugin wires create(): without WP Fastest Cache nothing is usable, and with the cache off nothing is asked. */
public function testCreateAsksTheCompatibilityOnlyWhenTheCacheIsOn(): void
{
$compatibility = new Compatibility([SuperCacheCompatibility::profile()]);
$listener = SuperCacheListener::create($compatibility);
$compatibility = new Compatibility([FastestCacheCompatibility::profile()]);
$listener = FastestCacheListener::create($compatibility);
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame('', $this->logged());
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertStringContainsString('wpsc-purge-all not usable', $this->logged());
self::assertStringContainsString('wpfc-purge-all not usable', $this->logged());
}
public function testTheDefaultCacheSwitchIsTheWpscGlobal(): void
public function testTheDefaultCacheSwitchIsTheStatusOfWpfc(): void
{
$post = new WP_Post();
Functions\when('get_post')->justReturn($post);
Functions\when('get_post_type_object')->justReturn((object) ['public' => true]);
$listener = new SuperCacheListener(static fn (): bool => true);
$listener = new FastestCacheListener(static fn (): bool => true);
$GLOBALS['cache_enabled'] = false;
$GLOBALS['wp_fastest_cache_options'] = (object) [];
$listener->onChange('lit/content-update', 'page', '12', 5);
$listener->flush();
self::assertSame('', $this->logged(), 'unset or false cache_enabled: silent, nothing purged');
self::assertSame('', $this->logged(), 'cache switched off: silent, nothing purged');
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
}
@@ -430,18 +418,18 @@ final class SuperCacheListenerTest extends TestCase
bool $public,
bool $exists
): void {
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
Functions\when('get_post_type_object')->justReturn((object) ['public' => $public]);
Functions\when('get_post')->justReturn($exists ? new WP_Post() : null);
$purged = [];
Functions\when('wp_cache_clear_cache')->alias(static function () use (&$purged): void {
Functions\when('wpfc_clear_all_cache')->alias(static function () use (&$purged): void {
$purged[] = 'all';
});
Functions\when('wp_cache_post_edit')->alias(static function () use (&$purged): void {
Functions\when('wpfc_clear_post_cache_by_id')->alias(static function () use (&$purged): void {
$purged[] = 'post';
});
$listener = new SuperCacheListener(static fn (): bool => true);
$listener = new FastestCacheListener(static fn (): bool => true);
$listener->onChange('lit/x', $type, $id, 1);
$listener->flush();
@@ -0,0 +1,421 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Integrations\FastestCache\FastestCacheProbe;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
use WP_Post;
final class FastestCacheProbeTest extends TestCase
{
private string $cacheRoot = '';
/** @var list<int> */
private array $deleted = [];
/** @var array<string, mixed>|null The wp_insert_post arguments. */
private ?array $inserted = null;
/** @var array<string, mixed> Globals the test changes (only those that existed). */
private array $savedGlobals = [];
private const GLOBALS_CHANGED = ['wp_fastest_cache', 'wp_fastest_cache_options', 'wpdb'];
protected function setUp(): void
{
parent::setUp();
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpfc-' . bin2hex(random_bytes(4)) . '/';
mkdir($this->cacheRoot . 'cache/all/', 0777, true);
$this->deleted = [];
$this->inserted = null;
$this->savedGlobals = [];
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $GLOBALS)) {
$this->savedGlobals[$name] = $GLOBALS[$name];
}
}
Functions\when('wp_insert_post')->alias(function (array $post): int {
$this->inserted = $post;
return 77;
});
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
self::assertTrue($force, 'never to the trash');
$this->deleted[] = $id;
return new WP_Post(['ID' => $id]);
});
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
}
protected function tearDown(): void
{
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $this->savedGlobals)) {
$GLOBALS[$name] = $this->savedGlobals[$name];
} else {
unset($GLOBALS[$name]);
}
}
self::removeTree($this->cacheRoot);
parent::tearDown();
}
private static function removeTree(string $path): void
{
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
if (in_array(basename($entry), ['.', '..'], true)) {
continue;
}
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
}
@rmdir($path);
}
private function pageDir(): string
{
return $this->cacheRoot . 'cache/all/lit-mcp-check-77/';
}
/**
* @param (Closure(int): void)|null $purge
* @param (Closure(int): ?string)|null $cacheDir
*/
private function probe(
?Closure $purge = null,
?Closure $cacheDir = null,
bool $enabled = true,
bool $hooked = true,
bool $slugSet = true
): FastestCacheProbe {
return new FastestCacheProbe(
static fn (): bool => $enabled,
static fn (): bool => $hooked,
$cacheDir ?? fn (int $id): string => $this->pageDir(),
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
static fn (int $id): bool => $slugSet
);
}
/** Captures what Log writes to the PHP error log while $run runs. */
private function logged(Closure $run): string
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$run();
return (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
}
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
{
$probe = $this->probe(function (int $id): void {
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
unlink($this->pageDir() . 'index.html');
});
self::assertSame([], $probe->run());
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertDirectoryExists($this->cacheRoot . 'cache/all/');
self::assertSame([77], $this->deleted);
}
public function testCreatesAMarkedDraftPage(): void
{
$this->probe()->run();
self::assertSame('page', $this->inserted['post_type']);
self::assertSame('draft', $this->inserted['post_status']);
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
}
/** WPFC moves the whole directory away (to cache/tmpWpfc). */
public function testADirectoryThePurgeMovedAwayIsGreen(): void
{
$problems = $this->probe(function (int $id): void {
rename($this->pageDir(), $this->cacheRoot . 'moved');
})->run();
self::assertSame([], $problems);
}
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static fn (int $id) => null)->run();
});
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Fastest Cache bestehen.'], $problems);
self::assertStringContainsString('lit-mcp-check-77', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testAForeignFileInThePageDirectoryIsLeftAlone(): void
{
$problems = $this->probe(function (int $id): void {
unlink($this->pageDir() . 'index.html');
touch($this->pageDir() . 'fremd.txt');
})->run();
self::assertSame([], $problems);
self::assertFileExists($this->pageDir() . 'fremd.txt');
}
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPFC kaputt');
})->run();
});
self::assertSame(['Fastest-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPFC kaputt', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
{
self::assertNull($this->probe(enabled: false)->run());
self::assertNull($this->inserted);
}
/** Without the hook wpfc_clear_post_cache_by_id does nothing: the purge of a post would silently fail. */
public function testAMissingPurgeHookIsAProblemWithoutAnyPage(): void
{
self::assertSame(
['WP Fastest Cache verarbeitet wpfc_clear_post_cache_by_id nicht mehr; das Leeren eines Beitrags bliebe wirkungslos.'],
$this->probe(hooked: false)->run()
);
self::assertNull($this->inserted);
}
public function testNothingToProbeWithoutACachePathForThePage(): void
{
self::assertNull($this->probe(cacheDir: static fn (int $id): ?string => null)->run());
self::assertSame([77], $this->deleted);
}
public function testARefusedSlugIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
{
$purged = false;
$problems = [];
$logged = $this->logged(function () use (&$problems, &$purged): void {
$problems = $this->probe(static function (int $id) use (&$purged): void {
$purged = true;
}, slugSet: false)->run();
});
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('77', $logged);
self::assertFalse($purged);
self::assertSame([77], $this->deleted);
}
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
{
$blocker = $this->cacheRoot . 'blocker';
file_put_contents($blocker, 'x');
$problems = [];
$logged = $this->logged(function () use (&$problems, $blocker): void {
$problems = $this->probe(cacheDir: static fn (int $id): string => $blocker . '/seite/')->run();
});
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('seite', $logged);
self::assertSame([77], $this->deleted);
}
// --- Defaults: the real WPFC and WordPress calls ----------------------------------------------------------------
/** Records calls; getWpContentDir like WPFC without WPML/multisite: content dir plus path. */
private function fakeWpfc(): object
{
return new class ($this->cacheRoot) {
/** @var list<array{mixed, mixed, mixed}> */
public array $calls = [];
public function __construct(private string $root)
{
}
public function singleDeleteCache(mixed $comment = false, mixed $post = false, mixed $parents = true): void
{
$this->calls[] = [$comment, $post, $parents];
}
public function getWpContentDir(string $path): string
{
return rtrim($this->root, '/') . $path;
}
};
}
public function testTheDefaultPurgeIsSingleDeleteCacheWithoutTheParentPages(): void
{
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
$probe = new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (int $id): bool => true
);
$this->logged(static function () use ($probe): void {
$probe->run();
});
self::assertSame([[false, 77, false]], $wpfc->calls);
}
public function testTheDefaultHookCheckAsksForTheMethodOfTheWpfcInstance(): void
{
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
Functions\expect('has_action')->twice()
->with('wpfc_clear_post_cache_by_id', [$wpfc, 'singleDeleteCache'])
->andReturn(10, false);
$probe = new FastestCacheProbe(
static fn (): bool => true,
null,
static fn (int $id): ?string => null,
null,
static fn (int $id): bool => true
);
self::assertNull($probe->run(), 'hooked: on to the page, no path: not checked');
self::assertCount(1, $probe->run() ?? [], 'not hooked: one problem');
}
public function testTheDefaultEnabledCheckReadsTheWpfcSwitch(): void
{
$GLOBALS['wp_fastest_cache_options'] = (object) [];
self::assertNull((new FastestCacheProbe())->run());
$GLOBALS['wp_fastest_cache_options'] = [];
self::assertNull((new FastestCacheProbe())->run());
}
public function testTheDefaultSlugIsWrittenStraightToTheDatabaseWithoutHooks(): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->expects('update')->with('wp_posts', ['post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$GLOBALS['wpdb'] = $wpdb;
Functions\expect('clean_post_cache')->never();
$dropped = [];
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
$dropped[] = $group . ':' . $id;
return true;
});
$probe = new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => unlink($this->pageDir() . 'index.html')
);
self::assertSame([], $probe->run());
self::assertSame(['posts:77'], $dropped, 'the object cache must not keep the empty slug');
}
/** @return array<string, array{0: int|false}> */
public static function refusedUpdates(): array
{
return ['database error' => [false], 'no row changed' => [0]];
}
/** @dataProvider refusedUpdates */
public function testTheDefaultSlugWriterTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->shouldReceive('update')->andReturn($result);
$GLOBALS['wpdb'] = $wpdb;
Functions\when('wp_cache_delete')->justReturn(true);
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = (new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
static fn (int $id) => self::fail('no purge without the slug')
))->run();
});
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
}
private function permalink(string $address): void
{
$GLOBALS['wp_fastest_cache'] = $this->fakeWpfc();
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 77, 'post_status' => 'draft']));
Functions\when('get_permalink')->alias(static function (WP_Post $post) use ($address): string {
self::assertSame('publish', $post->post_status, 'the address the draft would have once published');
return $address;
});
}
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowCacheAll(): void
{
$this->permalink('https://example.test/lit-mcp-check-77/');
self::assertSame($this->pageDir(), FastestCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathKeepsTheSubdirectoryOfTheSite(): void
{
$this->permalink('https://example.test/wp/lit-mcp-check-77/');
self::assertSame($this->cacheRoot . 'cache/all/wp/lit-mcp-check-77/', FastestCacheProbe::cacheDirFor(77));
}
/** @return array<string, array{0: string}> */
public static function foreignAddresses(): array
{
return [
'plain address' => ['https://example.test/?page_id=77'],
'home page' => ['https://example.test/'],
'other slug' => ['https://example.test/seite/'],
'other probe' => ['https://example.test/lit-mcp-check-78/'],
'slug as suffix only' => ['https://example.test/x-lit-mcp-check-77/'],
'traversal' => ['https://example.test/../lit-mcp-check-77/'],
];
}
/** @dataProvider foreignAddresses */
public function testTheDefaultCachePathIsNullForAnythingButTheProbePage(string $address): void
{
$this->permalink($address);
self::assertNull(FastestCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullForAMissingPost(): void
{
Functions\when('get_post')->justReturn(null);
self::assertNull(FastestCacheProbe::cacheDirFor(77));
}
}
@@ -2,27 +2,27 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCachePurgerTest extends TestCase
final class FastestCachePurgerTest 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,
$purger = new FastestCachePurger(
static fn (FastestCacheFeature $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')
static fn () => self::fail('no whole purge')
);
self::assertSame(SuperCachePurger::LOCKED, $purger->everything());
self::assertSame(FastestCachePurger::LOCKED, $purger->everything());
self::assertFalse($purger->post(3));
ini_set('error_log', (string) $previous);
}
@@ -33,7 +33,7 @@ final class SuperCachePurgerTest extends TestCase
Functions\when('get_transient')->alias(static function () use (&$stamp) {
return $stamp;
});
$purger = new SuperCachePurger(static fn (): bool => true);
$purger = new FastestCachePurger(static fn (): bool => true);
self::assertFalse($purger->throttled());
$stamp = (string) (time() - 5);
@@ -56,9 +56,9 @@ final class SuperCachePurgerTest extends TestCase
return true;
});
$purger = new SuperCachePurger(static fn (): bool => true);
$purger = new FastestCachePurger(static fn (): bool => true);
self::assertSame(SuperCachePurger::THROTTLED, $purger->everything());
self::assertSame(FastestCachePurger::THROTTLED, $purger->everything());
self::assertSame(1, $reads);
self::assertCount(1, $scheduledAt);
@@ -1,121 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheCompatibilityTest extends TestCase
{
public function testDescribesSuperCacheAsALockOnFailurePlugin(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertSame('wpsc', $profile->id);
self::assertSame('WP Super Cache', $profile->label);
self::assertSame(Tier::LockOnFailure, $profile->tier);
self::assertSame(['3.1.5'], $profile->testedVersions);
self::assertNull($profile->minVersion);
self::assertSame(SuperCacheFeature::cases(), $profile->capabilities);
}
/** Argument counts are those the listener passes; wp_cache_clear_cache($blog_id = 0) gets the blog. */
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
{
$manifest = SuperCacheCompatibility::profile()->manifests[0];
$calls = [];
foreach ($manifest->calls as $call) {
$calls[$call->symbol] = [$call->args, $call->features()];
}
self::assertSame([
'wp_cache_post_edit' => [1, [SuperCacheFeature::PurgePost]],
'wp_cache_clear_cache' => [1, [SuperCacheFeature::PurgeAll]],
], $calls);
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
}
/** Both purge functions read $cache_path; wp_cache_post_edit also $blog_cache_dir. */
public function testTheGlobalsTheFunctionsReadAreRequired(): void
{
$manifest = SuperCacheCompatibility::profile()->manifests[0];
self::assertSame(['WPCACHEHOME'], array_map(static fn ($c) => $c->name, $manifest->constants));
$globals = [];
foreach ($manifest->globals as $global) {
$globals[$global->name] = $global->features();
}
self::assertSame(
['cache_path' => SuperCacheFeature::cases(), 'blog_cache_dir' => [SuperCacheFeature::PurgePost]],
$globals
);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testActiveAndVersionFollowTheCacheHomeConstantAndThePluginHeader(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertFalse(($profile->active)());
self::assertNull(($profile->version)());
define('WPCACHEHOME', '/plugins/wp-super-cache/');
Functions\expect('get_file_data')
->once()
->with('/plugins/wp-super-cache/wp-cache.php', ['Version' => 'Version'])
->andReturn(['Version' => '3.1.5']);
self::assertTrue(($profile->active)());
self::assertSame('3.1.5', ($profile->version)());
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
{
define('WPCACHEHOME', '/plugins/wp-super-cache/');
Functions\when('get_file_data')->justReturn(['Version' => '']);
self::assertNull((SuperCacheCompatibility::profile()->version)());
}
public function testTheCheckHasTheProbeAsExtraStep(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertNotNull($profile->extraSteps);
$steps = ($profile->extraSteps)(false);
self::assertSame([SuperCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
self::assertTrue($steps[0]->skipped);
self::assertNotNull($profile->stepLabel);
self::assertSame(
'Leeren-Test an einer Wegwerf-Seite im Cache',
($profile->stepLabel)(new CheckStep(SuperCacheCheckSteps::PROBE, []))
);
}
public function testAllProfilesCoexistWithoutIdCollision(): void
{
$compatibility = new Compatibility(
[DiviProfile::profile(), PodsCompatibility::profile(), SuperCacheCompatibility::profile()]
);
self::assertSame(['divi', 'pods', 'wpsc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
self::assertSame(SuperCacheFeature::PurgeAll, $compatibility->capability('wpsc-purge-all'));
}
}
@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheIntegrationTest extends TestCase
{
private static function purger(): SuperCachePurger
{
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(ProvidesCompatibility::class, $integration);
self::assertSame('wpsc', $integration->id());
self::assertSame('WP Super Cache', $integration->label());
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
}
public function testIsAvailableOnlyWithSuperCache(): void
{
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden()))->isAvailable());
}
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
{
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
$integration->boot();
self::assertInstanceOf(Bootable::class, $integration);
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
}
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
{
Functions\expect('get_posts')->never();
Functions\when('current_user_can')->justReturn(true);
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
(new SuperCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
Functions\when('current_user_can')->justReturn(false);
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
$this->addToAssertionCount(1);
}
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
{
Functions\when('current_user_can')->justReturn(true);
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
self::assertSame('page', $args['post_type']);
self::assertSame(['draft', 'private'], $args['post_status']);
return [];
});
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
}
}
@@ -1,525 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Integrations\SuperCache\SuperCacheProbe;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
use WP_Post;
final class SuperCacheProbeTest extends TestCase
{
private string $cacheRoot = '';
/** @var list<int> */
private array $deleted = [];
/** @var list<string> "<id>:<status>" in call order. */
private array $statuses = [];
/** @var array<string, mixed>|null The wp_insert_post arguments. */
private ?array $inserted = null;
/** @var array<string, mixed> Globals the test changes (only those that existed). */
private array $savedGlobals = [];
private const GLOBALS_CHANGED = ['cache_enabled', 'cache_path', 'wpdb', 'wp_cache_clear_on_post_edit'];
/** The status the fake status switch refuses to set (returns false); null = none. */
private ?string $refusedStatus = null;
/** The status the fake status switch throws for; null = none. */
private ?string $throwingStatus = null;
protected function setUp(): void
{
parent::setUp();
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpsc-' . bin2hex(random_bytes(4)) . '/';
mkdir($this->cacheRoot . 'supercache/example.test/', 0777, true);
$this->deleted = [];
$this->statuses = [];
$this->inserted = null;
$this->refusedStatus = null;
$this->throwingStatus = null;
$this->savedGlobals = [];
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $GLOBALS)) {
$this->savedGlobals[$name] = $GLOBALS[$name];
}
}
Functions\when('wp_insert_post')->alias(function (array $post): int {
$this->inserted = $post;
return 77;
});
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
self::assertTrue($force, 'never to the trash');
$this->deleted[] = $id;
return new WP_Post(['ID' => $id]);
});
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
}
protected function tearDown(): void
{
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $this->savedGlobals)) {
$GLOBALS[$name] = $this->savedGlobals[$name];
} else {
unset($GLOBALS[$name]);
}
}
self::removeTree($this->cacheRoot);
parent::tearDown();
}
private static function removeTree(string $path): void
{
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
if (in_array(basename($entry), ['.', '..'], true)) {
continue;
}
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
}
@rmdir($path);
}
private function pageDir(): string
{
return $this->cacheRoot . 'supercache/example.test/lit-mcp-check-77/';
}
/** @param (Closure(int): void)|null $purge */
private function probe(?Closure $purge = null, ?Closure $cacheDir = null, bool $enabled = true): SuperCacheProbe
{
return new SuperCacheProbe(
static fn (): bool => $enabled,
$cacheDir ?? fn (int $id): string => $this->pageDir(),
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
function (int $id, string $status): bool {
$this->statuses[] = $id . ':' . $status;
if ($status === $this->throwingStatus) {
throw new RuntimeException('Status kaputt');
}
return $status !== $this->refusedStatus;
}
);
}
/** Purge fake that does what WPSC does: the page's cache file disappears. */
private function purgingProbe(): SuperCacheProbe
{
return $this->probe(function (int $id): void {
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
unlink($this->pageDir() . 'index.html');
});
}
/** Captures what Log writes to the PHP error log while $run runs. */
private function logged(Closure $run): string
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$run();
return (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
}
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
{
self::assertSame([], $this->purgingProbe()->run());
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testCreatesAMarkedDraftPageAndOnlyPassesThroughPrivateForThePurge(): void
{
$this->purgingProbe()->run();
self::assertSame('page', $this->inserted['post_type']);
self::assertSame('draft', $this->inserted['post_status']);
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static fn (int $id) => null)->run();
});
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Super Cache bestehen.'], $problems);
self::assertStringContainsString('lit-mcp-check-77', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testARenamedRebuildFileCountsAsPurgedAndIsCleanedUp(): void
{
$problems = $this->probe(function (int $id): void {
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
})->run();
self::assertSame([], $problems);
self::assertDirectoryDoesNotExist($this->pageDir());
}
public function testADirectoryThePurgeAlreadyRemovedIsNoProblem(): void
{
$problems = $this->probe(function (int $id): void {
unlink($this->pageDir() . 'index.html');
rmdir($this->pageDir());
})->run();
self::assertSame([], $problems);
}
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPSC kaputt');
})->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPSC kaputt', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
{
self::assertNull($this->probe(null, null, false)->run());
self::assertNull($this->inserted);
}
public function testNothingToProbeWithoutACachePathForThePage(): void
{
self::assertNull($this->probe(null, static fn (int $id): ?string => null)->run());
self::assertSame([77], $this->deleted);
}
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
{
$blocker = $this->cacheRoot . 'blocker';
file_put_contents($blocker, 'x');
$problems = [];
$logged = $this->logged(function () use (&$problems, $blocker): void {
$problems = $this->probe(null, static fn (int $id): string => $blocker . '/seite/')->run();
});
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('seite', $logged);
self::assertSame([77], $this->deleted);
}
public function testTheDefaultPurgeIsWpCachePostEdit(): void
{
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id): int {
unlink($this->pageDir() . 'index.html');
return $id;
});
$probe = new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (): bool => true
);
self::assertSame([], $probe->run());
}
public function testTheDefaultStatusSwitchWritesStatusAndSlugStraightToTheDatabaseWithoutHooks(): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'private', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'draft', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$GLOBALS['wpdb'] = $wpdb;
Functions\expect('clean_post_cache')->never();
$dropped = [];
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
$dropped[] = $group . ':' . $id;
return true;
});
$probe = new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => unlink($this->pageDir() . 'index.html')
);
self::assertSame([], $probe->run());
self::assertSame(['posts:77', 'posts:77'], $dropped, 'the object cache must not keep the old status');
}
public function testTheDefaultEnabledCheckReadsTheWpscGlobal(): void
{
$GLOBALS['cache_enabled'] = false;
self::assertNull((new SuperCacheProbe())->run());
$GLOBALS['cache_enabled'] = null;
self::assertNull((new SuperCacheProbe())->run());
}
private function wpscDirs(string $home, string $current): void
{
Functions\when('get_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $home . '/');
Functions\when('get_current_url_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $current . '/');
}
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowTheHomeHostDirectory(): void
{
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77');
self::assertSame($this->pageDir(), SuperCacheProbe::cacheDirFor(77));
}
/** WPSC derives the host from the request's Host header; the probe must not follow it somewhere else. */
public function testTheDefaultCachePathIsNullWhenWpscWouldUseAnotherHostThanTheHomeUrl(): void
{
$this->wpscDirs('example.test', 'www.example.test/lit-mcp-check-77');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullForAnythingButTheProbePageOfThisSite(): void
{
// A plain "?page_id=" address or an empty slug: WPSC resolves it to the home page's directory.
$this->wpscDirs('example.test', 'example.test');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/seite');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77/../../x');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-78');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
Functions\when('get_current_url_supercache_dir')->justReturn('/var/www/html/lit-mcp-check-77/');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullWhenWpscKnowsNoDirectory(): void
{
Functions\when('get_supercache_dir')->justReturn(false);
Functions\when('get_current_url_supercache_dir')->justReturn(false);
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testOnlyTheKnownCacheFilesOfTheProbePageAreRemoved(): void
{
$problems = $this->probe(function (int $id): void {
// What WPSC leaves after a rebuild-style purge, plus a file that is none of its names.
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
touch($this->pageDir() . 'index-https.html');
touch($this->pageDir() . 'index-https.html.needs-rebuild');
touch($this->pageDir() . 'fremd.txt');
})->run();
self::assertSame([], $problems);
self::assertSame(['fremd.txt'], array_values(array_diff(scandir($this->pageDir()), ['.', '..'])));
}
public function testTheCacheDirectoryOfThePageIsRemovedWhenEmpty(): void
{
$this->purgingProbe()->run();
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertDirectoryExists($this->cacheRoot . 'supercache/example.test/');
}
// --- P1: the purge must not empty the live cache ------------------------------------------------------------------
private function defaultPurgeProbe(): SuperCacheProbe
{
return new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (): bool => true
);
}
public function testTheDefaultPurgeRunsWithClearOnEditOffAndTheRelatedPagesFilterOffAndRestoresBoth(): void
{
$GLOBALS['wp_cache_clear_on_post_edit'] = 1;
$events = [];
$added = null;
Functions\expect('add_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
$events[] = 'add_filter returning ' . var_export($callback(true), true);
$added = $callback;
return true;
});
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
$events[] = $callback === $added ? 'remove_filter of the same callback' : 'remove_filter of ANOTHER callback';
return true;
});
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id) use (&$events): int {
$events[] = 'purge with clear_on_post_edit=' . var_export($GLOBALS['wp_cache_clear_on_post_edit'], true);
unlink($this->pageDir() . 'index.html');
return $id;
});
self::assertSame([], $this->defaultPurgeProbe()->run());
self::assertSame(
['add_filter returning false', 'purge with clear_on_post_edit=0', 'remove_filter of the same callback'],
$events
);
self::assertSame(1, $GLOBALS['wp_cache_clear_on_post_edit']);
}
public function testTheDefaultPurgeLeavesAnUnsetClearOnEditGlobalUnset(): void
{
unset($GLOBALS['wp_cache_clear_on_post_edit']);
Functions\when('add_filter')->justReturn(true);
Functions\when('remove_filter')->justReturn(true);
Functions\expect('wp_cache_post_edit')->once()->andReturnUsing(function (int $id): int {
self::assertSame(0, $GLOBALS['wp_cache_clear_on_post_edit']);
unlink($this->pageDir() . 'index.html');
return $id;
});
self::assertSame([], $this->defaultPurgeProbe()->run());
self::assertArrayNotHasKey('wp_cache_clear_on_post_edit', $GLOBALS);
}
public function testTheDefaultPurgeRestoresGlobalAndFilterWhenWpscThrows(): void
{
$GLOBALS['wp_cache_clear_on_post_edit'] = '1';
Functions\expect('add_filter')->once()->andReturn(true);
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)->andReturn(true);
Functions\expect('wp_cache_post_edit')->once()->andThrow(new RuntimeException('WPSC kaputt'));
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = $this->defaultPurgeProbe()->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertSame('1', $GLOBALS['wp_cache_clear_on_post_edit']);
}
// --- P2: a status the database refused is no purge result ---------------------------------------------------------
public function testARefusedPrivateStatusIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
{
$this->refusedStatus = 'private';
$purged = false;
$problems = [];
$logged = $this->logged(function () use (&$problems, &$purged): void {
$problems = $this->probe(static function (int $id) use (&$purged): void {
$purged = true;
})->run();
});
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('77', $logged);
self::assertStringContainsString('private', $logged);
self::assertFalse($purged);
self::assertSame([77], $this->deleted);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
/** @return array<string, array{0: int|false}> */
public static function refusedUpdates(): array
{
return ['database error' => [false], 'no row changed' => [0]];
}
/** @dataProvider refusedUpdates */
public function testTheDefaultStatusSwitchTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->shouldReceive('update')->andReturn($result);
$GLOBALS['wpdb'] = $wpdb;
Functions\when('wp_cache_delete')->justReturn(true);
Functions\expect('wp_cache_post_edit')->never();
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = (new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => null
))->run();
});
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertSame([77], $this->deleted);
}
public function testAFailingResetToDraftNeitherHidesTheResultNorEndsTheCheck(): void
{
$this->throwingStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->purgingProbe()->run();
});
self::assertSame([], $problems);
self::assertStringContainsString('Status kaputt', $logged);
self::assertSame([77], $this->deleted);
self::assertDirectoryDoesNotExist($this->pageDir());
}
public function testAFailingResetToDraftDoesNotReplaceAnExceptionOfThePurge(): void
{
$this->throwingStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPSC kaputt');
})->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPSC kaputt', $logged);
self::assertSame([77], $this->deleted);
}
public function testARefusedResetToDraftIsLoggedOnly(): void
{
$this->refusedStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->purgingProbe()->run();
});
self::assertSame([], $problems);
self::assertStringContainsString('draft', $logged);
}
}
+29 -28
View File
@@ -50,18 +50,18 @@ final class PluginTest extends TestCase
public function testIntegrationsInRegistryOrder(): void
{
self::assertSame(
['core', 'wordpress', 'media', 'divi', 'divi-library', 'divi-theme-builder', 'pods', 'wpsc'],
['core', 'wordpress', 'media', 'divi', 'divi-library', 'divi-theme-builder', 'pods', 'wpfc'],
self::registry()->ids()
);
}
/** Divi, Pods and WP Super Cache boot (hooks, orphan cleanup); a second boot registers nothing twice. */
/** Divi, Pods and WP Fastest Cache boot (hooks, orphan cleanup); a second boot registers nothing twice. */
public function testTheBootableIntegrationsBootExactlyOnce(): void
{
Actions\expectAdded('upgrader_process_complete')->once();
Actions\expectAdded('lit_mcp_compat_changed')->once();
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(3); // Divi, Pods and WP Super Cache
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(3); // Divi, Pods and WP Fastest Cache
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
$registry = self::registry();
@@ -97,17 +97,17 @@ final class PluginTest extends TestCase
self::assertArrayNotHasKey('pods', $registry->grouped());
}
/** WP Super Cache is registered with its profile; its tool group appears only while the plugin is loaded. */
public function testSuperCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void
/** WP Fastest Cache is registered with its profile; its tool group appears only while the plugin is loaded. */
public function testFastestCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void
{
$registry = self::registry();
self::assertContains('wpsc', $registry->ids());
self::assertArrayNotHasKey('wpsc', $registry->grouped());
self::assertContains('wpfc', $registry->ids());
self::assertArrayNotHasKey('wpfc', $registry->grouped());
}
/**
* The purger of the real wiring asks the registry's Compatibility at call time: with WP Super Cache loaded and
* The purger of the real wiring asks the registry's Compatibility at call time: with WP Fastest Cache loaded and
* its manifest met, the whole-cache purge runs (a registry captured by value would still be null: always locked).
*
* @runInSeparateProcess
@@ -115,33 +115,34 @@ final class PluginTest extends TestCase
*/
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']);
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
eval('function wpfc_clear_post_cache_by_id($id = false) { $GLOBALS["wpfc_calls"][] = "post:" . $id; }'
. 'function wpfc_clear_all_cache($minified = false) { $GLOBALS["wpfc_calls"][] = "all"; }'
. 'class WpFastestCache { public function singleDeleteCache($c = false, $p = false, $parents = true) {}'
. ' public function getWpContentDir($path = false) { return ""; } }');
$GLOBALS['wpfc_calls'] = [];
$GLOBALS['wp_fastest_cache'] = new \WpFastestCache();
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
Functions\when('get_file_data')->justReturn(['Version' => '1.5.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(
$wpfc = array_values(array_filter(
$integrations,
static fn ($integration): bool => $integration->id() === 'wpsc'
static fn ($integration): bool => $integration->id() === 'wpfc'
))[0];
$result = $wpsc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
$result = $wpfc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
self::assertSame('purged', $result['outcome']);
self::assertSame(['all:0'], $GLOBALS['wpsc_calls']);
self::assertSame(['all'], $GLOBALS['wpfc_calls']);
}
/** lit-site-status keeps its order: Divi, Pods, WP Super Cache, The Events Calendar. */
public function testSiteStatusListsDiviPodsSuperCacheAndTheEventsCalendarInThatOrder(): void
/** lit-site-status keeps its order: Divi, Pods, WP Fastest Cache, The Events Calendar. */
public function testSiteStatusListsDiviPodsFastestCacheAndTheEventsCalendarInThatOrder(): void
{
Functions\when('get_bloginfo')->justReturn('x');
Functions\when('home_url')->justReturn('https://example.test');
@@ -158,13 +159,13 @@ final class PluginTest extends TestCase
$components = $status->execute([])['components'];
self::assertSame(['divi', 'pods', 'wpsc', 'the-events-calendar'], array_column($components, 'id'));
self::assertSame(['divi', 'pods', 'wpfc', 'the-events-calendar'], array_column($components, 'id'));
$pods = array_column($components, null, 'id')['pods'];
self::assertFalse($pods['active'], 'no Pods in unit tests');
self::assertSame([], $pods['problems']);
$wpsc = array_column($components, null, 'id')['wpsc'];
self::assertFalse($wpsc['active'], 'no WP Super Cache in unit tests');
self::assertSame([], $wpsc['problems']);
$wpfc = array_column($components, null, 'id')['wpfc'];
self::assertFalse($wpfc['active'], 'no WP Fastest Cache in unit tests');
self::assertSame([], $wpfc['problems']);
}
public function testTheCompatibilityCoversAllProfilesWithoutIdCollision(): void
@@ -172,9 +173,9 @@ final class PluginTest extends TestCase
$compat = self::registry()->compatibility();
$ids = array_map(static fn (Profile $p): string => $p->id, $compat->profiles());
self::assertSame(['divi', 'pods', 'wpsc'], $ids);
self::assertSame(['divi', 'pods', 'wpfc'], $ids);
self::assertSame($ids, array_values(array_unique($ids)));
self::assertSame('wpsc', $compat->owner($compat->capability('wpsc-purge-all'))?->id);
self::assertSame('wpfc', $compat->owner($compat->capability('wpfc-purge-all'))?->id);
self::assertNotNull($compat->capability('page-write'));
self::assertNotNull($compat->capability('pods-field-read'));
self::assertSame('divi', $compat->owner($compat->capability('page-write'))?->id);
+6 -6
View File
@@ -12,7 +12,7 @@ use LIT\MCP\Admin\ConnectionStatus;
use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
use LIT\MCP\Limits;
use LIT\MCP\Updater\RecheckOnInstall;
use Mockery;
@@ -60,7 +60,7 @@ final class UninstallTest extends TestCase
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
@@ -73,15 +73,15 @@ final class UninstallTest extends TestCase
self::assertNotContains('wpmedia_mcp_oauth_rewrite_version', $deleted);
self::assertSame(
[
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
],
$transients
);
self::assertSame(
[
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
],
$hooks
);
+8 -8
View File
@@ -13,7 +13,7 @@ use LIT\MCP\Admin\LogTab;
use LIT\MCP\ChangeLog\ChangeLogCleanup;
use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
use LIT\MCP\Limits;
use LIT\MCP\Uninstaller;
use LIT\MCP\Updater\RecheckOnInstall;
@@ -61,16 +61,16 @@ final class UninstallerTest extends TestCase
Uninstaller::run();
self::assertSame(
[ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT],
[ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT],
$transients
);
self::assertSame([ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK], $hooks);
self::assertSame([ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK], $hooks);
self::assertSame(
[
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
@@ -108,15 +108,15 @@ final class UninstallerTest extends TestCase
self::assertSame(
[
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
],
$transients
);
self::assertSame(
[
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
],
$hooks
);
+1 -1
View File
@@ -4,7 +4,7 @@
* Plugin Name: 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.
* Version: 1.2.1
* Version: 1.2.2
* Update URI: https://git.loosen-it.de/Loosen-IT/WP-LIT-MCP
* Requires at least: 6.9
* Requires PHP: 8.1