Files
WP-LIT-MCP/src/Integrations/Divi/Pages/VisibleEdits.php
T
janniclas.loosenandClaude Opus 5.5 b1dd35abd8 feat: Divi tools edit Theme Builder layouts; visible layouts need confirm
TB layouts load through ThemeBuilderAccess (P12), live layouts (attached +
enabled, Q8) need a dry run and confirm (Q9) and get the shared restore point
of VisibleEdits, extracted from GlobalEdits (Q2: backup when revisions are
off, P9 refusals, P14 naming). divi/post-content only in body layouts (F22),
also for nested stored nodes. SaveScope::ThemeBuilderLayout sets Divi flags
and clears site CSS + TB cache.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 15:45:07 +02:00

139 lines
5.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use Closure;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\WordPress\Status\PostCloner;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* The restore point and the confirm merge shared by every Divi write visitors see at once (answer F2
* „vorher Revision/Sicherung“, ruling Q2): global or referenced library entries (GlobalEdits) and layouts
* of live Theme Builder templates (ThemeBuilderEdits). Their own rules decide whether confirm is needed;
* this class holds what is the same for both:
* - the revision is DiviSaver's restore point; when revisions are off for the entry, a backup copy
* (PostCloner, moved to the trash) is made after confirm and before the save – refused while it could
* not be faithful (users without unfiltered_html: its wp_insert_post runs kses) or would not survive
* (trash disabled) – ruling P9, S37;
* - a failed save after that backup names it (ruling P14);
* - a tool's own confirm_required (lit-divi-delete) is extended by the entry's question, so the one
* confirm the client sends covers both.
*/
final class VisibleEdits
{
/** @var Closure(WP_Post): int */
private Closure $backup;
/** @var Closure(): bool */
private Closure $trashEnabled;
/**
* @param (Closure(WP_Post): int)|null $backup Backup copy of the entry; null = PostCloner::backup().
* @param (Closure(): bool)|null $trashEnabled Whether the trash keeps the backup; null = EMPTY_TRASH_DAYS.
*/
public function __construct(?Closure $backup = null, ?Closure $trashEnabled = null)
{
$this->backup = $backup ?? static fn (WP_Post $post): int => (new PostCloner())->backup($post);
$this->trashEnabled = $trashEnabled
?? static fn (): bool => ! defined('EMPTY_TRASH_DAYS') || (bool) EMPTY_TRASH_DAYS;
}
/**
* Ruling P9: a backup (only made when revisions are off) must be faithful and must survive. $label
* names the entry in the message ("Bibliothekseintrag 12"), $hint says what to do instead.
*/
public function backupRefusal(WP_Post $post, string $label, string $hint): ?WP_Error
{
if (wp_revisions_enabled($post)) {
return null;
}
if (! current_user_can('unfiltered_html')) {
return Errors::unsupported(
sprintf(
'Revisionen sind für %s abgeschaltet, und die Sicherungskopie wäre für dieses Konto nicht '
. 'verlustfrei (WordPress filtert HTML); nichts wurde gespeichert.',
$label
),
$hint
);
}
if (! ($this->trashEnabled)()) {
return Errors::unsupported(
sprintf(
'Revisionen sind für %s abgeschaltet, und der Papierkorb ist deaktiviert (EMPTY_TRASH_DAYS = 0) '
. '– die Sicherungskopie würde sofort gelöscht; nichts wurde gespeichert.',
$label
),
$hint
);
}
return null;
}
/**
* The backup of $post when revisions are off for it (call backupRefusal() first): ['backup_id' => …],
* else nothing – the revision DiviSaver takes is the restore point.
*
* @return array{backup_id?: int}
* @throws WriteFailed When the backup could not be made (nothing was saved).
*/
public function backup(WP_Post $post): array
{
return wp_revisions_enabled($post) ? [] : ['backup_id' => ($this->backup)($post)];
}
/**
* A tool's own confirm_required $error extended by $note (appended to its message) and $details (the
* entry's answer fields), keeping the tool's own fields.
*
* @param array<string, mixed> $details
*/
public static function merged(WP_Error $error, string $note, array $details): WP_Error
{
$data = $error->get_error_data();
$own = is_array($data) ? array_diff_key($data, ['status' => true, 'hint' => true]) : [];
return Errors::confirmRequired(Errors::clientMessage($error) . ' ' . $note, array_merge($own, $details));
}
/**
* Ruling P14: a backup was made (revisions off) before the save failed – the error names it, because it
* stays in the trash and is the only earlier state. $entry names what it saved ("des Bibliothekseintrags").
*/
public static function keepingBackup(WriteFailed $e, mixed $backupId, string $entry): WriteFailed
{
if (! is_int($backupId)) {
return $e;
}
$kept = sprintf(
'Sicherung %d liegt im Papierkorb (vorheriger Stand %s, in WordPress wiederherstellbar).',
$backupId,
$entry
);
if ($e->rolledBack) {
return $e->withDetails(
['backup_id' => $backupId],
$kept . ' Den Aufruf erst nach Prüfung mit lit-divi-outline wiederholen.',
Errors::notSavedKeeping(sprintf('Sicherung %d im Papierkorb', $backupId))
);
}
return $e->withDetails(
['backup_id' => $backupId],
'Den Aufruf nicht einfach wiederholen. ' . $kept . ' Zuerst den Stand mit lit-divi-outline prüfen und '
. 'dem Nutzer Bescheid geben.'
);
}
}