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>
174 lines
6.2 KiB
PHP
174 lines
6.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Pods;
|
||
|
||
use LIT\MCP\Compatibility\ThrowawayPost;
|
||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||
use LIT\MCP\Log;
|
||
use Throwable;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Behaviour probe of the Pods check: through PodsFieldSource (always usable here – the probe is what frees a locked
|
||
* 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 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
|
||
* only extends (object = the type, e.g. an events plugin), last post and page. Creating and deleting the entry
|
||
* fires the hooks of other plugins on that type (unavoidable with wp_insert_post/wp_delete_post), and Pods may
|
||
* check other required fields on a partial save than the plain text field the probe writes.
|
||
*
|
||
* shortcut: relations (output ids) are not probed – they need a second pod entry; add when a Pods update breaks them.
|
||
*/
|
||
final class PodsProbe
|
||
{
|
||
public const VALUE = 'litcheck-pods-probe';
|
||
|
||
private const ABORTED = 'Pods-Probe abgebrochen (Details im PHP-Error-Log).';
|
||
|
||
private const RANK_OWN = 0;
|
||
private const RANK_EXTENDED = 1;
|
||
private const RANK_CORE = 2;
|
||
|
||
/** @var callable(string): mixed */
|
||
private $loadPod;
|
||
|
||
/** @var callable(): list<string> */
|
||
private $postTypes;
|
||
|
||
/**
|
||
* @param callable(string): mixed|null $loadPod Pods schema of a post type (raw field types).
|
||
* @param callable(): list<string>|null $postTypes Post types to look at; the best-ranked target wins, then the
|
||
* first of them.
|
||
*/
|
||
public function __construct(
|
||
private PodsFieldSource $source,
|
||
?callable $loadPod = null,
|
||
?callable $postTypes = null
|
||
) {
|
||
$this->loadPod = $loadPod ?? static fn (string $name): mixed => pods_api()->load_pod(['name' => $name]);
|
||
$this->postTypes = $postTypes ?? static fn (): array => array_values(array_unique([
|
||
...array_values(get_post_types(['_builtin' => false])),
|
||
'post',
|
||
'page',
|
||
]));
|
||
}
|
||
|
||
/** @return list<string>|null German problems, [] = green; null = no pod with a plain text field to probe. */
|
||
public function run(): ?array
|
||
{
|
||
try {
|
||
$target = $this->target();
|
||
} catch (Throwable $e) {
|
||
Log::failure('pods probe', $e);
|
||
|
||
return [self::ABORTED];
|
||
}
|
||
if ($target === null) {
|
||
return null;
|
||
}
|
||
|
||
[$postType, $field] = $target;
|
||
|
||
return self::throwaway()->run($postType, fn (int $id): array => $this->exercise($id, $postType, $field));
|
||
}
|
||
|
||
/** @return list<int> IDs of probe entries a dead check left behind, deleted now. */
|
||
public static function removeOrphans(): array
|
||
{
|
||
return self::throwaway()->removeOrphans(array_values(get_post_types()));
|
||
}
|
||
|
||
private static function throwaway(): ThrowawayPost
|
||
{
|
||
return new ThrowawayPost(
|
||
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 „'
|
||
. ThrowawayPost::TITLE . ' …“).',
|
||
'pods probe'
|
||
);
|
||
}
|
||
|
||
/** @return array{0: string, 1: FieldDefinition}|null Post type and field of the best-ranked text pod. */
|
||
private function target(): ?array
|
||
{
|
||
$best = null;
|
||
$bestRank = PHP_INT_MAX;
|
||
foreach (($this->postTypes)() as $postType) {
|
||
$pod = ($this->loadPod)($postType);
|
||
$field = is_object($pod) ? $this->textField($postType, $pod) : null;
|
||
if ($field === null) {
|
||
continue;
|
||
}
|
||
|
||
$rank = self::rank($postType, $pod);
|
||
if ($rank === self::RANK_OWN) {
|
||
return [$postType, $field];
|
||
}
|
||
if ($rank < $bestRank) {
|
||
[$best, $bestRank] = [[$postType, $field], $rank];
|
||
}
|
||
}
|
||
|
||
return $best;
|
||
}
|
||
|
||
private static function rank(string $postType, object $pod): int
|
||
{
|
||
if ((string) $pod->get_arg('object', '') === '') {
|
||
return self::RANK_OWN;
|
||
}
|
||
|
||
return in_array($postType, ['post', 'page'], true) ? self::RANK_CORE : self::RANK_EXTENDED;
|
||
}
|
||
|
||
private function textField(string $postType, object $pod): ?FieldDefinition
|
||
{
|
||
$definitions = [];
|
||
foreach ($this->source->fieldsFor($postType) as $definition) {
|
||
$definitions[$definition->key] = $definition;
|
||
}
|
||
foreach ($pod->get_fields() as $field) {
|
||
// Raw type 'text' only: email, slug, website … validate or rewrite the value.
|
||
$definition = $definitions[(string) $field->get_name()] ?? null;
|
||
if (
|
||
$definition !== null && (string) $field->get_type() === 'text'
|
||
&& $definition->status === FieldDefinition::WRITABLE
|
||
&& ! $definition->multiple
|
||
) {
|
||
return $definition;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/** @return list<string> */
|
||
private function exercise(int $id, string $postType, FieldDefinition $field): array
|
||
{
|
||
$this->source->write($id, $postType, [$field->key => self::VALUE], [$field->key => $field]);
|
||
$read = $this->source->read($id, $field);
|
||
if ($read === self::VALUE) {
|
||
return [];
|
||
}
|
||
|
||
// The field name and the value stay in the log: the report reaches MCP clients.
|
||
Log::line(sprintf(
|
||
'pods probe: field %s read back %s instead of the written value.',
|
||
$field->key,
|
||
is_scalar($read) ? '"' . $read . '"' : gettype($read)
|
||
));
|
||
|
||
return ['Gelesener Wert weicht vom geschriebenen ab.'];
|
||
}
|
||
}
|