feat(cache): Verhaltensprobe und Smoke für WP Super Cache
SuperCacheProbe prüft beim Prüfen, ob wp_cache_post_edit die Cache-Datei einer Testseite entfernt; während der Probe ist nur der Einzelseiten-Purge erlaubt, der Pfad kommt aus home und es werden nur feste Dateinamen gelöscht. ProbeStep bündelt die Schrittlogik von Pods und WP Super Cache; ThrowawayPost hält Marker und Titel zentral und räumt auch private Probe-Seiten auf (WP Super Cache wie Pods beim Laden des Reiters). super-cache-smoke.sh prüft gegen das echte Plugin; lib.sh sperrt parallele Smoke-Läufe per flock, wp-config.php wird atomar wiederhergestellt. Doku: Host-Abhängigkeit des Einzel-Purges, WP-Cron für den Nachlauf, Komplett-Purge durch Divi beim Prüfen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
05bf890481
commit
661d3945fd
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
use Closure;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The one check step a profile's behaviour probe makes (Profile::$extraSteps), shared by Pods and WP Super Cache. */
|
||||
final class ProbeStep
|
||||
{
|
||||
/**
|
||||
* Skipped while the manifest is red (the probe would fail anyway). A probe with nothing to probe (null) is a
|
||||
* green step that says so, not a skipped one: a skipped step makes the report red and would lock the plugin on a
|
||||
* site that simply has nothing to probe yet.
|
||||
*
|
||||
* @param Closure(): (list<string>|null) $probe Problems, [] = green, null = nothing to probe.
|
||||
* @return Closure(bool): list<CheckStep>
|
||||
*/
|
||||
public static function extra(string $key, string $label, string $labelNothingToProbe, Closure $probe): Closure
|
||||
{
|
||||
return static function (bool $manifestClean) use ($key, $label, $labelNothingToProbe, $probe): array {
|
||||
if (! $manifestClean) {
|
||||
return [new CheckStep($key, [], true, $label)];
|
||||
}
|
||||
|
||||
$none = false;
|
||||
$problems = CompatibilityCheck::guarded(static function () use ($probe, &$none): array {
|
||||
$result = $probe();
|
||||
$none = $result === null;
|
||||
|
||||
return $result ?? [];
|
||||
});
|
||||
|
||||
return [new CheckStep($key, $problems, false, $none ? $labelNothingToProbe : $label)];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ 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). */
|
||||
public const MARKER = '_lit_mcp_check_entry';
|
||||
|
||||
public const TITLE = '[LIT MCP Prüfung]';
|
||||
|
||||
/** Only entries not modified for this long are orphans – a running check's entry is younger. */
|
||||
private const ORPHAN_AGE_SECONDS = 600;
|
||||
|
||||
@@ -68,14 +73,14 @@ final class ThrowawayPost
|
||||
|
||||
/**
|
||||
* @param array<int, string>|string $postTypes
|
||||
* @return list<int> IDs of entries a dead check left behind, deleted now. Only drafts whose title starts with
|
||||
* the prefix are considered.
|
||||
* @return list<int> IDs of entries a dead check left behind, deleted now. Only drafts and private entries (a
|
||||
* probe may pass through private) whose title starts with the prefix are considered.
|
||||
*/
|
||||
public function removeOrphans(array|string $postTypes): array
|
||||
{
|
||||
$posts = get_posts([
|
||||
'post_type' => $postTypes,
|
||||
'post_status' => 'draft',
|
||||
'post_status' => ['draft', 'private'],
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a handful of marked entries at most.
|
||||
'meta_key' => $this->marker,
|
||||
// post_modified_gmt, not post_date_gmt: an edit by the check keeps it current. A literal GMT timestamp,
|
||||
|
||||
@@ -87,7 +87,7 @@ class WriteProbe
|
||||
|
||||
/**
|
||||
* @return list<int> IDs of check pages a dead check left behind, deleted now (ThrowawayPost::removeOrphans():
|
||||
* marked drafts with the check title only).
|
||||
* marked drafts and private pages with the check title only).
|
||||
*/
|
||||
public static function removeOrphans(): array
|
||||
{
|
||||
|
||||
Generated
+4
-19
@@ -6,7 +6,7 @@ namespace LIT\MCP\Integrations\Pods;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||
use LIT\MCP\Compatibility\ProbeStep;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -19,30 +19,15 @@ final class PodsCheckSteps
|
||||
private const LABEL_NO_TARGET = self::LABEL . ' (kein Pod mit Textfeld vorhanden, nicht geprüft)';
|
||||
|
||||
/**
|
||||
* The Profile::$extraSteps of Pods: the probe, skipped while the manifest is red (it would fail anyway). No pod
|
||||
* to probe (null) is a green step that says so, not a skipped one: a skipped step makes the report red and
|
||||
* would lock Pods on a site that simply has no pod yet.
|
||||
* The Profile::$extraSteps of Pods: the probe (ProbeStep: skipped on a red manifest, green with a hint when
|
||||
* there is no pod to probe).
|
||||
*
|
||||
* @param Closure(): (list<string>|null) $probe PodsProbe::run().
|
||||
* @return Closure(bool): list<CheckStep>
|
||||
*/
|
||||
public static function extra(Closure $probe): Closure
|
||||
{
|
||||
return static function (bool $manifestClean) use ($probe): array {
|
||||
if (! $manifestClean) {
|
||||
return [new CheckStep(self::PROBE, [], true, self::LABEL)];
|
||||
}
|
||||
|
||||
$none = false;
|
||||
$problems = CompatibilityCheck::guarded(static function () use ($probe, &$none): array {
|
||||
$result = $probe();
|
||||
$none = $result === null;
|
||||
|
||||
return $result ?? [];
|
||||
});
|
||||
|
||||
return [new CheckStep(self::PROBE, $problems, false, $none ? self::LABEL_NO_TARGET : self::LABEL)];
|
||||
};
|
||||
return ProbeStep::extra(self::PROBE, self::LABEL, self::LABEL_NO_TARGET, $probe);
|
||||
}
|
||||
|
||||
public static function label(CheckStep $step): string
|
||||
|
||||
Generated
+4
-8
@@ -16,7 +16,7 @@ defined('ABSPATH') || exit;
|
||||
* profile again) it loads the schema of an existing pod, writes a plain text field of a throwaway draft entry,
|
||||
* reads it back and compares. The pod and its schema are never touched (no save_pod, so no rewrite flush, no
|
||||
* schema cache change); only the entry is created and always deleted for good (ThrowawayPost). Entries a dead
|
||||
* check left behind carry MARKER; removeOrphans() deletes them. Runs only from the check route
|
||||
* check left behind carry ThrowawayPost::MARKER; removeOrphans() deletes them. Runs only from the check route
|
||||
* (Compatibility::check).
|
||||
*
|
||||
* Target: a pod of a post type Pods created itself (pod arg 'object' empty) comes first, then a foreign type Pods
|
||||
@@ -28,10 +28,6 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class PodsProbe
|
||||
{
|
||||
public const MARKER = '_lit_mcp_check_entry';
|
||||
|
||||
public const TITLE = '[LIT MCP Prüfung]';
|
||||
|
||||
public const VALUE = 'litcheck-pods-probe';
|
||||
|
||||
private const ABORTED = 'Pods-Probe abgebrochen (Details im PHP-Error-Log).';
|
||||
@@ -92,12 +88,12 @@ final class PodsProbe
|
||||
private static function throwaway(): ThrowawayPost
|
||||
{
|
||||
return new ThrowawayPost(
|
||||
self::MARKER,
|
||||
self::TITLE,
|
||||
ThrowawayPost::MARKER,
|
||||
ThrowawayPost::TITLE,
|
||||
self::ABORTED,
|
||||
'Der Probe-Eintrag konnte nicht angelegt werden (Details im PHP-Error-Log).',
|
||||
'Der Probe-Eintrag %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „'
|
||||
. self::TITLE . ' …“).',
|
||||
. ThrowawayPost::TITLE . ' …“).',
|
||||
'pods probe'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
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
|
||||
{
|
||||
public const PROBE = 'probe';
|
||||
|
||||
private const LABEL = 'Leeren-Test an einer Wegwerf-Seite im Cache';
|
||||
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
|
||||
* would lock the purge on a site that caches nothing.
|
||||
*
|
||||
* @param Closure(): (list<string>|null) $probe SuperCacheProbe::run().
|
||||
* @return Closure(bool): list<CheckStep>
|
||||
*/
|
||||
public static function extra(Closure $probe): Closure
|
||||
{
|
||||
return ProbeStep::extra(self::PROBE, self::LABEL, self::LABEL_CACHE_OFF, $probe);
|
||||
}
|
||||
|
||||
public static function label(CheckStep $step): string
|
||||
{
|
||||
// Reports saved before labels were stored have none.
|
||||
return $step->label
|
||||
?? ($step->key === self::PROBE ? self::LABEL : null)
|
||||
?? CheckStep::genericLabel($step->key, SuperCacheCompatibility::LABEL)
|
||||
?? $step->key;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ defined('ABSPATH') || exit;
|
||||
* 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).
|
||||
* (wp-cache-base.php). The check adds a purge probe (SuperCacheProbe).
|
||||
*/
|
||||
final class SuperCacheCompatibility
|
||||
{
|
||||
@@ -55,7 +55,9 @@ final class SuperCacheCompatibility
|
||||
|
||||
return $version === '' ? null : $version;
|
||||
},
|
||||
testedVersions: ['3.1.5']
|
||||
testedVersions: ['3.1.5'],
|
||||
extraSteps: SuperCacheCheckSteps::extra(static fn (): ?array => (new SuperCacheProbe())->run()),
|
||||
stepLabel: SuperCacheCheckSteps::label(...)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,45 @@ 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 only brings the compatibility profile (SuperCacheListener does the
|
||||
* work, wired by Plugin). Without abilities it is no tool group.
|
||||
* 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
|
||||
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';
|
||||
@@ -27,7 +55,7 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return defined('WPCACHEHOME');
|
||||
return ($this->available)();
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user