PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
betterdocs / includes / Abilities / Docs / ListDocs.php

ListDocs.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.2, at includes/Abilities/Docs/ListDocs.php

257 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * List docs ability.
4 *
5 * @package BetterDocs
6 * @since 4.9.0
7 */
8
9 namespace WPDeveloper\BetterDocs\Abilities\Docs;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 use WPDeveloper\BetterDocs\Abilities\AbilityBase;
16 use WPDeveloper\BetterDocs\Abilities\Traits\ResolvesTerms;
17 use WPDeveloper\BetterDocs\Abilities\Traits\ShapesDocs;
18
19 /**
20 * Find docs, by search text, term, status or author.
21 *
22 * Filters **never create the term they filter on**. `{tag: "typo"}` answers
23 * `not_found` rather than quietly creating `typo`, returning nothing, and
24 * leaving a junk term behind — the failure mode that makes a read tool
25 * dangerous.
26 *
27 * The page counts come from `X-WP-Total` / `X-WP-TotalPages` on the internal
28 * response, so `total` is the real number of matches and not the size of the
29 * page that was returned. Measured on WordPress 7.1: those headers count every
30 * match while `WP_REST_Posts_Controller` drops the items the caller may not
31 * read, so a page legitimately comes back shorter than `total` suggests. The
32 * headers are still the right source — `total_pages` is what a client loops
33 * over — and the tool description says the two can differ.
34 *
35 * The listing runs in **view** context, not edit: in edit context the same
36 * controller returns only the docs the caller may *edit*, so an author asking
37 * "what documentation exists?" would be shown their own three drafts and
38 * nothing else (measured: 1 item of 11 for `mcp-author`, against 10 in view
39 * context). A summary needs no raw fields, so view is both correct and more
40 * useful.
41 *
42 * @since 4.9.0
43 */
44 class ListDocs extends AbilityBase {
45
46 use ResolvesTerms;
47 use ShapesDocs;
48
49 /**
50 * Statuses that may be asked for.
51 *
52 * @since 4.9.0
53 */
54 const STATUSES = [ 'any', 'publish', 'draft', 'private', 'pending', 'future', 'trash' ];
55
56 /**
57 * @since 4.9.0
58 */
59 public function __construct() {
60 $this->id = 'betterdocs/list-docs';
61 $this->label = __( 'List docs', 'betterdocs' );
62 $this->description = __( 'List and filter BetterDocs docs by search text, category, tag, knowledge base, glossary term, status, author or slug. Terms may be given by id, slug or name; a filter never creates a term. Returns a page of doc summaries. total counts every match, while items omits any doc this user may not read, so a page can be shorter than total suggests.', 'betterdocs' );
63 $this->capability = 'edit_docs';
64 }
65
66 /**
67 * @since 4.9.0
68 *
69 * @return array
70 */
71 public function get_annotations() {
72 return [
73 'readonly' => true,
74 'destructive' => false,
75 'idempotent' => true,
76 'priority' => 1.5,
77 'openWorldHint' => false
78 ];
79 }
80
81 /**
82 * @since 4.9.0
83 *
84 * @return array
85 */
86 public function get_input_schema() {
87 return [
88 'type' => 'object',
89 'additionalProperties' => false,
90 'properties' => [
91 'search' => [
92 'type' => 'string',
93 'description' => __( 'Free-text search over title and content.', 'betterdocs' )
94 ],
95 'category' => [
96 'type' => [ 'integer', 'string' ],
97 'description' => __( 'A doc category, by id, slug or name. Never created.', 'betterdocs' )
98 ],
99 'tag' => [
100 'type' => [ 'integer', 'string' ],
101 'description' => __( 'A doc tag, by id, slug or name. Never created.', 'betterdocs' )
102 ],
103 'knowledge_base' => [
104 'type' => [ 'integer', 'string' ],
105 'description' => __( 'A knowledge base, by id, slug or name. Never created.', 'betterdocs' )
106 ],
107 'glossary' => [
108 'type' => [ 'integer', 'string' ],
109 'description' => __( 'A glossary term, by id, slug or name. Never created. Needs the enable_glossaries setting on.', 'betterdocs' )
110 ],
111 'status' => [
112 'type' => 'string',
113 'enum' => self::STATUSES,
114 'default' => 'any',
115 'description' => __( 'Publication status to filter on. Defaults to any.', 'betterdocs' )
116 ],
117 'author' => [
118 'type' => 'integer',
119 'description' => __( 'Only docs by this user id.', 'betterdocs' )
120 ],
121 'slug' => [
122 'type' => 'string',
123 'description' => __( 'Only the doc with this exact slug.', 'betterdocs' )
124 ],
125 'orderby' => [
126 'type' => 'string',
127 'enum' => [ 'date', 'modified', 'title', 'menu_order', 'relevance', 'id' ],
128 'default' => 'date',
129 'description' => __( 'Sort field. relevance only means anything alongside search.', 'betterdocs' )
130 ],
131 'order' => [
132 'type' => 'string',
133 'enum' => [ 'asc', 'desc' ],
134 'default' => 'desc'
135 ],
136 'page' => [
137 'type' => 'integer',
138 'minimum' => 1,
139 'default' => 1
140 ],
141 'per_page' => [
142 'type' => 'integer',
143 'minimum' => 1,
144 'maximum' => 100,
145 'default' => 20,
146 'description' => __( 'Docs per page, 1–100.', 'betterdocs' )
147 ]
148 ],
149 'default' => []
150 ];
151 }
152
153 /**
154 * @since 4.9.0
155 *
156 * @return array
157 */
158 public function get_output_schema() {
159 return [
160 'type' => 'object',
161 'properties' => [
162 'items' => [
163 'type' => 'array',
164 'items' => [
165 'type' => 'object',
166 'properties' => self::doc_summary_schema()
167 ]
168 ],
169 'total' => [ 'type' => 'integer' ],
170 'total_pages' => [ 'type' => 'integer' ],
171 'page' => [ 'type' => 'integer' ],
172 'per_page' => [ 'type' => 'integer' ]
173 ]
174 ];
175 }
176
177 /**
178 * @since 4.9.0
179 *
180 * @param array $input Validated input.
181 * @return array|\WP_Error
182 */
183 public function execute( $input ) {
184 $page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
185 $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 20;
186
187 $params = [
188 'context' => 'view',
189 'status' => isset( $input['status'] ) ? (string) $input['status'] : 'any',
190 'orderby' => isset( $input['orderby'] ) ? (string) $input['orderby'] : 'date',
191 'order' => isset( $input['order'] ) ? (string) $input['order'] : 'desc',
192 'page' => $page,
193 'per_page' => $per_page
194 ];
195
196 foreach ( [ 'search', 'slug' ] as $field ) {
197 if ( isset( $input[ $field ] ) && '' !== (string) $input[ $field ] ) {
198 $params[ $field ] = (string) $input[ $field ];
199 }
200 }
201
202 if ( isset( $input['author'] ) ) {
203 $params['author'] = (int) $input['author'];
204 }
205
206 $filters = [
207 'category' => 'doc_category',
208 'tag' => 'doc_tag',
209 'knowledge_base' => 'knowledge_base',
210 'glossary' => 'glossaries'
211 ];
212
213 foreach ( $filters as $field => $taxonomy ) {
214 if ( ! isset( $input[ $field ] ) || '' === $input[ $field ] ) {
215 continue;
216 }
217
218 // `false`: a filter never creates the term it is filtering on.
219 $ids = $this->resolve_terms( [ $input[ $field ] ], $taxonomy, false );
220
221 if ( is_wp_error( $ids ) ) {
222 return $ids;
223 }
224
225 $params[ $taxonomy ] = $ids;
226 }
227
228 $response = $this->dispatch_response( 'GET', '/docs', $params, 'wp/v2' );
229
230 if ( $response->is_error() ) {
231 $error = $response->as_error();
232
233 // A page past the last one is an empty page, not a missing doc.
234 if ( $this->is_page_out_of_range( $error ) ) {
235 return $this->empty_page( '/docs', $params, $page, $per_page, 'wp/v2' );
236 }
237
238 return $this->map_rest_error( $error, __( 'list docs', 'betterdocs' ) );
239 }
240
241 $headers = $response->get_headers();
242 $items = [];
243
244 foreach ( (array) $response->get_data() as $doc ) {
245 $items[] = $this->doc_summary( (array) $doc );
246 }
247
248 return [
249 'items' => $items,
250 'total' => isset( $headers['X-WP-Total'] ) ? (int) $headers['X-WP-Total'] : count( $items ),
251 'total_pages' => isset( $headers['X-WP-TotalPages'] ) ? (int) $headers['X-WP-TotalPages'] : 1,
252 'page' => $page,
253 'per_page' => $per_page
254 ];
255 }
256 }
257