feat: explicit publish, unpublish, schedule and trash tools
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress\Status;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\FollowUp;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
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. 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 ['readonly' => false, 'destructive' => false, 'idempotent' => 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']);
|
||||
}
|
||||
|
||||
if ($date->getTimestamp() <= time()) {
|
||||
return Errors::invalidInput(
|
||||
'Der Zeitpunkt muss in der Zukunft liegen.',
|
||||
['date' => 'liegt nicht in der Zukunft'],
|
||||
'Zum sofortigen Veröffentlichen lit-post-publish verwenden.'
|
||||
);
|
||||
}
|
||||
|
||||
$postId = (int) $post->ID;
|
||||
$result = wp_update_post([
|
||||
'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,
|
||||
], true);
|
||||
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 = [];
|
||||
FollowUp::run(
|
||||
fn () => do_action('lit_mcp_after_save', $postId, $this->name(), $input),
|
||||
$this->name() . ' lit_mcp_after_save',
|
||||
'Eine Erweiterung ist nach dem Planen auf einen Fehler gestoßen; die Änderung ist gespeichert.',
|
||||
$warnings
|
||||
);
|
||||
|
||||
$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)' : '');
|
||||
$logId = FollowUp::run(
|
||||
fn () => $this->log->record($this->name(), (string) $post->post_type, (string) $postId, $summary),
|
||||
$this->name() . ' change log',
|
||||
'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.',
|
||||
$warnings
|
||||
);
|
||||
|
||||
$updated = get_post($postId);
|
||||
$updated = $updated instanceof WP_Post ? $updated : $post;
|
||||
|
||||
return ToolResult::ok($summary . '.')
|
||||
->changed((string) $postId, (string) $post->post_type, 'scheduled', $title)
|
||||
->rev(Rev::ofPost($updated))
|
||||
->logId(is_int($logId) ? $logId : null)
|
||||
->urls((string) get_preview_post_link($updated), (string) get_edit_post_link($postId, 'raw'))
|
||||
->warnings($warnings)
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user