The 41 abilities return Annotations::read(), ::write(idempotent) or ::destructive(idempotent) instead of hand-written arrays (same arrays, same key order). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
173 lines
6.0 KiB
PHP
173 lines
6.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\WordPress\Status;
|
||
|
||
use DateTimeImmutable;
|
||
use DateTimeZone;
|
||
use LIT\MCP\Abilities\Ability;
|
||
use LIT\MCP\Abilities\Annotations;
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Abilities\ToolResult;
|
||
use LIT\MCP\ChangeLog\ChangeLog;
|
||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||
use WP_Error;
|
||
use WP_Post;
|
||
|
||
/**
|
||
* lit/post-schedule: schedules an entry for publication at a future point in time
|
||
* (site local time). An entry that is currently published stays live until then only
|
||
* if wp_update_post() keeps it that way; WordPress itself takes it offline once the
|
||
* status becomes "future".
|
||
*/
|
||
final class PostScheduleAbility implements Ability
|
||
{
|
||
private const FORMAT = 'Y-m-d H:i';
|
||
|
||
public function __construct(private ContentSchema $schema, private ChangeLog $log)
|
||
{
|
||
}
|
||
|
||
public function name(): string
|
||
{
|
||
return 'lit/post-schedule';
|
||
}
|
||
|
||
public function label(): string
|
||
{
|
||
return 'Veröffentlichung planen';
|
||
}
|
||
|
||
public function description(): string
|
||
{
|
||
return 'Plant die Veröffentlichung eines Eintrags zu einem Zeitpunkt in der Zukunft (Ortszeit der '
|
||
. 'Website). Ein bereits veröffentlichter Eintrag ist bis dahin offline; ein privater Eintrag wird '
|
||
. 'zum Zeitpunkt öffentlich. Nur auf ausdrücklichen Wunsch des Nutzers verwenden.';
|
||
}
|
||
|
||
public function inputSchema(): array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'post_id' => ['type' => 'integer', 'minimum' => 1],
|
||
'date' => [
|
||
'type' => 'string',
|
||
'pattern' => '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||
'description' => 'JJJJ-MM-TT HH:MM in der Zeitzone der Website.',
|
||
],
|
||
],
|
||
'required' => ['post_id', 'date'],
|
||
'additionalProperties' => false,
|
||
];
|
||
}
|
||
|
||
public function outputSchema(): ?array
|
||
{
|
||
return ToolResult::schema();
|
||
}
|
||
|
||
public function annotations(): array
|
||
{
|
||
return Annotations::write(true);
|
||
}
|
||
|
||
public function isDirect(): bool
|
||
{
|
||
return false;
|
||
}
|
||
|
||
public function permission(array $input): bool|WP_Error
|
||
{
|
||
$post = $this->load($input);
|
||
|
||
return $post instanceof WP_Error ? $post : true;
|
||
}
|
||
|
||
public function execute(array $input): mixed
|
||
{
|
||
$post = $this->load($input);
|
||
if ($post instanceof WP_Error) {
|
||
return $post;
|
||
}
|
||
|
||
$raw = (string) ($input['date'] ?? '');
|
||
$date = DateTimeImmutable::createFromFormat('!' . self::FORMAT, $raw, wp_timezone());
|
||
if ($date === false || $date->format(self::FORMAT) !== $raw) {
|
||
return Errors::invalidInput('Ungültiges Datum.', ['date' => 'Format JJJJ-MM-TT HH:MM erwartet']);
|
||
}
|
||
|
||
// Within PUBLISH_WINDOW WordPress would publish at once instead of scheduling.
|
||
if ($date->getTimestamp() <= time() + PostGuard::PUBLISH_WINDOW) {
|
||
return Errors::invalidInput(
|
||
'Der Zeitpunkt muss mindestens eine Minute in der Zukunft liegen.',
|
||
['date' => 'liegt nicht (mindestens eine Minute) in der Zukunft'],
|
||
'Zum sofortigen Veröffentlichen lit/post-publish (direkt: lit-post-publish) verwenden.'
|
||
);
|
||
}
|
||
|
||
$postId = (int) $post->ID;
|
||
$data = [
|
||
'ID' => $postId,
|
||
'post_status' => 'future',
|
||
'post_date' => $date->format('Y-m-d H:i:s'),
|
||
'post_date_gmt' => $date->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'),
|
||
'edit_date' => true,
|
||
];
|
||
$result = StatusWrite::run(
|
||
static fn () => wp_update_post($data, true),
|
||
$this->name(),
|
||
$this->log,
|
||
$post,
|
||
'WordPress hat beim Planen einen Fehler gemeldet.'
|
||
);
|
||
if ($result instanceof WP_Error) {
|
||
return Errors::invalidInput('WordPress hat das Planen abgelehnt: ' . $result->get_error_message());
|
||
}
|
||
|
||
if (get_post_status($postId) !== 'future') {
|
||
return Errors::unsupported(
|
||
'WordPress hat den Eintrag nicht als geplant gespeichert.',
|
||
'Eintrag in WordPress prüfen (z. B. Freigabe-Workflow eines anderen Plugins).'
|
||
);
|
||
}
|
||
|
||
// The status change is saved – failures from here on are logged and reported as warnings only.
|
||
$warnings = $post->post_status === 'private'
|
||
? [sprintf(
|
||
'Der Eintrag war privat und wird am %s ÖFFENTLICH veröffentlicht; soll er privat bleiben, die Planung '
|
||
. 'in WordPress zurücknehmen.',
|
||
$raw
|
||
)]
|
||
: [];
|
||
$change = (new PostSaveFollowUp($this->name(), $this->log, $postId, 'die Änderung', $warnings))
|
||
->afterSave($input, 'Planen');
|
||
$title = (string) $post->post_title;
|
||
$summary = sprintf('"%s" geplant für %s', $title, $raw)
|
||
. ($post->post_status === 'publish' ? ' (war veröffentlicht und ist bis dahin offline)' : '');
|
||
$change->log((string) $post->post_type, $summary);
|
||
$updated = $change->reread($post) ?? $post;
|
||
|
||
return $change->result($summary . '.', $updated)
|
||
->changed((string) $postId, (string) $post->post_type, 'scheduled', $title)
|
||
->urls((string) get_preview_post_link($updated), (string) get_edit_post_link($postId, 'raw'))
|
||
->toArray();
|
||
}
|
||
|
||
/** @param array<string, mixed> $input */
|
||
private function load(array $input): WP_Post|WP_Error
|
||
{
|
||
$post = PostGuard::editable($input['post_id'] ?? null, $this->schema);
|
||
if ($post instanceof WP_Error) {
|
||
return $post;
|
||
}
|
||
|
||
return current_user_can('publish_post', (int) $post->ID)
|
||
? $post
|
||
: Errors::forbidden('Keine Berechtigung, diesen Eintrag zu veröffentlichen oder zu planen.');
|
||
}
|
||
}
|