Files
WP-LIT-MCP/src/Integrations/WordPress/Content/ContentQueryAbility.php
T

208 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Content;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
final class ContentQueryAbility implements Ability
{
private const STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
private const DATE_PATTERN = '^\d{4}-\d{2}-\d{2}$';
public function __construct(private ContentSchema $schema, private PostPresenter $presenter)
{
}
public function name(): string
{
return 'lit/content-query';
}
public function label(): string
{
return 'Inhalte suchen';
}
public function description(): string
{
return 'Sucht Einträge eines Inhaltstyps. Filter: Status, Volltext, Taxonomie-Begriffe (IDs oder Slugs), '
. 'Feldwert, Datum, übergeordneter Eintrag; sortierbar; Cursor-Paginierung. Liefert Kurzdaten '
. '(ID, Titel, Status, URLs, Divi ja/nein) – Details über lit-content-get.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge([
'post_type' => ['type' => 'string', 'description' => 'Inhaltstyp, z. B. page oder post.'],
'status' => [
'type' => 'array',
'items' => ['type' => 'string', 'enum' => self::STATUSES],
'description' => 'Standard: alle außer Papierkorb.',
],
'search' => ['type' => 'string'],
'taxonomy' => [
'type' => 'object',
'properties' => [
'taxonomy' => ['type' => 'string'],
'terms' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => ['integer', 'string']]],
],
'required' => ['taxonomy', 'terms'],
'additionalProperties' => false,
],
'field' => [
'type' => 'object',
'properties' => [
'key' => ['type' => 'string'],
'value' => ['type' => ['string', 'number', 'boolean']],
'compare' => ['type' => 'string', 'enum' => ['=', '!=', 'LIKE', 'EXISTS', 'NOT EXISTS']],
],
'required' => ['key'],
'additionalProperties' => false,
],
'date_after' => ['type' => 'string', 'pattern' => self::DATE_PATTERN],
'date_before' => ['type' => 'string', 'pattern' => self::DATE_PATTERN],
'parent' => ['type' => 'integer', 'minimum' => 0],
'orderby' => ['type' => 'string', 'enum' => ['date', 'modified', 'title', 'menu_order']],
'order' => ['type' => 'string', 'enum' => ['asc', 'desc']],
], Cursor::inputProperties()),
'required' => ['post_type'],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return [
'type' => 'object',
'properties' => [
'items' => ['type' => 'array', 'items' => PostPresenter::summarySchema()],
'next_cursor' => ['type' => ['string', 'null']],
],
'required' => ['items', 'next_cursor'],
];
}
public function annotations(): array
{
return ['readonly' => true, 'destructive' => false, 'idempotent' => true];
}
public function isDirect(): bool
{
return true;
}
public function permission(array $input): bool|WP_Error
{
$type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'edit_posts');
return $type instanceof WP_Error ? $type : true;
}
public function execute(array $input): mixed
{
$offset = Cursor::decode(isset($input['cursor']) ? (string) $input['cursor'] : null);
if ($offset === null) {
return Cursor::invalid();
}
$limit = Cursor::limit($input['limit'] ?? null);
$posts = get_posts(array_merge($this->queryArgs($input), [
'posts_per_page' => $limit + 1,
'offset' => $offset,
]));
$page = Cursor::page(array_values($posts), $limit, $offset);
$items = [];
foreach ($page['items'] as $post) {
if ($post instanceof WP_Post && current_user_can('edit_post', (int) $post->ID)) {
$items[] = $this->presenter->summary($post);
}
}
return ['items' => $items, 'next_cursor' => $page['next_cursor']];
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
private function queryArgs(array $input): array
{
$args = [
'post_type' => (string) $input['post_type'],
'post_status' => $input['status'] ?? self::STATUSES,
'orderby' => (string) ($input['orderby'] ?? 'date'),
'order' => strtoupper((string) ($input['order'] ?? 'desc')),
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'suppress_filters' => false,
];
if (isset($input['search']) && $input['search'] !== '') {
$args['s'] = (string) $input['search'];
}
if (isset($input['parent'])) {
$args['post_parent'] = (int) $input['parent'];
}
if (isset($input['taxonomy']) && is_array($input['taxonomy'])) {
$args['tax_query'] = $this->taxQuery($input['taxonomy']);
}
if (isset($input['field']) && is_array($input['field'])) {
$compare = (string) ($input['field']['compare'] ?? '=');
$clause = ['key' => (string) $input['field']['key'], 'compare' => $compare];
if (! in_array($compare, ['EXISTS', 'NOT EXISTS'], true)) {
$clause['value'] = $input['field']['value'] ?? '';
}
$args['meta_query'] = [$clause];
}
$date = [];
if (isset($input['date_after'])) {
$date['after'] = (string) $input['date_after'];
}
if (isset($input['date_before'])) {
$date['before'] = (string) $input['date_before'];
}
if ($date !== []) {
$args['date_query'] = [array_merge($date, ['inclusive' => true])];
}
return $args;
}
/**
* @param array<string, mixed> $filter
* @return array<int|string, mixed>
*/
private function taxQuery(array $filter): array
{
$taxonomy = (string) $filter['taxonomy'];
$terms = (array) $filter['terms'];
$ids = array_values(array_filter($terms, 'is_int'));
$slugs = array_values(array_filter($terms, 'is_string'));
$clauses = [];
if ($ids !== []) {
$clauses[] = ['taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $ids];
}
if ($slugs !== []) {
$clauses[] = ['taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => $slugs];
}
return count($clauses) > 1 ? array_merge(['relation' => 'OR'], $clauses) : $clauses;
}
}