fix: never publish a missed schedule, refuse trashed posts, fail on meta read errors

- PostGuard::editable() refuses trashed entries unless the caller opts in
  (content-get, revisions-list, post-trash); publish/schedule/unpublish/update/
  restore/duplicate/replace now refuse them.
- PostGuard::statusStable(): content-update, revision-restore and
  replace-with-copy refuse a future post whose date has passed (any save would
  publish it); the content-update rollback and PostCloner row writes re-check.
- PostCloner::ownMeta() treats a failed SELECT as an error; replace() reads the
  copy before its first write.
- replace-with-copy needs create_posts (backup clone) and reports backup_id in
  write_failed/partial_write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 01:20:21 +02:00
co-authored by Claude Opus 5.5
parent 6a7193424c
commit d831cf59ca
24 changed files with 501 additions and 43 deletions
+8 -3
View File
@@ -98,15 +98,20 @@ final class Errors
return self::make('unsupported', $message, $hint, 400);
}
/** A write failed and was rolled back completely. */
public static function writeFailed(string $reason): WP_Error
/**
* A write failed and was rolled back completely.
*
* @param array<string, mixed> $details Extra error details (e.g. a backup that stays).
*/
public static function writeFailed(string $reason, array $details = []): WP_Error
{
return self::make(
'write_failed',
trim('Speichern fehlgeschlagen: ' . $reason) . ' Nichts wurde gespeichert.',
'Eingaben prüfen, ggf. korrigieren und erneut versuchen; tritt der Fehler wieder auf, den Administrator '
. 'informieren (Details im PHP-Error-Log der Website).',
500
500,
$details
);
}
+4 -4
View File
@@ -13,12 +13,12 @@ use WP_Error;
* written. getMessage() is the internal detail for the PHP error log; $reason is the
* client-safe German text. The AbilityRegistrar logs the detail and returns toError():
* lit_write_failed when everything was rolled back, lit_partial_write otherwise.
* $state/$hint/$details customise the partial-write error for writes that are not about a
* post (see Errors::partialWrite()).
* $state/$hint customise the partial-write error for writes that are not about a post (see
* Errors::partialWrite()); $details are added to either error.
*/
final class WriteFailed extends RuntimeException
{
/** @param array<string, mixed> $details Extra error details for an incomplete rollback. */
/** @param array<string, mixed> $details Extra error details for the client. */
public function __construct(
public readonly string $reason,
public readonly bool $rolledBack,
@@ -41,7 +41,7 @@ final class WriteFailed extends RuntimeException
public function toError(): WP_Error
{
return $this->rolledBack
? Errors::writeFailed($this->reason)
? Errors::writeFailed($this->reason, $this->details)
: Errors::partialWrite(
$this->reason,
$this->postId,