fix(compat): Bereinigung liegengebliebener Prüf-Einträge löschte laufende Prüfungen
Entwürfe haben in WordPress kein gültiges GMT-Datum (0000-00-00), deshalb hielt die Orphan-Bereinigung jeden markierten Prüf-Eintrag für alt und konnte eine laufende Divi-Prüfung abbrechen. Der neue Kern-Baustein ThrowawayPost setzt beim Anlegen ein echtes GMT-Datum, prüft das Alter über post_modified_gmt und kapselt Anlegen, Prüfen und Löschen (Anlegen jetzt innerhalb von try). WriteProbe nutzt ihn; Fehlermeldungen von WordPress gehen nur noch ins Error-Log. Live in DDEV belegt: frischer Eintrag bleibt, 11 Minuten alter wird gelöscht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
239d35c198
commit
42aa4f8dc9
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Compatibility;
|
||||
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The life cycle of a check probe's throwaway entry: a marked draft is created, exercised and always deleted for
|
||||
* good (finally, not to the trash, with its revisions and meta). Entries a dead check left behind carry the marker;
|
||||
* removeOrphans() deletes them. Nothing variable (exception or WP_Error text, read values) enters a report – it
|
||||
* reaches MCP clients through the lock reason – the details go to the PHP error log.
|
||||
*
|
||||
* Creating and deleting fire the hooks of every other plugin on the site (save_post, delete_post …); that cannot be
|
||||
* avoided with a real wp_insert_post()/wp_delete_post().
|
||||
*/
|
||||
final class ThrowawayPost
|
||||
{
|
||||
/** Only entries not modified for this long are orphans – a running check's entry is younger. */
|
||||
private const ORPHAN_AGE_SECONDS = 600;
|
||||
|
||||
/**
|
||||
* @param string $marker Meta key of the entries (only a query hint, the title prefix decides).
|
||||
* @param string $titlePrefix Title of the entries starts with it.
|
||||
* @param string $abortedText Report text when the exercise (or the creation) threw.
|
||||
* @param string $createFailedText Report text when the entry could not be created.
|
||||
* @param string $deleteFailedFormat sprintf format with %d for the ID of an entry that could not be deleted.
|
||||
* @param string $logContext Context of the log lines.
|
||||
*/
|
||||
public function __construct(
|
||||
private string $marker,
|
||||
private string $titlePrefix,
|
||||
private string $abortedText,
|
||||
private string $createFailedText,
|
||||
private string $deleteFailedFormat,
|
||||
private string $logContext
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(int): list<string> $exercise Gets the ID of the draft, returns the problems found.
|
||||
* @return list<string> Fixed German problem texts; [] = green.
|
||||
*/
|
||||
public function run(string $postType, callable $exercise): array
|
||||
{
|
||||
$id = 0;
|
||||
$problems = [];
|
||||
try {
|
||||
$id = $this->create($postType);
|
||||
$problems = $id > 0 ? $exercise($id) : [$this->createFailedText];
|
||||
} catch (Throwable $e) {
|
||||
Log::failure($this->logContext, $e);
|
||||
$problems = [$this->abortedText];
|
||||
} finally {
|
||||
if ($id > 0 && ! self::delete($id)) {
|
||||
$problems[] = sprintf($this->deleteFailedFormat, $id);
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public function removeOrphans(array|string $postTypes): array
|
||||
{
|
||||
$posts = get_posts([
|
||||
'post_type' => $postTypes,
|
||||
'post_status' => 'draft',
|
||||
// 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,
|
||||
// because a relative string would be read in the site's time zone against a GMT column.
|
||||
'date_query' => [[
|
||||
'before' => gmdate('Y-m-d H:i:s', time() - self::ORPHAN_AGE_SECONDS),
|
||||
'column' => 'post_modified_gmt',
|
||||
]],
|
||||
'posts_per_page' => 20,
|
||||
'no_found_rows' => true,
|
||||
'suppress_filters' => true,
|
||||
]);
|
||||
|
||||
$removed = [];
|
||||
foreach (is_array($posts) ? $posts : [] as $post) {
|
||||
if (! $post instanceof WP_Post || ! str_starts_with((string) $post->post_title, $this->titlePrefix)) {
|
||||
continue;
|
||||
}
|
||||
if (self::delete((int) $post->ID)) {
|
||||
$removed[] = (int) $post->ID;
|
||||
}
|
||||
}
|
||||
|
||||
return $removed;
|
||||
}
|
||||
|
||||
/** @return int The new ID; 0 when WordPress refused (logged). */
|
||||
private function create(string $postType): int
|
||||
{
|
||||
$id = wp_insert_post([
|
||||
'post_type' => $postType,
|
||||
'post_status' => 'draft',
|
||||
// Without it a draft keeps post_date_gmt and post_modified_gmt at 0000-00-00 and looks ancient.
|
||||
'post_date_gmt' => gmdate('Y-m-d H:i:s'),
|
||||
'post_title' => $this->titlePrefix . ' ' . gmdate('Y-m-d H:i'),
|
||||
'post_content' => '',
|
||||
'meta_input' => [$this->marker => '1'],
|
||||
], true);
|
||||
if ($id instanceof WP_Error) {
|
||||
Log::failure($this->logContext, 'could not create the entry: ' . $id->get_error_message());
|
||||
|
||||
return 0;
|
||||
}
|
||||
if ((int) $id <= 0) {
|
||||
Log::failure($this->logContext, 'could not create the entry: no ID');
|
||||
}
|
||||
|
||||
return max(0, (int) $id);
|
||||
}
|
||||
|
||||
private static function delete(int $id): bool
|
||||
{
|
||||
return wp_delete_post($id, true) instanceof WP_Post;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user