> post_id and tree_rev of every Divi write. */ public static function inputProperties(): array { return [ 'post_id' => ['type' => 'integer', 'minimum' => 1], 'tree_rev' => [ 'type' => 'string', 'description' => 'tree_rev aus lit-divi-outline bzw. der letzten Divi-Änderung (Pflicht).', ], ]; } /** @return array> Target position of insert and move. */ public static function positionProperties(): array { return [ 'parent' => [ 'type' => 'string', 'description' => 'Pfad-ID des Elternknotens (z. B. Spalte "s0.r0.c1"); ohne parent/before/after: ' . 'Ende der Seite.', ], 'before' => ['type' => 'string', 'description' => 'Pfad-ID: vor diesem Knoten einfügen.'], 'after' => ['type' => 'string', 'description' => 'Pfad-ID: nach diesem Knoten einfügen.'], 'index' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Position unter parent (0 = zuerst).'], ]; } /** * The page to change, after rights, type, layout and the tree_rev lock were checked. * * @param array $input */ public function page(array $input): DiviPage|WP_Error { $page = $this->loader->load($input['post_id'] ?? null, true); if ($page instanceof WP_Error) { return $page; } $problem = $this->loader->writable($page); if ($problem !== null) { return $problem; } if (! is_string($input['tree_rev'] ?? null) || $input['tree_rev'] !== $page->treeRev()) { return Errors::revMismatch( 'Die Divi-Seite wurde seit dem letzten Lesen geändert (tree_rev passt nicht); nichts wurde ' . 'geschrieben.', DiviPageLoader::OUTLINE_HINT ); } return $page; } /** * 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). */ public function composition(DiviPage $page): DiviComposition { return $this->composition->forPost((int) $page->post->ID); } /** Section label around $block, for automatic admin labels ("" when none). */ public static function sectionLabel(NodeIndex $index, ?Block $block): string { for ($node = $block; $node !== null; $node = self::parentBlock($index, $node)) { if ($node->name() === 'divi/section') { return NodeSummary::label($node); } } return ''; } /** * Saves the changed tree of $page and returns the ToolResult array. Call it only when the * whole input validated – every error before means nothing is written. * * @param array $input * @return array * @throws WriteFailed */ public function save(string $ability, DiviPage $page, DiviChange $change, array $input): array { $post = $page->post; $content = Serializer::document($page->document); if ($content === (string) $post->post_content) { return ToolResult::ok('Keine Änderung – die Seite ist bereits so.') ->rev(Rev::ofPost($post)) ->with('tree_rev', $page->treeRev()) ->warnings($change->warnings()) ->toArray(); } $index = NodeIndex::build($page->document); $quality = $this->composition->warnings()->collect($index, $change->touched()); $warnings = array_merge($change->warnings(), $quality); try { $saved = $this->saver->save($post, $content); } catch (WriteFailed $e) { if (! $e->rolledBack) { $this->log->recordPartial( $ability, (string) $post->post_type, (string) $post->ID, sprintf('Divi "%s": Änderung nur teilweise zurückgenommen', $post->post_title), $e->revisionId ); } throw $e; } $outline = $this->outline($index, $change); if ($outline['truncated']) { $warnings[] = self::OUTLINE_TRUNCATED; } $result = (new PostChangeResult( $ability, $this->log, (int) $post->ID, 'die Änderung', array_merge($warnings, $saved['warnings']) ))->afterSave($input); $updated = $result->reread($post) ?? $post; $summary = sprintf('Divi "%s": %s', $post->post_title, $change->summary); $result->log((string) $post->post_type, $summary, $saved['revision_id']); $status = sprintf('%s (Status unverändert: %s).', $change->summary, $updated->post_status); $tool = $result->result($status, $updated) ->revisionId($saved['revision_id']) ->outline($outline['text']) ->with('tree_rev', Rev::of((string) $updated->post_content)) ->urls((string) get_preview_post_link($updated), $page->editUrl()); foreach ($change->entries() as $entry) { $id = $entry['id'] ?? ($entry['block'] !== null ? $index->idOf($entry['block']) : null); $tool->changed($id ?? '?', $entry['type'], $entry['action'], $entry['label']); } return $tool->toArray(); } /** * Outline of the sections that contain changed nodes (breadth limit of ruling S18). * * @return array{text: ?string, truncated: bool} */ private function outline(NodeIndex $index, DiviChange $change): array { $sections = []; foreach ($change->entries() as $entry) { $id = $entry['block'] !== null ? $index->idOf($entry['block']) : null; if ($id !== null) { $sections[explode('.', $id)[0]] = true; } } if ($sections === []) { return ['text' => null, 'truncated' => false]; } $outline = new Outline($this->design->read()->presetNames()); $texts = []; $truncated = false; foreach (array_keys($sections) as $sectionId) { $texts[] = $outline->text($outline->rows($index, (string) $sectionId)); $truncated = $truncated || $outline->wasTruncated(); } return ['text' => implode("\n", $texts), 'truncated' => $truncated]; } private static function parentBlock(NodeIndex $index, Block $block): ?Block { $parent = $index->parentOf($block); return $parent instanceof Block && $parent->name() !== 'divi/placeholder' ? $parent : null; } }