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>
139 lines
5.3 KiB
PHP
139 lines
5.3 KiB
PHP
<?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.'
|
||
);
|
||
}
|
||
}
|