Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b3c0df1f1 | ||
|
|
6147ba8087 | ||
|
|
bee9da3a86 | ||
|
|
e1fad3dffe | ||
|
|
1ac347e76f |
@@ -100,10 +100,11 @@ 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.
|
||||
Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite. Wurde etwas außerhalb von LIT MCP
|
||||
geändert, leert `lit/cache-clear` den Cache auf Wunsch (`scope` `all` oder `post` mit `post_id`; erst Probelauf, dann
|
||||
nach Rückfrage `confirm: true`).
|
||||
|
||||
+9
-1
@@ -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.0
|
||||
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,14 @@ 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.2.0 =
|
||||
* Neu: Reiter „Kompatibilität“ (ersetzt den Divi-Reiter) mit Übersicht aller angebundenen Plugins: Zustand, erkannte Version, Fähigkeiten, letzter Prüfbericht, Prüfen, Freigeben und Zurückziehen – auch auf dem Handy und barrierefrei. LIT MCP prüft fremde Plugins ohne offizielle Schnittstelle laufend und sperrt bei einem Bruch nur die betroffenen Werkzeuge.
|
||||
* Neu: Pods läuft über diese Prüfung (ab Pods 2.8) und hat einen Lese- und Schreibtest an einem Wegwerf-Eintrag. Ein roter Test sperrt das Schreiben von Pods-Feldern, bis eine neue Prüfung grün ist.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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]';
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\FastestCache;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\FollowUp;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* lit/cache-clear: empties the WP 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', 'cache_off'];
|
||||
|
||||
public function __construct(private FastestCachePurger $purger, private ChangeLog $log)
|
||||
{
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Seiten-Cache leeren';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Leert den Seiten-Cache von WP 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 '
|
||||
. 'alle 30 Sekunden gedrosselt (ein Nachlauf folgt).';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'scope' => ['type' => 'string', 'enum' => ['all', 'post']],
|
||||
'post_id' => ['type' => 'integer', 'minimum' => 1, 'description' => 'Pflicht bei scope "post".'],
|
||||
'confirm' => ['type' => 'boolean', 'description' => 'true nach Bestätigung durch den Nutzer.'],
|
||||
],
|
||||
'required' => ['scope'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return ToolResult::schema(
|
||||
[
|
||||
'scope' => ['type' => 'string', 'enum' => ['all', 'post']],
|
||||
'outcome' => ['type' => 'string', 'enum' => self::OUTCOMES],
|
||||
],
|
||||
['scope', 'outcome']
|
||||
);
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::write(true);
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function requirements(array $input): array
|
||||
{
|
||||
return match ($input['scope'] ?? null) {
|
||||
'all' => [FastestCacheFeature::PurgeAll->id()],
|
||||
'post' => [FastestCacheFeature::PurgePost->id()],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$target = $this->target($input);
|
||||
|
||||
return $target instanceof WP_Error ? $target : true;
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
$target = $this->target($input);
|
||||
if ($target instanceof WP_Error) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$post = $target instanceof WP_Post ? $target : null;
|
||||
$scope = $post === null ? 'all' : 'post';
|
||||
$what = $post === null
|
||||
? 'den ganzen Seiten-Cache'
|
||||
: sprintf('die Seite von "%s" (ID %d)', $post->post_title, $post->ID);
|
||||
$enabled = $this->purger->cacheEnabled();
|
||||
|
||||
if (($input['confirm'] ?? false) !== true) {
|
||||
return $this->dryRun($scope, $post, $what, $enabled);
|
||||
}
|
||||
if (! $enabled) {
|
||||
return $this->result(
|
||||
$scope,
|
||||
'cache_off',
|
||||
'Der Seiten-Cache von WP Fastest Cache ist ausgeschaltet; nichts zu leeren.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($post !== null) {
|
||||
$outcome = $this->purger->post((int) $post->ID) ? FastestCachePurger::PURGED : FastestCachePurger::LOCKED;
|
||||
} else {
|
||||
$outcome = $this->purger->everything();
|
||||
}
|
||||
if ($outcome === FastestCachePurger::LOCKED) {
|
||||
return Errors::make(
|
||||
'compat_incompatible',
|
||||
'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) {
|
||||
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.',
|
||||
};
|
||||
|
||||
return $this->result($scope, $outcome, $summary, $post);
|
||||
}
|
||||
|
||||
private function dryRun(string $scope, ?WP_Post $post, string $what, bool $enabled): WP_Error
|
||||
{
|
||||
$throttled = $scope === 'all' && $this->purger->throttled();
|
||||
$note = '';
|
||||
if (! $enabled) {
|
||||
$note = ' Der Cache ist ausgeschaltet; es würde nichts geleert.';
|
||||
} elseif ($throttled) {
|
||||
$note = ' Das Leeren wäre gerade gedrosselt und folgt als Nachlauf.';
|
||||
}
|
||||
|
||||
return Errors::confirmRequired(
|
||||
sprintf('Probelauf: %s leeren?%s Nichts wurde geleert.', ucfirst($what), $note),
|
||||
array_filter(
|
||||
['scope' => $scope, 'post_id' => $post?->ID, 'cache_enabled' => $enabled, 'throttled' => $throttled],
|
||||
static fn (mixed $value): bool => $value !== null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function result(string $scope, string $outcome, string $summary, ?WP_Post $post = null): array
|
||||
{
|
||||
$result = ToolResult::ok($summary)->with('scope', $scope)->with('outcome', $outcome);
|
||||
if (in_array($outcome, ['purged', 'throttled'], true)) {
|
||||
$warnings = [];
|
||||
$logId = FollowUp::run(
|
||||
fn () => $this->log->record(
|
||||
self::NAME,
|
||||
'wpfc',
|
||||
$post === null ? 'all' : (string) $post->ID,
|
||||
$summary
|
||||
),
|
||||
self::NAME . ' change log',
|
||||
'Änderungsprotokoll konnte nicht geschrieben werden; der Cache wurde trotzdem behandelt.',
|
||||
$warnings
|
||||
);
|
||||
$result->logId(is_int($logId) ? $logId : null)->warnings($warnings);
|
||||
}
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* The checked target: the WP_Post of scope "post", true for "all" – or the error. Input is not schema-validated
|
||||
* yet when permission() runs.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
private function target(array $input): WP_Post|true|WP_Error
|
||||
{
|
||||
$scope = $input['scope'] ?? null;
|
||||
if ($scope === 'all') {
|
||||
return current_user_can('manage_options')
|
||||
? true
|
||||
: Errors::forbidden('Keine Berechtigung, den ganzen Seiten-Cache zu leeren (manage_options).');
|
||||
}
|
||||
if ($scope !== 'post') {
|
||||
return Errors::invalidInput('scope fehlt oder ist ungültig.', ['scope' => '"all" oder "post"']);
|
||||
}
|
||||
|
||||
$id = $input['post_id'] ?? null;
|
||||
if (! is_int($id) || $id < 1) {
|
||||
return Errors::invalidInput(
|
||||
'post_id fehlt oder ist ungültig.',
|
||||
['post_id' => 'positive Ganzzahl erwartet']
|
||||
);
|
||||
}
|
||||
$post = get_post($id);
|
||||
if (! $post instanceof WP_Post) {
|
||||
return Errors::notFound(sprintf('Eintrag %d nicht gefunden.', $id));
|
||||
}
|
||||
|
||||
return current_user_can('edit_post', $id)
|
||||
? $post
|
||||
: Errors::forbidden(sprintf('Keine Berechtigung, Eintrag %d zu bearbeiten.', $id));
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
{
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\FastestCache;
|
||||
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Bootable;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 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 FastestCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
||||
{
|
||||
/** @var callable(): bool */
|
||||
private $available;
|
||||
|
||||
/** @param callable(): bool|null $available Whether WP Fastest Cache is loaded; null = WPFC_MAIN_PATH is defined. */
|
||||
public function __construct(
|
||||
?callable $available,
|
||||
private FastestCachePurger $purger,
|
||||
private ChangeLog $log
|
||||
) {
|
||||
$this->available = $available ?? static fn (): bool => defined('WPFC_MAIN_PATH');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
add_action(CompatibilityTab::LOAD_ACTION, [$this, 'removeOrphans']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 === 'wpfc' && $this->isAvailable() && current_user_can('manage_options')) {
|
||||
FastestCacheProbe::removeOrphans();
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return 'wpfc';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return FastestCacheCompatibility::LABEL;
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return ($this->available)();
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
return [new CacheClearAbility($this->purger, $this->log)];
|
||||
}
|
||||
|
||||
public function compatibilities(): array
|
||||
{
|
||||
return [FastestCacheCompatibility::profile()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\FastestCache;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\Compatibility;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 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 (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 FastestCacheListener
|
||||
{
|
||||
public const TRAILING_HOOK = FastestCachePurger::TRAILING_HOOK;
|
||||
public const PURGED_AT = FastestCachePurger::PURGED_AT;
|
||||
|
||||
private FastestCachePurger $purger;
|
||||
private Closure $postIdFor;
|
||||
/** @var array<int, true> posts changed in this request */
|
||||
private array $posts = [];
|
||||
private bool $everything = false;
|
||||
private bool $queued = false;
|
||||
|
||||
/**
|
||||
* Defaults are the real WPFC and WordPress functions; closures only for tests.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public function __construct(
|
||||
Closure $usable,
|
||||
?Closure $cacheEnabled = null,
|
||||
?Closure $purgePost = null,
|
||||
?Closure $purgeAll = null,
|
||||
?Closure $postIdFor = null
|
||||
) {
|
||||
$this->purger = new FastestCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
|
||||
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
||||
}
|
||||
|
||||
public static function create(Compatibility $compatibility): self
|
||||
{
|
||||
return new self($compatibility->usable(...));
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action(ChangeLog::CHANGED_ACTION, [$this, 'onChange'], 10, 4);
|
||||
add_action(self::TRAILING_HOOK, [$this, 'trailingPurge']);
|
||||
}
|
||||
|
||||
/** lit_mcp_changed: only collects, see flush(). Never throws. */
|
||||
public function onChange(string $ability, string $objectType, string $objectId, ?int $logId = null): void
|
||||
{
|
||||
try {
|
||||
$ignored = CompatibilityTab::isReleaseAbility($ability) || $ability === CacheClearAbility::NAME;
|
||||
if ($ignored || ! $this->purger->cacheEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$postId = ($this->postIdFor)($objectType, $objectId);
|
||||
if ($postId === null) {
|
||||
$this->everything = true;
|
||||
} else {
|
||||
$this->posts[$postId] = true;
|
||||
}
|
||||
if (! $this->queued) {
|
||||
$this->queued = true;
|
||||
add_action('shutdown', [$this, 'flush']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Fastest Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** shutdown: the one purge of the request. Never throws: a failed purge must not break the response. */
|
||||
public function flush(): void
|
||||
{
|
||||
$posts = array_keys($this->posts);
|
||||
$everything = $this->everything;
|
||||
$this->posts = [];
|
||||
$this->everything = false;
|
||||
|
||||
try {
|
||||
if ($everything || count($posts) > 1) {
|
||||
$this->purger->everything();
|
||||
} elseif ($posts !== []) {
|
||||
$this->purger->post($posts[0]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Fastest Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cron TRAILING_HOOK: the whole-cache purge a throttled one postponed. Same checks as the first try. */
|
||||
public function trailingPurge(): void
|
||||
{
|
||||
try {
|
||||
if ($this->purger->cacheEnabled()) {
|
||||
$this->purger->everything();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Fastest Cache trailing purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
private static function publicPostId(string $objectType, string $objectId): ?int
|
||||
{
|
||||
if ($objectType === 'attachment' || ! ctype_digit($objectId)) {
|
||||
return null;
|
||||
}
|
||||
$type = get_post_type_object($objectType);
|
||||
if ($type === null || empty($type->public)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_post((int) $objectId) instanceof \WP_Post ? (int) $objectId : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\FastestCache;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Log;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 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 FastestCachePurger
|
||||
{
|
||||
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_wpfc_purged_at';
|
||||
/** Seconds between two whole-cache purges. */
|
||||
private const THROTTLE = 30;
|
||||
|
||||
public const PURGED = 'purged';
|
||||
public const THROTTLED = 'throttled';
|
||||
public const LOCKED = 'locked';
|
||||
|
||||
private Closure $usable;
|
||||
private Closure $cacheEnabled;
|
||||
private Closure $purgePost;
|
||||
private Closure $purgeAll;
|
||||
private bool $warned = false;
|
||||
|
||||
/**
|
||||
* Defaults are the real WPFC functions; closures only for tests.
|
||||
*
|
||||
* @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,
|
||||
?Closure $cacheEnabled = null,
|
||||
?Closure $purgePost = null,
|
||||
?Closure $purgeAll = null
|
||||
) {
|
||||
$this->usable = $usable;
|
||||
$this->cacheEnabled = $cacheEnabled ?? FastestCacheCompatibility::cacheEnabled(...);
|
||||
$this->purgePost = $purgePost ?? static function (int $id): void {
|
||||
wpfc_clear_post_cache_by_id($id);
|
||||
};
|
||||
$this->purgeAll = $purgeAll ?? static function (): void {
|
||||
wpfc_clear_all_cache();
|
||||
};
|
||||
}
|
||||
|
||||
public function cacheEnabled(): bool
|
||||
{
|
||||
return ($this->cacheEnabled)();
|
||||
}
|
||||
|
||||
/** Whether a whole-cache purge now would only be postponed. */
|
||||
public function throttled(): bool
|
||||
{
|
||||
return self::within($this->lastPurge());
|
||||
}
|
||||
|
||||
private static function within(?int $last): bool
|
||||
{
|
||||
return $last !== null && time() - $last < self::THROTTLE;
|
||||
}
|
||||
|
||||
/** Time of the last whole-cache purge; null = none within the lock time. Transients may come back as strings. */
|
||||
private function lastPurge(): ?int
|
||||
{
|
||||
$last = get_transient(self::PURGED_AT);
|
||||
|
||||
return is_numeric($last) ? (int) $last : null;
|
||||
}
|
||||
|
||||
/** wpfc_clear_post_cache_by_id for one post; false = locked. Never throttled. */
|
||||
public function post(int $postId): bool
|
||||
{
|
||||
if (! $this->allowed(FastestCacheFeature::PurgePost)) {
|
||||
return false;
|
||||
}
|
||||
($this->purgePost)($postId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
||||
* are per site: the callback runs on this blog).
|
||||
*
|
||||
* @return self::PURGED|self::THROTTLED|self::LOCKED
|
||||
*/
|
||||
public function everything(): string
|
||||
{
|
||||
if (! $this->allowed(FastestCacheFeature::PurgeAll)) {
|
||||
return self::LOCKED;
|
||||
}
|
||||
|
||||
$last = $this->lastPurge();
|
||||
if ($last !== null && self::within($last)) {
|
||||
if (wp_next_scheduled(self::TRAILING_HOOK) === false) {
|
||||
wp_schedule_single_event($last + self::THROTTLE, self::TRAILING_HOOK);
|
||||
}
|
||||
|
||||
return self::THROTTLED;
|
||||
}
|
||||
|
||||
($this->purgeAll)();
|
||||
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
||||
|
||||
return self::PURGED;
|
||||
}
|
||||
|
||||
private function allowed(FastestCacheFeature $feature): bool
|
||||
{
|
||||
if (($this->usable)($feature)) {
|
||||
return true;
|
||||
}
|
||||
if (! $this->warned) {
|
||||
$this->warned = true;
|
||||
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,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Bootable;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* WP Super Cache has no tools: the integration brings the compatibility profile (SuperCacheListener does the
|
||||
* work, wired by Plugin) and the cleanup of probe pages a dead check left behind. Without abilities it is no tool
|
||||
* group.
|
||||
*/
|
||||
final class SuperCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
||||
{
|
||||
/** @var callable(): bool */
|
||||
private $available;
|
||||
|
||||
/** @param callable(): bool|null $available Whether WP Super Cache is loaded; null = WPCACHEHOME is defined. */
|
||||
public function __construct(?callable $available = null)
|
||||
{
|
||||
$this->available = $available ?? static fn (): bool => defined('WPCACHEHOME');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
add_action(CompatibilityTab::LOAD_ACTION, [$this, 'removeOrphans']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function removeOrphans(string $profileId): void
|
||||
{
|
||||
if ($profileId === 'wpsc' && $this->isAvailable() && current_user_can('manage_options')) {
|
||||
SuperCacheProbe::removeOrphans();
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return 'wpsc';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return SuperCacheCompatibility::LABEL;
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return ($this->available)();
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function compatibilities(): array
|
||||
{
|
||||
return [SuperCacheCompatibility::profile()];
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\Compatibility;
|
||||
use LIT\MCP\Log;
|
||||
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.
|
||||
*
|
||||
* 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. Single-post purges are never throttled. The release/withdraw entries of the check workflow change
|
||||
* nothing on the site and purge nothing.
|
||||
*/
|
||||
final class SuperCacheListener
|
||||
{
|
||||
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
|
||||
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
||||
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
|
||||
/** Seconds between two whole-cache purges. */
|
||||
private const THROTTLE = 30;
|
||||
|
||||
private Closure $usable;
|
||||
private Closure $cacheEnabled;
|
||||
private Closure $purgePost;
|
||||
private Closure $purgeAll;
|
||||
private Closure $postIdFor;
|
||||
/** @var array<int, true> posts changed in this request */
|
||||
private array $posts = [];
|
||||
private bool $everything = false;
|
||||
private bool $queued = false;
|
||||
private bool $warned = false;
|
||||
|
||||
/**
|
||||
* Defaults are the real WPSC 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(string, string): ?int)|null $postIdFor Post ID if objectType/objectId name an existing
|
||||
* post of a public type but attachment.
|
||||
*/
|
||||
public function __construct(
|
||||
Closure $usable,
|
||||
?Closure $cacheEnabled = null,
|
||||
?Closure $purgePost = null,
|
||||
?Closure $purgeAll = null,
|
||||
?Closure $postIdFor = null
|
||||
) {
|
||||
$this->usable = $usable;
|
||||
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
||||
$this->purgePost = $purgePost ?? static function (int $id): void {
|
||||
wp_cache_post_edit($id);
|
||||
};
|
||||
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
|
||||
wp_cache_clear_cache($blog);
|
||||
};
|
||||
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
||||
}
|
||||
|
||||
public static function create(Compatibility $compatibility): self
|
||||
{
|
||||
return new self($compatibility->usable(...));
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action(ChangeLog::CHANGED_ACTION, [$this, 'onChange'], 10, 4);
|
||||
add_action(self::TRAILING_HOOK, [$this, 'trailingPurge']);
|
||||
}
|
||||
|
||||
/** lit_mcp_changed: only collects, see flush(). Never throws. */
|
||||
public function onChange(string $ability, string $objectType, string $objectId, ?int $logId = null): void
|
||||
{
|
||||
try {
|
||||
if (CompatibilityTab::isReleaseAbility($ability) || ! ($this->cacheEnabled)()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$postId = ($this->postIdFor)($objectType, $objectId);
|
||||
if ($postId === null) {
|
||||
$this->everything = true;
|
||||
} else {
|
||||
$this->posts[$postId] = true;
|
||||
}
|
||||
if (! $this->queued) {
|
||||
$this->queued = true;
|
||||
add_action('shutdown', [$this, 'flush']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** shutdown: the one purge of the request. Never throws: a failed purge must not break the response. */
|
||||
public function flush(): void
|
||||
{
|
||||
$posts = array_keys($this->posts);
|
||||
$everything = $this->everything;
|
||||
$this->posts = [];
|
||||
$this->everything = false;
|
||||
|
||||
try {
|
||||
if ($everything || count($posts) > 1) {
|
||||
$this->purgeEverything();
|
||||
} elseif ($posts !== []) {
|
||||
$this->purge(SuperCacheFeature::PurgePost, fn () => ($this->purgePost)($posts[0]));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cron TRAILING_HOOK: the whole-cache purge a throttled one postponed. Same checks as the first try. */
|
||||
public function trailingPurge(): void
|
||||
{
|
||||
try {
|
||||
if (($this->cacheEnabled)()) {
|
||||
$this->purgeEverything();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache trailing purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
||||
* are per site: the callback runs on this blog, no blog switch, wp_cache_clear_cache takes the blog ID).
|
||||
*/
|
||||
private function purgeEverything(): void
|
||||
{
|
||||
$this->purge(SuperCacheFeature::PurgeAll, function (): void {
|
||||
$last = get_transient(self::PURGED_AT);
|
||||
// Transients in the options table come back as strings.
|
||||
if (is_numeric($last) && time() - (int) $last < self::THROTTLE) {
|
||||
if (wp_next_scheduled(self::TRAILING_HOOK) === false) {
|
||||
wp_schedule_single_event((int) $last + self::THROTTLE, self::TRAILING_HOOK);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Known and unavoidable on a multisite: WPSC also empties the shared blogs/ meta directory of all sites
|
||||
// in its blog branch (wp-cache-phase2.php, wp_cache_clear_cache).
|
||||
($this->purgeAll)(is_multisite() ? get_current_blog_id() : 0);
|
||||
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param Closure(): void $purge */
|
||||
private function purge(SuperCacheFeature $feature, Closure $purge): void
|
||||
{
|
||||
if (! ($this->usable)($feature)) {
|
||||
if (! $this->warned) {
|
||||
$this->warned = true;
|
||||
Log::line(sprintf('WP Super Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$purge();
|
||||
}
|
||||
|
||||
private static function publicPostId(string $objectType, string $objectId): ?int
|
||||
{
|
||||
if ($objectType === 'attachment' || ! ctype_digit($objectId)) {
|
||||
return null;
|
||||
}
|
||||
$type = get_post_type_object($objectType);
|
||||
if ($type === null || empty($type->public)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_post((int) $objectId) instanceof \WP_Post ? (int) $objectId : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
-6
@@ -37,8 +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\SuperCacheIntegration;
|
||||
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;
|
||||
@@ -59,7 +61,7 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class Plugin
|
||||
{
|
||||
public const VERSION = '1.2.0';
|
||||
public const VERSION = '1.2.2';
|
||||
|
||||
private static string $pluginFile = '';
|
||||
private static ?SettingsPage $settingsPage = null;
|
||||
@@ -98,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();
|
||||
|
||||
@@ -214,7 +216,16 @@ 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 FastestCachePurger(
|
||||
static function (FastestCacheFeature $feature) use (&$registry): bool {
|
||||
return $registry instanceof IntegrationRegistry && $registry->compatibility()->usable($feature);
|
||||
}
|
||||
),
|
||||
$changeLog
|
||||
)
|
||||
);
|
||||
|
||||
return $registry;
|
||||
|
||||
+6
-6
@@ -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. */
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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): 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_ALL=$ROOT/wp-content/cache/all
|
||||
HTACCESS=$ROOT/.htaccess
|
||||
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 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}'; }
|
||||
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")
|
||||
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)
|
||||
OPTIONS_BEFORE=$(list_options)
|
||||
ACTIVE_PLUGINS_BEFORE=$(wp option get active_plugins --format=json)
|
||||
ORIGINAL_DESCRIPTION=$(wp option get blogdescription)
|
||||
CREATED_POSTS=()
|
||||
CREATED_TERMS=()
|
||||
|
||||
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 transient delete "$PURGED_AT" >/dev/null 2>&1
|
||||
cleanup_app_passwords_for "$ADMIN_LOGIN"
|
||||
if [ "$WPFC_OPTION_BEFORE" = __absent__ ]; then
|
||||
wp option delete WpFastestCache >/dev/null 2>&1
|
||||
else
|
||||
wp option update WpFastestCache "$WPFC_OPTION_BEFORE" >/dev/null 2>&1
|
||||
fi
|
||||
[ "$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
|
||||
for option in $(list_options); do
|
||||
grep -qx "$option" <<<"$OPTIONS_BEFORE" || wp option delete "$option" >/dev/null 2>&1
|
||||
done
|
||||
set -e
|
||||
}
|
||||
|
||||
# Prints the proof; returns non-zero on any difference (used in the trap, so it must not exit).
|
||||
verify_restored() {
|
||||
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; }
|
||||
[ "$(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; }
|
||||
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || { echo "FAIL: transient $PURGED_AT left"; ok=1; }
|
||||
[ "$(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-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"
|
||||
}
|
||||
|
||||
on_exit() {
|
||||
local rc=$1
|
||||
restore
|
||||
verify_restored || rc=1
|
||||
guard_exit "$rc" || exit $?
|
||||
exit "$rc"
|
||||
}
|
||||
trap 'on_exit $?' EXIT
|
||||
trap 'exit 130' INT TERM HUP
|
||||
|
||||
# --- Helpers ---------------------------------------------------------------------------------------------------------
|
||||
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 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
|
||||
fail "timeout: $what"
|
||||
}
|
||||
cache_file_exists() { [ -n "$(cache_file)" ]; }
|
||||
cache_file_gone() { [ -z "$(cache_file)" ]; }
|
||||
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 Fastest Cache has written its file.
|
||||
fill_cache() {
|
||||
local i
|
||||
for i in $(seq 1 10); do
|
||||
fetch_post
|
||||
cache_file_exists && return 0
|
||||
sleep 1
|
||||
done
|
||||
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"
|
||||
}
|
||||
|
||||
# --- 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
|
||||
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 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/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"
|
||||
}
|
||||
run_check
|
||||
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 "/wpfc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
|
||||
|
||||
# 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_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 == "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 "$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 "/wpfc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
|
||||
|
||||
# 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#$ROOT/##")"
|
||||
|
||||
# 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 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]}}")
|
||||
echo "$UPDATED" | jq -e '.ok == true' >/dev/null || fail "lit-content-update (terms only): $UPDATED"
|
||||
wait_for "the cache file of the post is gone after lit-content-update" cache_file_gone
|
||||
[ "$(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"
|
||||
|
||||
# 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 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")
|
||||
[ "$(event_count)" = 0 ] || fail "the first whole purge must not schedule a trailing purge"
|
||||
pass "setting change empties the whole cache (purge time stored, no trailing event)"
|
||||
|
||||
fill_cache
|
||||
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 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"
|
||||
|
||||
# 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"
|
||||
[ "$(event_count)" = 0 ] || fail "no trailing event may be left after it ran"
|
||||
[ "$(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"
|
||||
|
||||
# 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")
|
||||
[ "$(error_code "$DRY")" = confirm_required ] || fail "lit/cache-clear dry run $ARGS must ask for confirm: $DRY"
|
||||
echo "$DRY" | jq -e '(.__error | fromjson | .cache_enabled) == true' >/dev/null || fail "dry run must report the cache as on: $DRY"
|
||||
done
|
||||
sleep 2
|
||||
cache_file_exists || fail "the dry runs of lit/cache-clear emptied the cache"
|
||||
[ "$(event_count)" = 0 ] || fail "a dry run must not schedule anything"
|
||||
pass "lit/cache-clear without confirm: asks for confirmation, reports the cache as on, cache file stays"
|
||||
|
||||
CLEARED=$(ability lit/cache-clear "{\"scope\":\"post\",\"post_id\":$POST_ID,\"confirm\":true}")
|
||||
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear post: $CLEARED"
|
||||
wait_for "the cache file of the post is gone after lit/cache-clear scope post" cache_file_gone
|
||||
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
|
||||
pass "lit/cache-clear scope post with confirm: the page of the post is emptied"
|
||||
|
||||
# The whole-cache purge needs the lock time of the earlier steps to be over.
|
||||
WAIT=$(($(wp transient get "$PURGED_AT") + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
||||
fill_cache
|
||||
CLEARED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
|
||||
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear all: $CLEARED"
|
||||
wait_for "the cache is empty after lit/cache-clear scope all" cache_file_gone
|
||||
sleep 2
|
||||
# Its own change log entry must not purge a second time (that would be throttled and leave a trailing event).
|
||||
[ "$(event_count)" = 0 ] || fail "the change log entry of lit/cache-clear triggered a second purge"
|
||||
LAST=$(wp transient get "$PURGED_AT")
|
||||
pass "lit/cache-clear scope all with confirm: whole cache emptied once, own log entry purges nothing"
|
||||
|
||||
fill_cache
|
||||
THROTTLED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
|
||||
echo "$THROTTLED" | jq -e '.ok == true and .outcome == "throttled"' >/dev/null || fail "lit/cache-clear all inside the lock time: $THROTTLED"
|
||||
[ "$(event_count)" = 1 ] || fail "a throttled lit/cache-clear must schedule exactly one trailing purge"
|
||||
cache_file_exists || fail "throttled: the cache file must stay"
|
||||
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "a throttled purge must not move the purge time"
|
||||
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
||||
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
|
||||
cache_file_gone || fail "the trailing purge did not empty the cache"
|
||||
pass "lit/cache-clear scope all inside 30 s: throttled, one trailing event, cron run empties the cache"
|
||||
|
||||
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
|
||||
restore
|
||||
verify_restored || fail "the state before the run was not restored"
|
||||
pass "cleanup: test post, categories, options, transient and cron events gone"
|
||||
|
||||
finish
|
||||
@@ -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=""
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
#!/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
|
||||
#
|
||||
# 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.
|
||||
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
|
||||
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
|
||||
BASE=http://localhost
|
||||
|
||||
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)
|
||||
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
|
||||
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)
|
||||
OPTIONS_BEFORE=$(list_options)
|
||||
ACTIVE_PLUGINS_BEFORE=$(wp option get active_plugins --format=json)
|
||||
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
|
||||
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"
|
||||
fi
|
||||
# The profile's cron event and every option or event the plugin added.
|
||||
for hook in $(list_hooks); do
|
||||
grep -qx "$hook" <<<"$HOOKS_BEFORE" || wp cron event delete "$hook" >/dev/null 2>&1
|
||||
done
|
||||
for option in $(list_options); do
|
||||
grep -qx "$option" <<<"$OPTIONS_BEFORE" || wp option delete "$option" >/dev/null 2>&1
|
||||
done
|
||||
set -e
|
||||
}
|
||||
|
||||
# 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")
|
||||
[ "$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; }
|
||||
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || { echo "FAIL: transient $PURGED_AT left"; ok=1; }
|
||||
[ "$(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 ] \
|
||||
|| { echo "FAIL: the test post is left"; ok=1; }
|
||||
return "$ok"
|
||||
}
|
||||
|
||||
on_exit() {
|
||||
local rc=$1
|
||||
restore
|
||||
verify_restored || rc=1
|
||||
rm -rf "$BACKUP"
|
||||
guard_exit "$rc" || exit $?
|
||||
exit "$rc"
|
||||
}
|
||||
trap 'on_exit $?' EXIT
|
||||
trap 'exit 130' INT TERM HUP
|
||||
|
||||
# --- Helpers ---------------------------------------------------------------------------------------------------------
|
||||
wpsc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpsc")'; }
|
||||
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; }
|
||||
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
|
||||
fail "timeout: $what"
|
||||
}
|
||||
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).
|
||||
fill_cache() {
|
||||
local i
|
||||
for i in $(seq 1 10); do
|
||||
fetch_post
|
||||
sleep 1
|
||||
cache_file_exists && return 0
|
||||
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
|
||||
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
|
||||
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"
|
||||
|
||||
# 2. Check route with the cache still 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")
|
||||
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)
|
||||
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'"
|
||||
|
||||
# 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
|
||||
run_check
|
||||
REPORT=$(stored_option lit_mcp_wpsc_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"
|
||||
[ "$(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")
|
||||
[ "$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"
|
||||
|
||||
# 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)
|
||||
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/##")"
|
||||
|
||||
# 6. Control: a term change outside this plugin does not touch the cache (WP Super 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"
|
||||
# ... 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]}}")
|
||||
echo "$UPDATED" | jq -e '.ok == true' >/dev/null || fail "lit-content-update (terms only): $UPDATED"
|
||||
wait_for "the cache file of the post is gone after lit-content-update" cache_file_gone
|
||||
[ "$(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
|
||||
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"
|
||||
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")
|
||||
[ "$(event_count)" = 0 ] || fail "the first whole purge must not schedule a trailing purge"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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"
|
||||
[ "$(event_count)" = 0 ] || fail "no trailing event may be left after it ran"
|
||||
[ "$(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"
|
||||
|
||||
# 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"
|
||||
|
||||
finish
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\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;
|
||||
use WP_Post;
|
||||
|
||||
final class CacheClearAbilityTest extends TestCase
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $purges = [];
|
||||
/** @var list<string> */
|
||||
private array $scheduled = [];
|
||||
private int|false $lastPurge = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->purges = [];
|
||||
$this->scheduled = [];
|
||||
$this->lastPurge = false;
|
||||
Functions\when('is_multisite')->justReturn(false);
|
||||
Functions\when('get_transient')->alias(fn (): int|false => $this->lastPurge);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||
Functions\when('wp_schedule_single_event')->alias(function (int $at, string $hook): bool {
|
||||
$this->scheduled[] = $hook;
|
||||
|
||||
return true;
|
||||
});
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('get_post')->alias(
|
||||
static fn (int $id): ?WP_Post => $id === 7
|
||||
? new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_title' => 'Start', 'post_status' => 'publish'])
|
||||
: ($id === 8
|
||||
? new WP_Post(['ID' => 8, 'post_type' => 'page', 'post_title' => 'Idee', 'post_status' => 'draft'])
|
||||
: null)
|
||||
);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($GLOBALS['wp_fastest_cache_options']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
|
||||
{
|
||||
$purger = new FastestCachePurger(
|
||||
static fn (FastestCacheFeature $feature): bool => $usable,
|
||||
static fn (): bool => $cacheOn,
|
||||
function (int $id): void {
|
||||
$this->purges[] = 'post:' . $id;
|
||||
},
|
||||
function (): void {
|
||||
$this->purges[] = 'all';
|
||||
}
|
||||
);
|
||||
|
||||
return new CacheClearAbility($purger, $log ?? FakeChangeLog::forbidden());
|
||||
}
|
||||
|
||||
public function testDescribesItselfLikeAGuardedWriteTool(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
|
||||
self::assertInstanceOf(Ability::class, $ability);
|
||||
self::assertSame('lit/cache-clear', $ability->name());
|
||||
self::assertFalse($ability->isDirect());
|
||||
self::assertSame(Annotations::write(true), $ability->annotations());
|
||||
self::assertStringContainsString('confirm: true', $ability->description());
|
||||
self::assertStringContainsString('WP Fastest Cache', $ability->description());
|
||||
|
||||
$schema = $ability->inputSchema();
|
||||
self::assertSame(['scope'], $schema['required']);
|
||||
self::assertFalse($schema['additionalProperties']);
|
||||
self::assertSame(['all', 'post'], $schema['properties']['scope']['enum']);
|
||||
self::assertSame(['scope', 'post_id', 'confirm'], array_keys($schema['properties']));
|
||||
self::assertArrayHasKey('outcome', $ability->outputSchema()['properties']);
|
||||
}
|
||||
|
||||
public function testDeclaresTheCapabilityOfTheScope(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
|
||||
self::assertInstanceOf(DeclaresRequirements::class, $ability);
|
||||
self::assertSame(['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([]));
|
||||
}
|
||||
|
||||
public function testAllNeedsManageOptions(): void
|
||||
{
|
||||
$asked = [];
|
||||
Functions\when('current_user_can')->alias(function (string $cap) use (&$asked): bool {
|
||||
$asked[] = $cap;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
self::assertToolError($this->ability()->permission(['scope' => 'all']), 'forbidden');
|
||||
self::assertSame(['manage_options'], $asked);
|
||||
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
self::assertTrue($this->ability()->permission(['scope' => 'all']));
|
||||
}
|
||||
|
||||
public function testPostNeedsEditPostForThatId(): void
|
||||
{
|
||||
$asked = [];
|
||||
Functions\when('current_user_can')->alias(function (string $cap, int $id) use (&$asked): bool {
|
||||
$asked[] = [$cap, $id];
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 7]), 'forbidden');
|
||||
self::assertSame([['edit_post', 7]], $asked);
|
||||
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
self::assertTrue($this->ability()->permission(['scope' => 'post', 'post_id' => 7]));
|
||||
}
|
||||
|
||||
public function testAnUnknownPostIsNotFound(): void
|
||||
{
|
||||
self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 99]), 'not_found');
|
||||
self::assertToolError($this->ability()->execute(['scope' => 'post', 'post_id' => 99, 'confirm' => true]), 'not_found');
|
||||
self::assertSame([], $this->purges);
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<string, mixed>}> */
|
||||
public static function invalidInputs(): array
|
||||
{
|
||||
return [
|
||||
'no scope' => [[]],
|
||||
'unknown scope' => [['scope' => 'site']],
|
||||
'post without id' => [['scope' => 'post']],
|
||||
'post with zero' => [['scope' => 'post', 'post_id' => 0]],
|
||||
'post with text' => [['scope' => 'post', 'post_id' => 'abc']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidInputs
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function testInvalidInputIsRefusedWithoutEffect(array $input): void
|
||||
{
|
||||
self::assertToolError($this->ability()->permission($input), 'validation_failed');
|
||||
self::assertToolError($this->ability()->execute($input + ['confirm' => true]), 'validation_failed');
|
||||
self::assertSame([], $this->purges);
|
||||
}
|
||||
|
||||
public function testTheDryRunClearsNothingAndReportsTheState(): void
|
||||
{
|
||||
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||
$this->lastPurge = time() - 5;
|
||||
|
||||
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
|
||||
|
||||
self::assertSame([], $this->purges);
|
||||
self::assertSame([], $this->scheduled);
|
||||
self::assertSame('all', $payload['scope']);
|
||||
self::assertTrue($payload['cache_enabled']);
|
||||
self::assertTrue($payload['throttled']);
|
||||
self::assertStringContainsString('Probelauf', $payload['message']);
|
||||
}
|
||||
|
||||
public function testTheDryRunOfAPostNamesThePostAndIgnoresAConfirmThatIsNotTrue(): void
|
||||
{
|
||||
$payload = self::assertToolError(
|
||||
$this->ability(cacheOn: false)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => 'true']),
|
||||
'confirm_required'
|
||||
);
|
||||
|
||||
self::assertSame([], $this->purges);
|
||||
self::assertSame(7, $payload['post_id']);
|
||||
self::assertFalse($payload['cache_enabled']);
|
||||
self::assertStringContainsString('Start', $payload['message']);
|
||||
self::assertStringContainsString('ausgeschaltet', $payload['message']);
|
||||
}
|
||||
|
||||
public function testConfirmClearsThePageOfThePostAndLogsIt(): void
|
||||
{
|
||||
$log = new FakeChangeLog(4);
|
||||
|
||||
$result = $this->ability($log)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => true]);
|
||||
|
||||
self::assertSame(['post:7'], $this->purges);
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('purged', $result['outcome']);
|
||||
self::assertSame(4, $result['log_id']);
|
||||
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => '7']);
|
||||
}
|
||||
|
||||
/** WPFC finds the address of any post (a trashed one too), so no status is skipped. */
|
||||
public function testADraftIsClearedToo(): void
|
||||
{
|
||||
$result = $this->ability(new FakeChangeLog(2))->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
|
||||
|
||||
self::assertSame(['post:8'], $this->purges);
|
||||
self::assertSame('purged', $result['outcome']);
|
||||
}
|
||||
|
||||
public function testConfirmClearsTheWholeCacheAtOnce(): void
|
||||
{
|
||||
$log = new FakeChangeLog(1);
|
||||
|
||||
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
|
||||
|
||||
self::assertSame(['all'], $this->purges);
|
||||
self::assertSame('purged', $result['outcome']);
|
||||
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => 'all']);
|
||||
}
|
||||
|
||||
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
|
||||
{
|
||||
$this->lastPurge = time() - 5;
|
||||
|
||||
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
|
||||
|
||||
self::assertSame([], $this->purges);
|
||||
self::assertSame([FastestCachePurger::TRAILING_HOOK], $this->scheduled);
|
||||
self::assertSame('throttled', $result['outcome']);
|
||||
self::assertStringContainsString('Nachlauf', $result['summary']);
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<string, mixed>}> */
|
||||
public static function scopes(): array
|
||||
{
|
||||
return ['all' => [['scope' => 'all']], 'post' => [['scope' => 'post', 'post_id' => 7]]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider scopes
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function testWithTheCacheOffNothingHappensAndNothingIsLogged(array $input): void
|
||||
{
|
||||
$result = $this->ability(FakeChangeLog::forbidden(), cacheOn: false)->execute($input + ['confirm' => true]);
|
||||
|
||||
self::assertSame([], $this->purges);
|
||||
self::assertSame('cache_off', $result['outcome']);
|
||||
self::assertNull($result['log_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider confirmedScopes
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function testALockedPurgeIsReportedAsAnErrorAndNeitherPurgesNorLogs(array $input): void
|
||||
{
|
||||
$previous = ini_set('error_log', '/dev/null');
|
||||
|
||||
$result = $this->ability(FakeChangeLog::forbidden(), usable: false)->execute($input);
|
||||
|
||||
ini_set('error_log', (string) $previous);
|
||||
self::assertInstanceOf(WP_Error::class, $result);
|
||||
self::assertSame('lit_compat_incompatible', $result->get_error_code());
|
||||
self::assertStringContainsString(
|
||||
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
||||
$result->get_error_message()
|
||||
);
|
||||
self::assertSame([], $this->purges);
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<string, mixed>}> */
|
||||
public static function confirmedScopes(): array
|
||||
{
|
||||
return [
|
||||
'all' => [['scope' => 'all', 'confirm' => true]],
|
||||
'post' => [['scope' => 'post', 'post_id' => 7, 'confirm' => true]],
|
||||
];
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -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'));
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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');
|
||||
}
|
||||
}
|
||||
+50
-50
@@ -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). */
|
||||
@@ -200,6 +200,18 @@ final class SuperCacheListenerTest extends TestCase
|
||||
self::assertSame([], $this->purges);
|
||||
}
|
||||
|
||||
/** lit/cache-clear purges itself; its change log entry must not trigger a second purge. */
|
||||
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
|
||||
{
|
||||
$listener = $this->listener();
|
||||
$listener->onChange('lit/cache-clear', 'wpfc', 'all', 1);
|
||||
$listener->onChange('lit/cache-clear', 'wpfc', '12', 2);
|
||||
$listener->flush();
|
||||
|
||||
self::assertSame([], $this->purges);
|
||||
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
|
||||
}
|
||||
|
||||
public function testNothingHappensWhileTheCacheIsOff(): void
|
||||
{
|
||||
$listener = $this->listener(cacheOn: false);
|
||||
@@ -221,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. */
|
||||
@@ -232,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
|
||||
@@ -246,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();
|
||||
@@ -265,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]);
|
||||
}
|
||||
@@ -293,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. */
|
||||
@@ -325,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);
|
||||
}
|
||||
|
||||
@@ -339,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,
|
||||
@@ -355,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
|
||||
);
|
||||
|
||||
@@ -377,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']));
|
||||
}
|
||||
|
||||
@@ -418,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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
|
||||
final class FastestCachePurgerTest extends TestCase
|
||||
{
|
||||
public function testALockedWholePurgeDoesNothingAndSaysSo(): void
|
||||
{
|
||||
$previous = ini_set('error_log', '/dev/null');
|
||||
Functions\expect('get_transient')->never();
|
||||
$purger = new FastestCachePurger(
|
||||
static fn (FastestCacheFeature $feature): bool => false,
|
||||
static fn (): bool => true,
|
||||
static fn (int $id) => self::fail('no post purge'),
|
||||
static fn () => self::fail('no whole purge')
|
||||
);
|
||||
|
||||
self::assertSame(FastestCachePurger::LOCKED, $purger->everything());
|
||||
self::assertFalse($purger->post(3));
|
||||
ini_set('error_log', (string) $previous);
|
||||
}
|
||||
|
||||
public function testThrottledReadsTheStamp(): void
|
||||
{
|
||||
$stamp = false;
|
||||
Functions\when('get_transient')->alias(static function () use (&$stamp) {
|
||||
return $stamp;
|
||||
});
|
||||
$purger = new FastestCachePurger(static fn (): bool => true);
|
||||
|
||||
self::assertFalse($purger->throttled());
|
||||
$stamp = (string) (time() - 5);
|
||||
self::assertTrue($purger->throttled());
|
||||
$stamp = time() - 31;
|
||||
self::assertFalse($purger->throttled());
|
||||
}
|
||||
|
||||
/** The stamp is read once: a transient expiring between two reads must not schedule the trailing purge in 1970. */
|
||||
public function testEverythingReadsTheStampOnceAndSchedulesTheTrailingPurgeInTheFuture(): void
|
||||
{
|
||||
$reads = 0;
|
||||
$scheduledAt = [];
|
||||
Functions\when('get_transient')->alias(static function () use (&$reads) {
|
||||
return ++$reads === 1 ? time() - 5 : false;
|
||||
});
|
||||
Functions\when('wp_next_scheduled')->justReturn(false);
|
||||
Functions\when('wp_schedule_single_event')->alias(static function (int $at) use (&$scheduledAt): bool {
|
||||
$scheduledAt[] = $at;
|
||||
|
||||
return true;
|
||||
});
|
||||
$purger = new FastestCachePurger(static fn (): bool => true);
|
||||
|
||||
self::assertSame(FastestCachePurger::THROTTLED, $purger->everything());
|
||||
|
||||
self::assertSame(1, $reads);
|
||||
self::assertCount(1, $scheduledAt);
|
||||
self::assertGreaterThanOrEqual(time(), $scheduledAt[0]);
|
||||
}
|
||||
}
|
||||
@@ -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,68 +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\Tests\Unit\TestCase;
|
||||
|
||||
final class SuperCacheIntegrationTest extends TestCase
|
||||
{
|
||||
public function testIsAnIntegrationWithoutAbilitiesThatProvidesItsProfile(): void
|
||||
{
|
||||
$integration = new SuperCacheIntegration();
|
||||
|
||||
self::assertInstanceOf(Integration::class, $integration);
|
||||
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
|
||||
self::assertSame('wpsc', $integration->id());
|
||||
self::assertSame('WP Super Cache', $integration->label());
|
||||
self::assertSame([], $integration->abilities());
|
||||
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
|
||||
}
|
||||
|
||||
public function testIsAvailableOnlyWithSuperCache(): void
|
||||
{
|
||||
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration())->isAvailable());
|
||||
}
|
||||
|
||||
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
|
||||
{
|
||||
$integration = new SuperCacheIntegration();
|
||||
$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))->removeOrphans('pods');
|
||||
(new SuperCacheIntegration(static fn (): bool => false))->removeOrphans('wpsc');
|
||||
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
(new SuperCacheIntegration(static fn (): bool => true))->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))->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);
|
||||
}
|
||||
}
|
||||
+55
-18
@@ -34,11 +34,13 @@ use Mockery;
|
||||
/** The wiring of the composition root: Plugin::integrations() and the steps Plugin::boot() takes with it. */
|
||||
final class PluginTest extends TestCase
|
||||
{
|
||||
private static function registry(ToolAvailability $tools = new ToolAvailability()): IntegrationRegistry
|
||||
{
|
||||
private static function registry(
|
||||
ToolAvailability $tools = new ToolAvailability(),
|
||||
?FakeChangeLog $log = null
|
||||
): IntegrationRegistry {
|
||||
return Plugin::integrations(
|
||||
Mockery::mock(ContentSchema::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
$log ?? FakeChangeLog::forbidden(),
|
||||
new Limits(),
|
||||
$tools
|
||||
);
|
||||
@@ -48,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();
|
||||
@@ -95,17 +97,52 @@ final class PluginTest extends TestCase
|
||||
self::assertArrayNotHasKey('pods', $registry->grouped());
|
||||
}
|
||||
|
||||
/** WP Super Cache has no tools either: registered for its profile only. */
|
||||
public function testSuperCacheIsRegisteredWithoutBecomingAToolGroup(): 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());
|
||||
}
|
||||
|
||||
/** lit-site-status keeps its order: Divi, Pods, WP Super Cache, The Events Calendar. */
|
||||
public function testSiteStatusListsDiviPodsSuperCacheAndTheEventsCalendarInThatOrder(): void
|
||||
/**
|
||||
* 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
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function testTheWiredPurgerAsksTheCompatibilityOfTheRegistry(): void
|
||||
{
|
||||
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)));
|
||||
$wpfc = array_values(array_filter(
|
||||
$integrations,
|
||||
static fn ($integration): bool => $integration->id() === 'wpfc'
|
||||
))[0];
|
||||
|
||||
$result = $wpfc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
|
||||
|
||||
self::assertSame('purged', $result['outcome']);
|
||||
self::assertSame(['all'], $GLOBALS['wpfc_calls']);
|
||||
}
|
||||
|
||||
/** 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');
|
||||
@@ -122,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
|
||||
@@ -136,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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
@@ -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.0
|
||||
* 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
|
||||
|
||||
Reference in New Issue
Block a user