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 / Faq / ListFAQGroups.php

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

251 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * List FAQ groups ability.
4 *
5 * @package BetterDocs
6 * @since 4.9.0
7 */
8
9 namespace WPDeveloper\BetterDocs\Abilities\Faq;
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\ShapesFAQs;
17
18 /**
19 * What FAQ groups exist, how many questions are in each, and which of them the
20 * front end is actually showing.
21 *
22 * The tool to call before creating a group or attaching one to a doc, because
23 * it is what turns "the billing FAQ" into an id instead of a second group with
24 * a near-identical name.
25 *
26 * **Order is the site's own.** `FAQBuilder::faq_category_orderby_meta()` hooks
27 * `rest_betterdocs_faq_category_query` and sets `orderby`/`order` from the FAQ
28 * Builder's header preference (the `betterdocs_faq_order` option) on every REST
29 * request for this taxonomy — measured, not assumed. So this tool takes no
30 * `orderby`: the list comes back in the order the FAQ Builder and the front end
31 * show it, which is the order an agent should reason about anyway.
32 *
33 * @since 4.9.0
34 */
35 class ListFAQGroups extends AbilityBase {
36
37 use ShapesFAQs;
38
39 /**
40 * @since 4.9.0
41 */
42 public function __construct() {
43 $this->id = 'betterdocs/list-faq-groups';
44 $this->label = __( 'List FAQ groups', 'betterdocs' );
45 $this->description = __( 'List BetterDocs FAQ groups with their question counts and whether each is published or draft, in the order the site shows them. Call this before creating a group so you reuse what exists.', 'betterdocs' );
46 $this->capability = 'edit_others_docs';
47 }
48
49 /**
50 * @since 4.9.0
51 *
52 * @return array
53 */
54 public function get_annotations() {
55 return [
56 'readonly' => true,
57 'destructive' => false,
58 'idempotent' => true,
59 'priority' => 1.5,
60 'openWorldHint' => false
61 ];
62 }
63
64 /**
65 * @since 4.9.0
66 *
67 * @return array
68 */
69 public function get_input_schema() {
70 return [
71 'type' => 'object',
72 'additionalProperties' => false,
73 'properties' => [
74 'search' => [
75 'type' => 'string',
76 'description' => __( 'Free-text search over group names and slugs.', 'betterdocs' )
77 ],
78 'status' => [
79 'type' => 'string',
80 'enum' => [ 'any', 'publish', 'draft' ],
81 'default' => 'any',
82 'description' => __( 'publish lists only the groups the front end shows, draft only the hidden ones. Defaults to any.', 'betterdocs' )
83 ],
84 'page' => [
85 'type' => 'integer',
86 'minimum' => 1,
87 'default' => 1
88 ],
89 'per_page' => [
90 'type' => 'integer',
91 'minimum' => 1,
92 'maximum' => 100,
93 'default' => 50,
94 'description' => __( 'Groups per page, 1–100.', 'betterdocs' )
95 ]
96 ],
97 'default' => []
98 ];
99 }
100
101 /**
102 * @since 4.9.0
103 *
104 * @return array
105 */
106 public function get_output_schema() {
107 return [
108 'type' => 'object',
109 'properties' => [
110 'items' => [
111 'type' => 'array',
112 'items' => [
113 'type' => 'object',
114 'properties' => self::group_shape_schema()
115 ]
116 ],
117 'total' => [ 'type' => 'integer' ],
118 'total_pages' => [ 'type' => 'integer' ],
119 'page' => [ 'type' => 'integer' ],
120 'per_page' => [ 'type' => 'integer' ]
121 ]
122 ];
123 }
124
125 /**
126 * @since 4.9.0
127 *
128 * @param array $input Validated input.
129 * @return array|\WP_Error
130 */
131 public function execute( $input ) {
132 $page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
133 $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 50;
134 $status = isset( $input['status'] ) ? (string) $input['status'] : 'any';
135
136 $params = [
137 // View, not edit: `WP_REST_Terms_Controller` refuses `context=edit`
138 // without the taxonomy's `edit_terms` capability, and this
139 // taxonomy's is `edit_doc_terms` — which the FAQ tools deliberately
140 // do not require (ADR-005). Term meta, the count and the draft count
141 // are all present in view context anyway.
142 'context' => 'view',
143 'hide_empty' => false,
144 'page' => $page,
145 'per_page' => $per_page
146 ];
147
148 if ( isset( $input['search'] ) && '' !== (string) $input['search'] ) {
149 $params['search'] = (string) $input['search'];
150 }
151
152 $response = $this->dispatch_groups( $params, $status );
153
154 if ( $response->is_error() ) {
155 return $this->map_faq_error( $response->as_error(), __( 'list FAQ groups', 'betterdocs' ) );
156 }
157
158 $headers = $response->get_headers();
159 $items = [];
160
161 foreach ( (array) $response->get_data() as $term ) {
162 $items[] = $this->group_shape( (array) $term );
163 }
164
165 return [
166 'items' => $items,
167 'total' => isset( $headers['X-WP-Total'] ) ? (int) $headers['X-WP-Total'] : count( $items ),
168 'total_pages' => isset( $headers['X-WP-TotalPages'] ) ? (int) $headers['X-WP-TotalPages'] : 1,
169 'page' => $page,
170 'per_page' => $per_page
171 ];
172 }
173
174 /**
175 * Run the collection request, narrowing it to one `status` when asked.
176 *
177 * The group's published/hidden state is a term meta the REST controller
178 * knows nothing about, so the filter goes in as a `meta_query` on this one
179 * request — identity-compared, so nothing else in the page is affected — and
180 * `X-WP-Total` then counts the same set the items came from. Filtering the
181 * page after the fact would have made `total` a lie.
182 *
183 * `draft` deliberately also matches a group with **no** `status` meta at
184 * all: the front end (`Query::faq_terms_query_args()`) shows only `status =
185 * 1`, so a group that predates the meta is hidden, and reporting it as
186 * published would be wrong.
187 *
188 * @since 4.9.0
189 *
190 * @param array $params Query parameters.
191 * @param string $status `any`, `publish` or `draft`.
192 * @return \WP_REST_Response
193 */
194 protected function dispatch_groups( array $params, $status ) {
195 $request = new \WP_REST_Request( 'GET', '/wp/v2/' . self::GROUP_TAXONOMY );
196
197 $request->set_header( 'Content-Type', 'application/json' );
198 $request->set_query_params( $params );
199
200 if ( 'publish' !== $status && 'draft' !== $status ) {
201 return rest_do_request( $request );
202 }
203
204 $meta_query = 'publish' === $status
205 ? [
206 [
207 'key' => 'status',
208 'value' => '1',
209 'compare' => '='
210 ]
211 ]
212 : [
213 'relation' => 'OR',
214 [
215 'key' => 'status',
216 'value' => '1',
217 'compare' => '!='
218 ],
219 [
220 'key' => 'status',
221 'compare' => 'NOT EXISTS'
222 ]
223 ];
224
225 $narrow = function ( $args, $filtered_request ) use ( $request, $meta_query ) {
226 if ( $filtered_request !== $request ) {
227 return $args;
228 }
229
230 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- the group's published state is a term meta; filtering on it is the feature.
231 $args['meta_query'] = isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) && ! empty( $args['meta_query'] )
232 ? [
233 'relation' => 'AND',
234 $args['meta_query'],
235 $meta_query
236 ]
237 : $meta_query;
238
239 return $args;
240 };
241
242 add_filter( 'rest_' . self::GROUP_TAXONOMY . '_query', $narrow, 20, 2 );
243
244 $response = rest_do_request( $request );
245
246 remove_filter( 'rest_' . self::GROUP_TAXONOMY . '_query', $narrow, 20 );
247
248 return $response;
249 }
250 }
251