'object', 'properties' => array_merge([ 'post_type' => ['type' => 'string'], 'title' => ['type' => 'string', 'minLength' => 1], 'divi' => [ 'type' => 'array', 'minItems' => 1, 'maxItems' => 30, 'items' => ['type' => 'object'], 'description' => 'Divi-Layout in Kurzform: Abschnitte wie bei lit-divi-insert (nicht zusammen mit ' . 'content). Nur wenn Divi 5 aktiv und der Divi-Builder für den Inhaltstyp eingeschaltet ist.', ], ], PostWriter::inputProperties()), 'required' => ['post_type', 'title'], 'additionalProperties' => false, ]; } public function outputSchema(): ?array { return ToolResult::schema(['tree_rev' => ['type' => 'string']]); } public function annotations(): array { return ['readonly' => false, 'destructive' => false, 'idempotent' => false]; } public function isDirect(): bool { return true; } public function permission(array $input): bool|WP_Error { $type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'create_posts'); return $type instanceof WP_Error ? $type : true; } public function execute(array $input): mixed { $type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'create_posts'); if ($type instanceof WP_Error) { return $type; } $postType = (string) $input['post_type']; $withDivi = array_key_exists('divi', $input); if ($withDivi && ($this->divi === null || ! $this->divi->available())) { return Errors::unsupported( 'Divi-Inhalt ist auf dieser Website nicht möglich (Divi 5 ist nicht aktiv); nichts wurde angelegt.', 'Ohne divi anlegen und den Inhalt über content oder im Editor setzen.' ); } $prepared = $this->writer->prepare($postType, $input, true); $problem = self::combined($prepared, $withDivi ? $this->checkDivi($postType, $input) : null); if ($problem !== null) { return $problem; } $data = array_merge($this->writer->postData($input), [ 'post_type' => $postType, 'post_status' => 'draft', 'post_author' => get_current_user_id(), ]); try { $postId = wp_insert_post(wp_slash($data), true); } catch (Throwable $e) { // A hook threw – the row may or may not exist and its ID is unknown; nothing is // searched or deleted, the client is told to check (lit_partial_write without id). $this->logPartial($postType, '', 'Anlegen abgebrochen; Entwurf möglicherweise vorhanden'); throw new WriteFailed(self::REASON_INSERT, false, null, null, $e); } if ($postId instanceof WP_Error) { return Errors::invalidInput('WordPress hat den Eintrag abgelehnt: ' . $postId->get_error_message()); } $postId = (int) $postId; $denied = $this->writer->deniedMeta($postId, $postType, $prepared['fields'], 'add_post_meta'); if ($denied !== []) { return $this->refuse($postId, $postType, $denied); } $layout = null; try { $layout = $withDivi ? $this->divi?->write($postId, $postType, self::sections($input)) : null; } catch (Throwable $e) { $this->discard($postId, $postType, $e); } if ($layout instanceof WP_Error) { return $this->refuse($postId, $postType, $layout); } try { $this->writer->writeExtras($postId, $postType, $prepared, $input); } catch (Throwable $e) { $this->discard($postId, $postType, $e); } $warnings = array_merge($prepared['warnings'], $layout['warnings'] ?? []); return $this->result($postId, $postType, $input, $warnings, $layout['outline'] ?? null); } /** * The Divi description, checked completely before anything is created (spec §6.2). * * @param array $input */ private function checkDivi(string $postType, array $input): ?WP_Error { if (array_key_exists('content', $input)) { return Errors::invalidInput('content und divi schließen sich aus; nichts wurde angelegt.', [ 'content' => 'bei Divi-Inhalt weglassen', ]); } return $this->divi?->check($postType, self::sections($input)); } /** * @param array $input * @return list */ private static function sections(array $input): array { return is_array($input['divi'] ?? null) ? array_values($input['divi']) : []; } /** * One error with the fields of both checks (all or nothing: every problem comes back at once); * invalid_attr_path wins, so its suggestions stay visible. Null when both passed. * * @param array|WP_Error $prepared */ private static function combined(array|WP_Error $prepared, ?WP_Error $divi): ?WP_Error { if (! $prepared instanceof WP_Error || $divi === null) { return $prepared instanceof WP_Error ? $prepared : $divi; } $fields = array_merge(self::fields($prepared), self::fields($divi)); if ($fields === []) { return $prepared; } return $divi->get_error_code() === 'lit_invalid_attr_path' ? Errors::invalidAttrPath($fields) : Errors::invalidInput('Eingaben sind ungültig; nichts wurde angelegt.', $fields); } /** @return array */ private static function fields(WP_Error $error): array { $data = $error->get_error_data(); return is_array($data) && is_array($data['fields'] ?? null) ? $data['fields'] : []; } /** * @param array $input * @param list $warnings * @param string|null $outline Divi outline of the new layout (with divi only). * @return array */ private function result(int $postId, string $postType, array $input, array $warnings, ?string $outline): array { // The draft is saved – failures from here on are logged and reported as warnings only. $change = (new PostChangeResult($this->name(), $this->log, $postId, 'der Entwurf', $warnings)) ->afterSave($input); $post = $change->reread(); $title = $post instanceof WP_Post ? (string) $post->post_title : (string) $input['title']; $change->log($postType, sprintf('Entwurf "%s" angelegt', $title)); $result = $change->result(sprintf('"%s" als Entwurf angelegt (ID %d).', $title, $postId), $post) ->changed((string) $postId, $postType, 'created', $title) ->urls((string) get_preview_post_link($postId), (string) get_edit_post_link($postId, 'raw')); if (array_key_exists('divi', $input)) { // The Divi tools lock on tree_rev (ruling P16): the next lit-divi-* call can use it right away. $result->outline($outline) ->with('tree_rev', Rev::of($post instanceof WP_Post ? (string) $post->post_content : '')); } return $result->toArray(); } /** * Rolls the brand-new draft back: terms it created, then the draft itself (force – * it cannot have been used anywhere yet). * * @throws WriteFailed Always. */ private function discard(int $postId, string $postType, Throwable $cause): never { $problems = []; try { $this->writer->deleteCreatedTerms(); } catch (Throwable $e) { $problems[] = $e->getMessage(); } try { if (! wp_delete_post($postId, true)) { $problems[] = sprintf('draft %d could not be deleted', $postId); } } catch (Throwable $e) { $problems[] = sprintf('draft %d could not be deleted: %s', $postId, $e->getMessage()); } $detail = sprintf('Extras of new post %d could not be written: %s', $postId, $cause->getMessage()); if ($problems === []) { throw new WriteFailed(self::REASON_EXTRAS, true, null, null, $cause, $detail); } $summary = sprintf('Entwurf %d: Anlegen nur teilweise zurückgenommen', $postId); $this->logPartial($postType, (string) $postId, $summary); throw new WriteFailed( self::REASON_EXTRAS, false, $postId, null, $cause, $detail . '; rollback: ' . implode('; ', $problems) ); } /** * Some checks need the draft: meta rights (add_post_meta needs its ID) and the Divi layout * against the new entry's own dynamic content options. A refusal removes the brand-new draft * again and is reported like any invalid input. * * @param array|WP_Error $denied fields. => error, or the error itself. * @throws WriteFailed When the draft cannot be deleted. */ private function refuse(int $postId, string $postType, array|WP_Error $denied): WP_Error { try { $deleted = (bool) wp_delete_post($postId, true); } catch (Throwable $e) { $deleted = false; } if (! $deleted) { $summary = sprintf('Entwurf %d: Anlegen nicht zurückgenommen', $postId); $this->logPartial($postType, (string) $postId, $summary); $fields = $denied instanceof WP_Error ? self::fields($denied) : $denied; throw new WriteFailed( ($denied instanceof WP_Error ? 'Ungültiger Divi-Inhalt: ' : 'Keine Berechtigung für ') . implode(', ', array_keys($fields)) . '.', false, $postId, null, null, sprintf('New post %d refused after the insert; draft could not be deleted', $postId) ); } return $denied instanceof WP_Error ? $denied : Errors::invalidInput('Eingaben sind ungültig; nichts wurde gespeichert.', $denied); } private function logPartial(string $postType, string $postId, string $summary): void { $this->log->recordPartial($this->name(), $postType, $postId, $summary); } }