$input */ public function page(array $input): DiviPage|WP_Error { $page = $this->loader->loadWritable($input['post_id'] ?? null); if ($page instanceof WP_Error) { return $page; } 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); } /** * 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, SaveScope $scope = SaveScope::Page ): 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()) ->urls((string) get_preview_post_link($post), $page->editUrl()) ->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, $scope); } 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 PostSaveFollowUp( $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) { $tool->changed(DiviChange::idOf($entry, $index) ?? '?', $entry['type'], $entry['action'], $entry['label']); } return $tool->toArray(); } /** * 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. * * @param array $input * @return array|WP_Error * @throws WriteFailed */ public function commit(string $ability, DiviPage $page, DiviChange $change, array $input): array|WP_Error { if (! $page->isLibraryItem()) { return $this->save($ability, $page, $change, $input); } if ($this->globals === null) { return Errors::unsupported( 'Bibliothekseinträge lassen sich über diese Verbindung nicht ändern.', 'Den Eintrag im Divi-Builder bearbeiten.' ); } if (Serializer::document($page->document) === (string) $page->post->post_content) { return $this->save($ability, $page, $change, $input, SaveScope::LibraryItem); } $prepared = $this->globals->prepare($page, $input); if ($prepared instanceof WP_Error) { return $prepared; } try { $saved = $this->save($ability, $page, $change, $input, $prepared['scope']); } catch (WriteFailed $e) { throw self::keepingBackup($e, $prepared['details']['backup_id'] ?? null); } 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. */ public function notice(DiviPage $page, WP_Error $error): WP_Error { return $page->isLibraryItem() && $this->globals !== null ? $this->globals->notice($page, $error) : $error; } /** * Outline of the sections that contain changed nodes (breadth limit of ruling S18). A removed * node has no block any more, but its ID from before the removal still names the (surviving) * section – unless the removed node was that section itself: its ID now names the next section, * which did not change (review m-7). * * @return array{text: ?string, truncated: bool} */ private function outline(NodeIndex $index, DiviChange $change): array { $sections = []; foreach ($change->entries() as $entry) { $id = DiviChange::idOf($entry, $index); $wholeSection = $entry['block'] === null && $id !== null && ! str_contains($id, '.'); if ($id !== null && ! $wholeSection) { $sections[explode('.', $id)[0]] = true; } } if ($sections === []) { return ['text' => null, 'truncated' => false]; } $outline = new Outline($this->design->read()->presetNames()); return $outline->summary($index, array_map('strval', array_keys($sections))); } }