feat: lit/library-save creates library entries like Divi (E4)
Reuses NewDraft for insert and rollback (P6), requires edit_global_library for scope global (P7), refuses opaque and global roots before wrapping (P11) and checks the node with StoredNodeGuard. The source page is never written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,15 +100,7 @@ final class DiviIntegration implements Integration
|
||||
public function abilities(): array
|
||||
{
|
||||
$loader = $this->loader();
|
||||
$composition = new DiviComposition($this->modules, $this->design);
|
||||
$writer = new DiviWriter(
|
||||
$loader,
|
||||
new DiviSaver(),
|
||||
$this->log,
|
||||
$this->design,
|
||||
$composition,
|
||||
new GlobalEdits($this->access, new GlobalUsages())
|
||||
);
|
||||
$writer = $this->writer($loader);
|
||||
|
||||
return [
|
||||
new DiviOutlineAbility($loader, $this->design),
|
||||
@@ -127,7 +119,29 @@ final class DiviIntegration implements Integration
|
||||
/** The group "Divi-Bibliothek" (lit/library-*), sharing this integration's library access. */
|
||||
public function library(): LibraryIntegration
|
||||
{
|
||||
return new LibraryIntegration($this->access, $this->loader(), $this->design);
|
||||
$loader = $this->loader();
|
||||
|
||||
return new LibraryIntegration(
|
||||
$this->access,
|
||||
$loader,
|
||||
$this->design,
|
||||
$this->writer($loader),
|
||||
new DiviSaver(),
|
||||
$this->log,
|
||||
$this->modules
|
||||
);
|
||||
}
|
||||
|
||||
private function writer(DiviPageLoader $loader): DiviWriter
|
||||
{
|
||||
return new DiviWriter(
|
||||
$loader,
|
||||
new DiviSaver(),
|
||||
$this->log,
|
||||
$this->design,
|
||||
new DiviComposition($this->modules, $this->design),
|
||||
new GlobalEdits($this->access, new GlobalUsages())
|
||||
);
|
||||
}
|
||||
|
||||
private function loader(): DiviPageLoader
|
||||
|
||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
@@ -126,6 +131,50 @@ class DiviLibraryStore
|
||||
];
|
||||
}
|
||||
|
||||
/** Client text when the terms or meta of a new entry could not be written. */
|
||||
public const REASON_CREATE = 'Der Bibliothekseintrag konnte nicht vollständig angelegt werden.';
|
||||
|
||||
/**
|
||||
* A published entry with the terms and meta of $spec and empty content (E4; the layout follows
|
||||
* through DiviSaver::saveNew()). All or nothing through $draft (ruling P6, the transaction of
|
||||
* lit/content-create): a refused insert or missing taxonomies create nothing; a failed term or
|
||||
* meta write deletes the entry again (write_failed), or reports partial_write and logs it
|
||||
* when that delete fails; a throwing insert hook is a partial write without ID.
|
||||
*
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
public function create(LibraryItemSpec $spec, NewDraft $draft): int
|
||||
{
|
||||
$missing = $this->missingTaxonomies();
|
||||
if ($missing !== []) {
|
||||
throw WriteFailed::rolledBack(
|
||||
'Die Divi-Bibliothek ist in dieser Anfrage nicht geladen.',
|
||||
detail: 'Divi Library taxonomies not registered: ' . implode(', ', $missing)
|
||||
);
|
||||
}
|
||||
|
||||
$id = $draft->insert([
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $spec->title,
|
||||
'post_content' => '',
|
||||
]);
|
||||
if ($id instanceof WP_Error) {
|
||||
throw WriteFailed::rolledBack(
|
||||
'WordPress hat den Bibliothekseintrag abgelehnt.',
|
||||
detail: 'wp_insert_post refused the library entry: ' . $id->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->describeNew($id, $spec);
|
||||
} catch (Throwable $e) {
|
||||
$draft->discard($id, $e, self::REASON_CREATE);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Library taxonomies not registered in this request (F2: Divi registers them in
|
||||
* ET_Builder_Library; fixture finding 6: not in every context).
|
||||
@@ -140,6 +189,27 @@ class DiviLibraryStore
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Terms and meta of $spec on the new entry $id, each checked.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function describeNew(int $id, LibraryItemSpec $spec): void
|
||||
{
|
||||
foreach ($spec->terms() as $taxonomy => $terms) {
|
||||
$result = wp_set_object_terms($id, $terms, $taxonomy);
|
||||
if ($result instanceof WP_Error) {
|
||||
throw new RuntimeException(sprintf('terms %s: %s', $taxonomy, $result->get_error_message()));
|
||||
}
|
||||
}
|
||||
foreach ($spec->meta() as $key => $value) {
|
||||
update_post_meta($id, $key, $value);
|
||||
if ((string) get_post_meta($id, $key, true) !== $value) {
|
||||
throw new RuntimeException(sprintf('meta %s of library entry %d not stored', $key, $id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<WP_Term> */
|
||||
private function terms(int $id, string $taxonomy): array
|
||||
{
|
||||
|
||||
@@ -120,6 +120,34 @@ class LibraryAccess
|
||||
return $this->isGlobal($item) ? $this->roles->refusal('edit_global_library') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating an entry: canList(), publish_posts of et_pb_layout (library entries are always
|
||||
* published, F2), the role option save_library and the Tools-tab gate; a global entry
|
||||
* additionally edit_global_library (ruling P7 – the same option as for editing one, F12).
|
||||
*/
|
||||
public function canSave(bool $global = false): ?WP_Error
|
||||
{
|
||||
$refusal = $this->canList();
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
$type = get_post_type_object(DiviLibraryStore::POST_TYPE);
|
||||
$publish = is_object($type) && isset($type->cap->publish_posts)
|
||||
? (string) $type->cap->publish_posts
|
||||
: 'publish_posts';
|
||||
if (! current_user_can($publish)) {
|
||||
return Errors::forbidden('Keine Berechtigung, Einträge der Divi-Bibliothek anzulegen (publish_posts).');
|
||||
}
|
||||
|
||||
$refusal = $this->roles->refusal('save_library') ?? $this->writableGate();
|
||||
if ($refusal !== null || ! $global) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
return $this->roles->refusal('edit_global_library');
|
||||
}
|
||||
|
||||
/** Refusal while the Tools tab keeps this connection from writing to the library. */
|
||||
public function writableGate(): ?WP_Error
|
||||
{
|
||||
|
||||
@@ -4,15 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The group "Divi-Bibliothek" (E9): lit/library-list, later lit/library-save and lit-library-insert.
|
||||
* The group "Divi-Bibliothek" (E9): lit/library-list, lit/library-save, later lit-library-insert.
|
||||
* Built by DiviIntegration::library() so it shares the library access (rights, Tools-tab gates –
|
||||
* its id LibraryAccess::GROUP is what the P9 hiding and the write gate ask). Available with the
|
||||
* Divi 5 builder, like the Divi group.
|
||||
@@ -22,7 +25,11 @@ final class LibraryIntegration implements Integration
|
||||
public function __construct(
|
||||
private LibraryAccess $access,
|
||||
private DiviPageLoader $loader,
|
||||
private DesignSystemReader $design
|
||||
private DesignSystemReader $design,
|
||||
private DiviWriter $writer,
|
||||
private DiviSaver $saver,
|
||||
private ChangeLog $log,
|
||||
private DiviModules $modules
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -45,6 +52,14 @@ final class LibraryIntegration implements Integration
|
||||
{
|
||||
return [
|
||||
new LibraryListAbility($this->access, $this->loader, $this->design, new GlobalUsages()),
|
||||
new LibrarySaveAbility(
|
||||
$this->access,
|
||||
$this->loader,
|
||||
$this->writer,
|
||||
$this->saver,
|
||||
$this->log,
|
||||
$this->modules
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Title, terms and meta of a new library entry, like Divi's et_pb_submit_layout() writes them
|
||||
* (builder/shortcode-core.php:654-734; compared with the real builder entries, fixtures stage3):
|
||||
* terms scope (global / non_global), layout_type, module_width, optional layout_category/layout_tag;
|
||||
* meta _et_pb_module_type (modules), _et_pb_excluded_global_options = "[]" (global modules),
|
||||
* _et_pb_built_for_post_type, _et_pb_template_type. _et_pb_row_layout is left out – Divi 5.2.1 stores
|
||||
* "[object Object]" there (finding 5). Pure PHP.
|
||||
*/
|
||||
final class LibraryItemSpec
|
||||
{
|
||||
private const MODULE_CATEGORIES = ['module', 'fullwidth-module'];
|
||||
|
||||
/**
|
||||
* @param list<int> $categories layout_category term IDs.
|
||||
* @param list<int> $tags layout_tag term IDs.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $title,
|
||||
public readonly string $layoutType,
|
||||
public readonly bool $global,
|
||||
public readonly string $moduleWidth,
|
||||
public readonly ?string $moduleType,
|
||||
public readonly string $builtFor,
|
||||
public readonly array $categories = [],
|
||||
public readonly array $tags = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry that holds $node, or the German reason it cannot be saved: sections, rows and
|
||||
* modules only (Divi's "add to library"); a global module is already an entry; a global entry
|
||||
* holds no global module (F10). Opaque roots and global instances are refused before anything
|
||||
* wraps them (ruling P11), so no entry ever starts with an instance.
|
||||
*/
|
||||
public static function forNode(
|
||||
Block $node,
|
||||
ModuleCatalog $catalog,
|
||||
string $title,
|
||||
bool $global,
|
||||
string $builtFor
|
||||
): self|string {
|
||||
if ($node->isGlobal()) {
|
||||
$ref = NodeSummary::globalRef($node);
|
||||
|
||||
return sprintf(
|
||||
'Dieser Knoten ist bereits ein globales Modul (Bibliothekseintrag %s); ihn mit lit-library-insert '
|
||||
. 'einfügen statt erneut speichern.',
|
||||
$ref['id'] ?? '?'
|
||||
);
|
||||
}
|
||||
if ($node->isOpaque()) {
|
||||
return sprintf(
|
||||
'Die Einstellungen von %s sind nicht lesbar; der Knoten lässt sich nicht in der Bibliothek speichern.',
|
||||
$node->name()
|
||||
);
|
||||
}
|
||||
|
||||
$name = $node->name();
|
||||
if ($name === 'divi/section') {
|
||||
$type = 'section';
|
||||
$width = AttrMap::desktop($node->attrs(), 'module.advanced.type') === 'fullwidth' ? 'fullwidth' : 'regular';
|
||||
} elseif ($name === 'divi/row') {
|
||||
$type = 'row';
|
||||
$width = 'regular';
|
||||
} elseif ($catalog->has($name) && in_array($catalog->category($name), self::MODULE_CATEGORIES, true)) {
|
||||
$type = 'module';
|
||||
$width = $catalog->category($name) === 'fullwidth-module' ? 'fullwidth' : 'regular';
|
||||
} else {
|
||||
return sprintf(
|
||||
'%s lässt sich nicht einzeln in der Bibliothek speichern (nur Abschnitte, Zeilen und Module).',
|
||||
$name
|
||||
);
|
||||
}
|
||||
|
||||
if ($global && LibraryContent::containsGlobal($node)) {
|
||||
return 'Ein globaler Bibliothekseintrag darf keine globalen Module enthalten (Divi erlaubt keine '
|
||||
. 'Verschachtelung); als normalen Eintrag speichern.';
|
||||
}
|
||||
|
||||
return new self($title, $type, $global, $width, $type === 'module' ? $name : null, $builtFor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $categories
|
||||
* @param list<int> $tags
|
||||
*/
|
||||
public function withTerms(array $categories, array $tags): self
|
||||
{
|
||||
return new self(
|
||||
$this->title,
|
||||
$this->layoutType,
|
||||
$this->global,
|
||||
$this->moduleWidth,
|
||||
$this->moduleType,
|
||||
$this->builtFor,
|
||||
$categories,
|
||||
$tags
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, list<int|string>> Taxonomy => slugs (system terms) or term IDs. */
|
||||
public function terms(): array
|
||||
{
|
||||
$terms = [
|
||||
'scope' => [$this->global ? 'global' : 'non_global'],
|
||||
'layout_type' => [$this->layoutType],
|
||||
'module_width' => [$this->moduleWidth],
|
||||
];
|
||||
if ($this->categories !== []) {
|
||||
$terms['layout_category'] = $this->categories;
|
||||
}
|
||||
if ($this->tags !== []) {
|
||||
$terms['layout_tag'] = $this->tags;
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function meta(): array
|
||||
{
|
||||
$meta = [];
|
||||
if ($this->moduleType !== null) {
|
||||
$meta['_et_pb_module_type'] = $this->moduleType;
|
||||
}
|
||||
if ($this->global && $this->layoutType === 'module') {
|
||||
$meta['_et_pb_excluded_global_options'] = '[]';
|
||||
}
|
||||
$meta['_et_pb_built_for_post_type'] = $this->builtFor;
|
||||
$meta['_et_pb_template_type'] = $this->layoutType;
|
||||
|
||||
return $meta;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Term;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* lit/library-save: a node of a Divi page (or library entry) becomes a new Divi Library entry,
|
||||
* normal or global (spec §5.7, E4). The entry gets the node's stored bytes in the
|
||||
* divi/placeholder frame (LibraryContent::wrap, line breaks between blocks unified like
|
||||
* LibraryContent::load), saved through DiviSaver::saveNew with SaveScope::LibraryItem (the one
|
||||
* content write, D7; no Divi flags). The node is checked by StoredNodeGuard (E6) because it moves
|
||||
* to another post. The source is never written – linking it to the entry is
|
||||
* lit/library-make-global (Etappe 3b). All or nothing through NewDraft (ruling P6): any failure
|
||||
* after the insert deletes the new entry, a failed delete is an honest partial_write.
|
||||
*/
|
||||
final class LibrarySaveAbility implements Ability
|
||||
{
|
||||
private const TYPE_LABELS = ['section' => 'Abschnitt', 'row' => 'Zeile', 'module' => 'Modul'];
|
||||
|
||||
private const SCOPES = ['normal', 'global'];
|
||||
|
||||
private const REASON_LAYOUT = 'Der Inhalt des Bibliothekseintrags konnte nicht gespeichert werden.';
|
||||
|
||||
public function __construct(
|
||||
private LibraryAccess $access,
|
||||
private DiviPageLoader $loader,
|
||||
private DiviWriter $writer,
|
||||
private DiviSaver $saver,
|
||||
private ChangeLog $log,
|
||||
private DiviModules $modules
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'lit/library-save';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'In der Divi-Bibliothek speichern';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Speichert einen Divi-Knoten (Abschnitt, Zeile oder Modul) als neuen Eintrag der Divi-Bibliothek – '
|
||||
. 'normal oder global (scope). Der Knoten auf der Seite bleibt unverändert. post_id, tree_rev und die '
|
||||
. 'Pfad-ID aus lit-divi-outline; Kategorien und Schlagwörter nur vorhandene. Globale Einträge mit '
|
||||
. 'lit-library-insert (mode "global") auf Seiten einfügen.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'post_id' => [
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
'description' => 'Seite (oder Eintrag) mit dem Knoten.',
|
||||
],
|
||||
'tree_rev' => ['type' => 'string', 'description' => 'tree_rev aus lit-divi-outline dieser Seite.'],
|
||||
'id' => ['type' => 'string', 'description' => 'Pfad-ID des Knotens, z. B. "s1".'],
|
||||
'title' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 200],
|
||||
'scope' => ['type' => 'string', 'enum' => self::SCOPES, 'description' => 'Standard: normal.'],
|
||||
'categories' => ['type' => 'array', 'maxItems' => 10, 'items' => ['type' => 'string']],
|
||||
'tags' => ['type' => 'array', 'maxItems' => 10, 'items' => ['type' => 'string']],
|
||||
],
|
||||
'required' => ['post_id', 'tree_rev', 'id', 'title'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return ToolResult::schema(
|
||||
['library_id' => ['type' => 'integer'], 'tree_rev' => ['type' => 'string']],
|
||||
['library_id', 'tree_rev']
|
||||
);
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::write(false);
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$refusal = $this->access->canSave(self::isGlobal($input));
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||||
|
||||
return $page instanceof WP_Error ? $page : true;
|
||||
}
|
||||
|
||||
/** @throws WriteFailed After the insert (the new entry is rolled back first, ruling P6). */
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
if (! in_array($input['scope'] ?? 'normal', self::SCOPES, true)) {
|
||||
return Errors::invalidInput('scope ist „normal“ oder „global“; nichts wurde angelegt.', [
|
||||
'scope' => 'normal oder global',
|
||||
]);
|
||||
}
|
||||
|
||||
$refusal = $this->access->canSave(self::isGlobal($input));
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||||
if ($page instanceof WP_Error) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
if (! is_string($input['tree_rev'] ?? null) || $input['tree_rev'] !== $page->treeRev()) {
|
||||
return Errors::revMismatch(
|
||||
'Die Seite wurde seit dem letzten Lesen geändert (tree_rev passt nicht); nichts wurde angelegt.',
|
||||
DiviPageLoader::OUTLINE_HINT
|
||||
);
|
||||
}
|
||||
|
||||
$id = is_string($input['id'] ?? null) ? $input['id'] : '';
|
||||
$node = $page->index->find($id);
|
||||
if ($node === null) {
|
||||
return DiviErrors::unknownNode($id, $page->post);
|
||||
}
|
||||
|
||||
$title = sanitize_text_field(is_string($input['title'] ?? null) ? $input['title'] : '');
|
||||
if ($title === '') {
|
||||
return Errors::invalidInput('Titel fehlt; nichts wurde angelegt.', ['title' => 'nicht leer']);
|
||||
}
|
||||
|
||||
$spec = LibraryItemSpec::forNode(
|
||||
$node,
|
||||
$this->modules->catalog(),
|
||||
$title,
|
||||
self::isGlobal($input),
|
||||
$this->builtFor($page)
|
||||
);
|
||||
if (is_string($spec)) {
|
||||
return Errors::invalidInput($spec . ' Nichts wurde angelegt.', ['id' => $spec]);
|
||||
}
|
||||
|
||||
$problems = $this->writer->composition($page)->storedGuard()->problems($node);
|
||||
if ($problems !== []) {
|
||||
return Errors::invalidInput(
|
||||
'Der Knoten enthält Einstellungen, die nur im Divi-Builder übertragen werden dürfen; nichts wurde '
|
||||
. 'angelegt.',
|
||||
['id' => implode('; ', $problems)]
|
||||
);
|
||||
}
|
||||
|
||||
$categories = $this->termIds('layout_category', $input['categories'] ?? []);
|
||||
$tags = $this->termIds('layout_tag', $input['tags'] ?? []);
|
||||
foreach (['categories' => $categories, 'tags' => $tags] as $field => $ids) {
|
||||
if (is_string($ids)) {
|
||||
return Errors::invalidInput(
|
||||
'Unbekannte Kategorie oder unbekanntes Schlagwort; nichts wurde angelegt.',
|
||||
[$field => $ids]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->create($page, $spec->withTerms($categories, $tags), self::content($node), $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @return array<string, mixed>
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function create(DiviPage $page, LibraryItemSpec $spec, string $content, array $input): array
|
||||
{
|
||||
$draft = new NewDraft(null, $this->log, $this->name(), DiviLibraryStore::POST_TYPE, 'Bibliothekseintrag');
|
||||
$libraryId = $this->access->store()->create($spec, $draft);
|
||||
|
||||
$warnings = [];
|
||||
try {
|
||||
$warnings = $this->saver->saveNew($libraryId, $content, SaveScope::LibraryItem);
|
||||
} catch (Throwable $e) {
|
||||
$draft->discard($libraryId, $e, self::REASON_LAYOUT);
|
||||
}
|
||||
|
||||
$followUp = (new PostSaveFollowUp($this->name(), $this->log, $libraryId, 'der Bibliothekseintrag', $warnings))
|
||||
->afterSave($input);
|
||||
$created = $followUp->reread();
|
||||
$kind = (self::TYPE_LABELS[$spec->layoutType] ?? $spec->layoutType) . ($spec->global ? ', global' : '');
|
||||
$followUp->log(
|
||||
DiviLibraryStore::POST_TYPE,
|
||||
sprintf('Bibliothek: „%s“ angelegt (%s) aus Eintrag %d', $spec->title, $kind, (int) $page->post->ID)
|
||||
);
|
||||
|
||||
$result = $followUp->result(sprintf('Bibliothekseintrag „%s“ angelegt (%s).', $spec->title, $kind), $created)
|
||||
->with('library_id', $libraryId)
|
||||
->with('tree_rev', Rev::of($created !== null ? (string) $created->post_content : $content))
|
||||
->urls(null, (string) get_edit_post_link($libraryId, 'raw'));
|
||||
if ($spec->global) {
|
||||
$result->warning(
|
||||
'Der Knoten auf der Seite bleibt eine eigenständige Kopie; verknüpfte Instanzen mit lit-library-insert '
|
||||
. '(mode "global") einfügen.'
|
||||
);
|
||||
}
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
/** The node's stored bytes framed like the builder's "add to library", line breaks unified. */
|
||||
private static function content(Block $node): string
|
||||
{
|
||||
$copy = $node->copy();
|
||||
$copy->unifyLineBreaks();
|
||||
|
||||
return LibraryContent::wrap($copy);
|
||||
}
|
||||
|
||||
/** _et_pb_built_for_post_type: the source's post type; a library entry passes its own on. */
|
||||
private function builtFor(DiviPage $page): string
|
||||
{
|
||||
if (! $page->isLibraryItem()) {
|
||||
return (string) $page->post->post_type;
|
||||
}
|
||||
|
||||
$builtFor = (string) get_post_meta((int) $page->post->ID, '_et_pb_built_for_post_type', true);
|
||||
|
||||
return $builtFor !== '' ? $builtFor : 'page';
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $input */
|
||||
private static function isGlobal(array $input): bool
|
||||
{
|
||||
return ($input['scope'] ?? 'normal') === 'global';
|
||||
}
|
||||
|
||||
/** @return list<int>|string Term IDs of existing terms by name, or the problem. */
|
||||
private function termIds(string $taxonomy, mixed $names): array|string
|
||||
{
|
||||
$ids = [];
|
||||
foreach (is_array($names) ? $names : [] as $name) {
|
||||
$term = is_string($name) ? get_term_by('name', $name, $taxonomy) : false;
|
||||
if (! $term instanceof WP_Term) {
|
||||
return sprintf(
|
||||
'„%s“ gibt es nicht; nur vorhandene Begriffe (neue in der Divi-Bibliothek anlegen).',
|
||||
is_string($name) ? $name : '?'
|
||||
);
|
||||
}
|
||||
$ids[] = (int) $term->term_id;
|
||||
}
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user