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>
This commit is contained in:
@@ -36,6 +36,7 @@ defined('ABSPATH') || exit;
|
||||
class DiviComposition
|
||||
{
|
||||
private ?int $postId = null;
|
||||
private ?string $themeBuilderArea = null;
|
||||
private ?VariableRef $references = null;
|
||||
private ?Placeholders $placeholders = null;
|
||||
private ?ValueCleaner $cleaner = null;
|
||||
@@ -52,11 +53,12 @@ class DiviComposition
|
||||
) {
|
||||
}
|
||||
|
||||
/** A fresh composition for entry $postId (own VariableRef with that entry's live map). */
|
||||
public function forPost(int $postId): static
|
||||
/** A fresh composition for entry $postId (own VariableRef with its live map; TB area for StructureRules). */
|
||||
public function forPost(int $postId, ?string $themeBuilderArea = null): static
|
||||
{
|
||||
$bound = clone $this;
|
||||
$bound->postId = $postId;
|
||||
$bound->themeBuilderArea = $themeBuilderArea;
|
||||
$bound->references = null;
|
||||
$bound->placeholders = null;
|
||||
$bound->cleaner = null;
|
||||
@@ -116,7 +118,7 @@ class DiviComposition
|
||||
/** @internal Public for tests; code uses composer() / editor(). */
|
||||
public function rules(): StructureRules
|
||||
{
|
||||
return new StructureRules($this->modules->catalog());
|
||||
return new StructureRules($this->modules->catalog(), $this->themeBuilderArea);
|
||||
}
|
||||
|
||||
/** E6: the check for stored nodes that move into this composition's entry (same cleaner and references). */
|
||||
|
||||
@@ -22,10 +22,23 @@ final class DiviPage
|
||||
public readonly WP_Post $post,
|
||||
public readonly Document $document,
|
||||
public readonly NodeIndex $index,
|
||||
private bool $libraryItem = false
|
||||
private bool $libraryItem = false,
|
||||
private ?string $themeBuilderArea = null
|
||||
) {
|
||||
}
|
||||
|
||||
/** Whether the entry is a Theme Builder layout (header, body or footer), loaded through ThemeBuilderAccess. */
|
||||
public function isThemeBuilderLayout(): bool
|
||||
{
|
||||
return $this->themeBuilderArea !== null;
|
||||
}
|
||||
|
||||
/** "header", "body" or "footer" for a Theme Builder layout, else null. */
|
||||
public function themeBuilderArea(): ?string
|
||||
{
|
||||
return $this->themeBuilderArea;
|
||||
}
|
||||
|
||||
/** Whether the entry is a Divi Library entry (et_pb_layout), loaded through LibraryAccess. */
|
||||
public function isLibraryItem(): bool
|
||||
{
|
||||
@@ -47,6 +60,11 @@ final class DiviPage
|
||||
|
||||
public function editUrl(): string
|
||||
{
|
||||
if ($this->themeBuilderArea !== null) {
|
||||
// Divi opens TB layouts from the Theme Builder screen (the builder URL needs its nonce).
|
||||
return (string) admin_url('admin.php?page=et_theme_builder');
|
||||
}
|
||||
|
||||
// A library entry has no public permalink (public: false); Divi's library list opens it here.
|
||||
return $this->libraryItem
|
||||
? (string) get_edit_post_link($this->post, 'raw')
|
||||
|
||||
@@ -9,6 +9,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Log;
|
||||
@@ -18,7 +20,8 @@ use WP_Post;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Loads an entry for the Divi tools: Divi Library entries via LibraryAccess (E1/E3), every other
|
||||
* Loads an entry for the Divi tools: Divi Library entries via LibraryAccess (E1/E3), Theme Builder
|
||||
* layouts via ThemeBuilderAccess (Etappe 3b; hidden ones answer like a missing ID, P12), every other
|
||||
* entry with its rights and status via PostGuard (spec §4.5), then the block tree. Divi 4
|
||||
* shortcode content and unbalanced markup are refused with a hint – spec §3.4: Divi 4 content
|
||||
* is kept, never edited.
|
||||
@@ -31,7 +34,8 @@ final class DiviPageLoader
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private Parser $parser = new Parser(),
|
||||
private ?LibraryAccess $library = null
|
||||
private ?LibraryAccess $library = null,
|
||||
private ?ThemeBuilderAccess $themeBuilder = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -50,9 +54,10 @@ final class DiviPageLoader
|
||||
/** Readers pass $forWrite = false (trashed entries may be read, like lit-content-get). */
|
||||
public function load(mixed $postId, bool $forWrite): DiviPage|WP_Error
|
||||
{
|
||||
// Library entries (E1): with a library access, for reading and writing.
|
||||
// Library entries (E1) and Theme Builder layouts (Etappe 3b) through their access, everything else PostGuard.
|
||||
$item = $this->library?->item($postId, $forWrite);
|
||||
$post = $item ?? PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
|
||||
$layout = $item === null ? $this->themeBuilder?->layout($postId, $forWrite) : null;
|
||||
$post = $item ?? $layout ?? PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
|
||||
if ($post instanceof WP_Error) {
|
||||
return $post;
|
||||
}
|
||||
@@ -84,7 +89,9 @@ final class DiviPageLoader
|
||||
);
|
||||
}
|
||||
|
||||
return new DiviPage($post, $document, NodeIndex::build($document), $item instanceof WP_Post);
|
||||
$area = $layout instanceof WP_Post ? DiviThemeBuilder::areaOf((string) $layout->post_type) : null;
|
||||
|
||||
return new DiviPage($post, $document, NodeIndex::build($document), $item instanceof WP_Post, $area);
|
||||
}
|
||||
|
||||
/** The entry loaded for writing and checked by writable() – the refusal of either step. */
|
||||
@@ -99,14 +106,16 @@ final class DiviPageLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional checks for writing: the type must use the Divi builder (library entries always do),
|
||||
* Additional checks for writing: the type must use the Divi builder (library entries and Theme Builder
|
||||
* layouts always do),
|
||||
* the content must be a Divi 5 layout (or empty) and saving must not publish a missed schedule
|
||||
* (spec §2).
|
||||
*/
|
||||
public function writable(DiviPage $page): ?WP_Error
|
||||
{
|
||||
$post = $page->post;
|
||||
if (! $page->isLibraryItem() && ! $this->schema->diviEnabled((string) $post->post_type)) {
|
||||
$builderPost = $page->isLibraryItem() || $page->isThemeBuilderLayout();
|
||||
if (! $builderPost && ! $this->schema->diviEnabled((string) $post->post_type)) {
|
||||
return Errors::unsupported(
|
||||
sprintf('Für den Inhaltstyp "%s" ist der Divi-Builder nicht aktiviert.', $post->post_type),
|
||||
'In Divi → Theme-Optionen → Builder den Builder für diesen Inhaltstyp aktivieren (Administrator).'
|
||||
|
||||
@@ -387,7 +387,8 @@ final class DiviReplaceTextAbility implements Ability
|
||||
}
|
||||
|
||||
/**
|
||||
* A page commit() refused: its message, and for a global library entry the pages it shows on.
|
||||
* A page commit() refused: its message, and for a global library entry the pages it shows on (for a live
|
||||
* Theme Builder layout the templates).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -398,6 +399,16 @@ final class DiviReplaceTextAbility implements Ability
|
||||
return ['ok' => false, 'error' => Errors::clientMessage($error) . ' Diese Seite wurde nicht geändert.'];
|
||||
}
|
||||
|
||||
if (isset($data['templates'])) {
|
||||
// A layout of a live Theme Builder template (Etappe 3b): visible site-wide, like a global entry.
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => Errors::clientMessage($error) . ' Sichtbare Theme-Builder-Layouts nicht mit '
|
||||
. 'lit/divi-replace-text, sondern nach Rückfrage mit lit-divi-update (confirm: true) ändern.',
|
||||
'templates' => $data['templates'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => Errors::clientMessage($error) . ' Globale Bibliothekseinträge nicht mit lit/divi-replace-text, '
|
||||
|
||||
@@ -52,7 +52,8 @@ defined('ABSPATH') || exit;
|
||||
* S5, S7: docs/superpowers/notes/2026-09-26-etappe-2-entscheidungen.md.
|
||||
*
|
||||
* SaveScope: library entries get no Divi flags; a global (or referenced) entry additionally clears
|
||||
* the static CSS of every page and the Theme Builder cache.
|
||||
* the static CSS of every page and the Theme Builder cache; a Theme Builder layout gets the Divi flags
|
||||
* and clears like a global entry.
|
||||
*
|
||||
* If saving throws, the previous content is written back (WriteFailed: rolled back or not).
|
||||
* Not final so abilities can be tested with a mock.
|
||||
|
||||
@@ -36,11 +36,13 @@ abstract class DiviWriteAbility implements Ability
|
||||
return ToolResult::schema([
|
||||
'tree_rev' => ['type' => 'string'],
|
||||
// Global library entries (answer F2): the pages that show the entry, and the backup
|
||||
// made when revisions are off.
|
||||
// made when revisions are off (also for live Theme Builder layouts, ruling Q2).
|
||||
'affected' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||
'affected_count' => ['type' => 'integer'],
|
||||
'hidden_count' => ['type' => 'integer'],
|
||||
'backup_id' => ['type' => 'integer'],
|
||||
// Theme Builder layouts: the enabled templates that show the layout.
|
||||
'templates' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||
], ['tree_rev']);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
use WP_Error;
|
||||
|
||||
@@ -25,7 +26,8 @@ defined('ABSPATH') || exit;
|
||||
* tree – only when its whole input validated (all or nothing) –, serialize, save via DiviSaver,
|
||||
* then lit_mcp_after_save, change log and the ToolResult with changed nodes, affected outline,
|
||||
* quality warnings (spec §5.5), revision_id, the new rev and tree_rev. commit() is the entry point
|
||||
* of the tools: library entries additionally pass GlobalEdits (shape, dry run/confirm, backup).
|
||||
* of the tools: library entries additionally pass GlobalEdits (shape, dry run/confirm, backup), Theme
|
||||
* Builder layouts ThemeBuilderEdits (live layouts: dry run/confirm, backup; ruling Q2/Q9).
|
||||
*/
|
||||
final class DiviWriter
|
||||
{
|
||||
@@ -38,7 +40,8 @@ final class DiviWriter
|
||||
private ChangeLog $log,
|
||||
private DesignSystemReader $design,
|
||||
private DiviComposition $composition,
|
||||
private ?GlobalEdits $globals = null
|
||||
private ?GlobalEdits $globals = null,
|
||||
private ?ThemeBuilderEdits $themeBuilder = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -67,11 +70,12 @@ final class DiviWriter
|
||||
|
||||
/**
|
||||
* The Compose classes for $page: bound to its entry, so placeholders and $variable(…)$
|
||||
* references are checked against Divi's dynamic content options for it (ruling S22).
|
||||
* references are checked against Divi's dynamic content options for it (ruling S22); a Theme
|
||||
* Builder layout also by its area (F22: modules Divi renders only elsewhere are refused).
|
||||
*/
|
||||
public function composition(DiviPage $page): DiviComposition
|
||||
{
|
||||
return $this->composition->forPost((int) $page->post->ID);
|
||||
return $this->composition->forPost((int) $page->post->ID, $page->themeBuilderArea());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +155,7 @@ final class DiviWriter
|
||||
/**
|
||||
* save() for the Divi tools (E2): pages as before; a library entry keeps its shape, a global one
|
||||
* needs confirm (dry run first), gets a backup when revisions are off and clears every page's
|
||||
* cache; the result then names the affected pages.
|
||||
* cache; the result then names the affected pages. Theme Builder layouts: commitLayout().
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return array<string, mixed>|WP_Error
|
||||
@@ -159,6 +163,10 @@ final class DiviWriter
|
||||
*/
|
||||
public function commit(string $ability, DiviPage $page, DiviChange $change, array $input): array|WP_Error
|
||||
{
|
||||
if ($page->isThemeBuilderLayout()) {
|
||||
return $this->commitLayout($ability, $page, $change, $input);
|
||||
}
|
||||
|
||||
if (! $page->isLibraryItem()) {
|
||||
return $this->save($ability, $page, $change, $input);
|
||||
}
|
||||
@@ -179,48 +187,71 @@ final class DiviWriter
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Bibliothekseintrags');
|
||||
}
|
||||
|
||||
/**
|
||||
* save() for a Theme Builder layout: a layout visitors see needs confirm and gets the restore point
|
||||
* of a visible change (ThemeBuilderEdits, rulings Q2/Q9); the result names the templates that show it.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return array<string, mixed>|WP_Error
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function commitLayout(string $ability, DiviPage $page, DiviChange $change, array $input): array|WP_Error
|
||||
{
|
||||
if ($this->themeBuilder === null) {
|
||||
return Errors::unsupported(
|
||||
'Theme-Builder-Layouts lassen sich über diese Verbindung nicht ändern.',
|
||||
'Das Layout im Divi Theme Builder bearbeiten.'
|
||||
);
|
||||
}
|
||||
|
||||
if (Serializer::document($page->document) === (string) $page->post->post_content) {
|
||||
return $this->save($ability, $page, $change, $input, SaveScope::ThemeBuilderLayout);
|
||||
}
|
||||
|
||||
$prepared = $this->themeBuilder->prepare($page, $input);
|
||||
if ($prepared instanceof WP_Error) {
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Theme-Builder-Layouts');
|
||||
}
|
||||
|
||||
/**
|
||||
* save() with the scope GlobalEdits/ThemeBuilderEdits prepared; their details join the result, and a
|
||||
* failure after their backup names it (ruling P14, VisibleEdits). $entry names the entry in that error.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @param array{scope: SaveScope, details: array<string, mixed>} $prepared
|
||||
* @return array<string, mixed>
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function savePrepared(
|
||||
string $ability,
|
||||
DiviPage $page,
|
||||
DiviChange $change,
|
||||
array $input,
|
||||
array $prepared,
|
||||
string $entry
|
||||
): array {
|
||||
try {
|
||||
$saved = $this->save($ability, $page, $change, $input, $prepared['scope']);
|
||||
} catch (WriteFailed $e) {
|
||||
throw self::keepingBackup($e, $prepared['details']['backup_id'] ?? null);
|
||||
throw VisibleEdits::keepingBackup($e, $prepared['details']['backup_id'] ?? null, $entry);
|
||||
}
|
||||
|
||||
return array_merge($saved, $prepared['details']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P14: GlobalEdits made a backup (revisions off) before the save failed – the error names
|
||||
* it, because it stays in the trash and is the only earlier state.
|
||||
*/
|
||||
private static function keepingBackup(WriteFailed $e, mixed $backupId): WriteFailed
|
||||
{
|
||||
if (! is_int($backupId)) {
|
||||
return $e;
|
||||
}
|
||||
|
||||
$kept = sprintf(
|
||||
'Sicherung %d liegt im Papierkorb (vorheriger Stand des Bibliothekseintrags, in WordPress '
|
||||
. 'wiederherstellbar).',
|
||||
$backupId
|
||||
);
|
||||
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.'
|
||||
);
|
||||
}
|
||||
|
||||
/** A tool's confirm_required on a global library entry, extended by the affected pages. */
|
||||
/** A tool's confirm_required on a global library entry or a live Theme Builder layout, extended by who sees it. */
|
||||
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
||||
{
|
||||
if ($page->isThemeBuilderLayout()) {
|
||||
return $this->themeBuilder?->notice($page, $error) ?? $error;
|
||||
}
|
||||
|
||||
return $page->isLibraryItem() && $this->globals !== null ? $this->globals->notice($page, $error) : $error;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,16 @@ enum SaveScope
|
||||
/** A global library entry: like LibraryItem, plus the CSS of every page and the Theme Builder cache (spec §5.6). */
|
||||
case GlobalItem;
|
||||
|
||||
/** A Theme Builder layout: Divi flags like the builder (finding 4); shown site-wide – all CSS, TB cache. */
|
||||
case ThemeBuilderLayout;
|
||||
|
||||
public function setsFlags(): bool
|
||||
{
|
||||
return $this === self::Page;
|
||||
return $this === self::Page || $this === self::ThemeBuilderLayout;
|
||||
}
|
||||
|
||||
public function clearsSiteCss(): bool
|
||||
{
|
||||
return $this === self::GlobalItem;
|
||||
return $this === self::GlobalItem || $this === self::ThemeBuilderLayout;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user