PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 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 All 201 releases
betterdocs / includes / Abilities / Traits / ShapesDocs.php

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

290 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The one doc shape every Docs ability answers with, and the translation from
4 * WordPress' REST errors to BetterDocs' typed ones.
5 *
6 * @package BetterDocs
7 * @since 4.9.0
8 */
9
10 namespace WPDeveloper\BetterDocs\Abilities\Traits;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 use WPDeveloper\BetterDocs\Abilities\AbilityError;
17
18 /**
19 * Five tools, one summary object.
20 *
21 * `bd-create-doc`, `bd-update-doc`, `bd-get-doc` and every item of
22 * `bd-list-docs` return the same keys in the same order, so an agent learns the
23 * shape once. `bd-get-doc` adds the content and the analytics on top.
24 *
25 * The error half matters as much. `WP_REST_Posts_Controller` refuses with
26 * `rest_cannot_edit` whether the caller lacks `edit_others_docs`, or
27 * `edit_published_docs`, or is simply not allowed near the post — one code for
28 * three different fixes. {@see self::editing_capability()} works out which
29 * capability a specific post actually needs from that post's author and status,
30 * so the refusal names the thing an administrator would have to grant.
31 *
32 * @since 4.9.0
33 */
34 trait ShapesDocs {
35
36 /**
37 * The BetterDocs post type these tools address.
38 *
39 * @since 4.9.0
40 */
41 protected static $doc_post_type = 'docs';
42
43 /**
44 * The doc summary every Docs tool answers with.
45 *
46 * @since 4.9.0
47 *
48 * @param array $doc A `wp/v2/docs` item.
49 * @return array
50 */
51 protected function doc_summary( array $doc ) {
52 $id = isset( $doc['id'] ) ? (int) $doc['id'] : 0;
53
54 return [
55 'id' => $id,
56 'title' => $this->rendered_or_raw( isset( $doc['title'] ) ? $doc['title'] : '' ),
57 'slug' => isset( $doc['slug'] ) ? (string) $doc['slug'] : '',
58 'status' => isset( $doc['status'] ) ? (string) $doc['status'] : '',
59 'url' => isset( $doc['link'] ) ? (string) $doc['link'] : '',
60 'edit_url' => $id > 0 ? admin_url( 'post.php?post=' . $id . '&action=edit' ) : '',
61 'excerpt' => $this->rendered_or_raw( isset( $doc['excerpt'] ) ? $doc['excerpt'] : '' ),
62 'categories' => $this->term_summaries( isset( $doc['doc_category'] ) ? (array) $doc['doc_category'] : [], 'doc_category' ),
63 'tags' => $this->term_summaries( isset( $doc['doc_tag'] ) ? (array) $doc['doc_tag'] : [], 'doc_tag' ),
64 'knowledge_bases' => $this->term_summaries( isset( $doc['knowledge_base'] ) ? (array) $doc['knowledge_base'] : [], 'knowledge_base' ),
65 'glossaries' => $this->term_summaries( isset( $doc['glossaries'] ) ? (array) $doc['glossaries'] : [], 'glossaries' ),
66 'author' => $this->author_summary( isset( $doc['author'] ) ? (int) $doc['author'] : 0 ),
67 'modified' => isset( $doc['modified'] ) ? (string) $doc['modified'] : ''
68 ];
69 }
70
71 /**
72 * JSON Schema for {@see self::doc_summary()}.
73 *
74 * @since 4.9.0
75 *
76 * @return array
77 */
78 protected static function doc_summary_schema() {
79 $term_list = [
80 'type' => 'array',
81 'items' => [
82 'type' => 'object',
83 'properties' => [
84 'id' => [ 'type' => 'integer' ],
85 'name' => [ 'type' => 'string' ],
86 'slug' => [ 'type' => 'string' ]
87 ]
88 ]
89 ];
90
91 return [
92 'id' => [ 'type' => 'integer' ],
93 'title' => [ 'type' => 'string' ],
94 'slug' => [ 'type' => 'string' ],
95 'status' => [ 'type' => 'string' ],
96 'url' => [ 'type' => 'string' ],
97 'edit_url' => [ 'type' => 'string' ],
98 'excerpt' => [ 'type' => 'string' ],
99 'categories' => $term_list,
100 'tags' => $term_list,
101 'knowledge_bases' => $term_list,
102 // Empty unless the enable_glossaries setting is on.
103 'glossaries' => $term_list,
104 'author' => [
105 'type' => 'object',
106 'properties' => [
107 'id' => [ 'type' => 'integer' ],
108 'name' => [ 'type' => 'string' ]
109 ]
110 ],
111 'modified' => [ 'type' => 'string' ]
112 ];
113 }
114
115 /**
116 * WordPress returns `{raw, rendered}` in edit context and `{rendered}` in
117 * view context; either way an agent wants one string.
118 *
119 * @since 4.9.0
120 *
121 * @param mixed $field A REST title/excerpt field.
122 * @return string
123 */
124 protected function rendered_or_raw( $field ) {
125 if ( is_string( $field ) ) {
126 return $field;
127 }
128
129 if ( ! is_array( $field ) ) {
130 return '';
131 }
132
133 if ( isset( $field['raw'] ) && '' !== $field['raw'] ) {
134 return (string) $field['raw'];
135 }
136
137 return isset( $field['rendered'] ) ? (string) $field['rendered'] : '';
138 }
139
140 /**
141 * `{id, name}` for a user id.
142 *
143 * @since 4.9.0
144 *
145 * @param int $user_id User id.
146 * @return array
147 */
148 protected function author_summary( $user_id ) {
149 $user_id = (int) $user_id;
150 $user = $user_id > 0 ? get_userdata( $user_id ) : false;
151
152 return [
153 'id' => $user_id,
154 'name' => $user && isset( $user->display_name ) ? (string) $user->display_name : ''
155 ];
156 }
157
158 /**
159 * Load a doc, refusing anything that is not one.
160 *
161 * @since 4.9.0
162 *
163 * @param int $id Post id.
164 * @return \WP_Post|\WP_Error
165 */
166 protected function require_doc( $id ) {
167 $id = (int) $id;
168 $post = $id > 0 ? get_post( $id ) : null;
169
170 if ( ! $post || self::$doc_post_type !== $post->post_type ) {
171 return AbilityError::not_found( 'doc', $id );
172 }
173
174 return $post;
175 }
176
177 /**
178 * The capability a specific doc needs before this user may edit it.
179 *
180 * WordPress' `edit_post` meta capability resolves to a different primitive
181 * depending on who wrote the post and whether it is published; naming the
182 * generic `edit_docs` in the refusal would send an administrator to grant a
183 * capability the user already holds.
184 *
185 * @since 4.9.0
186 *
187 * @param \WP_Post $post The doc.
188 * @return string
189 */
190 protected function editing_capability( \WP_Post $post ) {
191 if ( (int) $post->post_author !== get_current_user_id() ) {
192 return 'edit_others_docs';
193 }
194
195 if ( 'publish' === $post->post_status ) {
196 return 'edit_published_docs';
197 }
198
199 if ( 'private' === $post->post_status ) {
200 return 'edit_private_docs';
201 }
202
203 return 'edit_docs';
204 }
205
206 /**
207 * The same question for deletion.
208 *
209 * @since 4.9.0
210 *
211 * @param \WP_Post $post The doc.
212 * @return string
213 */
214 protected function deleting_capability( \WP_Post $post ) {
215 if ( (int) $post->post_author !== get_current_user_id() ) {
216 return 'delete_others_docs';
217 }
218
219 if ( 'publish' === $post->post_status ) {
220 return 'delete_published_docs';
221 }
222
223 if ( 'private' === $post->post_status ) {
224 return 'delete_private_docs';
225 }
226
227 return 'delete_docs';
228 }
229
230 /**
231 * Translate a `wp/v2/docs` refusal into BetterDocs' typed vocabulary.
232 *
233 * @since 4.9.0
234 *
235 * @param \WP_Error $error What the controller returned.
236 * @param string $what What the caller was trying to do, as a phrase.
237 * @param \WP_Post|null $post The doc in play, when there is one.
238 * @return \WP_Error
239 */
240 protected function map_rest_error( \WP_Error $error, $what, $post = null ) {
241 $code = $error->get_error_code();
242 $message = $error->get_error_message();
243 $data = (array) $error->get_error_data();
244
245 switch ( $code ) {
246 case 'rest_post_invalid_id':
247 case 'rest_post_invalid_page_number':
248 return AbilityError::not_found( 'doc', $post instanceof \WP_Post ? (int) $post->ID : 0 );
249
250 case 'rest_cannot_create':
251 return AbilityError::capability_missing( 'edit_docs', $what );
252
253 case 'rest_cannot_publish':
254 return AbilityError::capability_missing( 'publish_docs', $what );
255
256 case 'rest_cannot_edit':
257 case 'rest_cannot_edit_others':
258 return AbilityError::capability_missing(
259 $post instanceof \WP_Post ? $this->editing_capability( $post ) : 'edit_docs',
260 $what
261 );
262
263 case 'rest_cannot_delete':
264 return AbilityError::capability_missing(
265 $post instanceof \WP_Post ? $this->deleting_capability( $post ) : 'delete_docs',
266 $what
267 );
268
269 case 'rest_cannot_read':
270 case 'rest_forbidden':
271 case 'rest_forbidden_context':
272 return AbilityError::capability_missing( 'read_private_docs', $what );
273
274 case 'rest_already_trashed':
275 return AbilityError::conflict( $message, [ 'object' => 'doc' ] );
276
277 case 'rest_invalid_param':
278 $field = isset( $data['params'] ) && is_array( $data['params'] ) ? (string) key( $data['params'] ) : '';
279
280 return AbilityError::invalid_input(
281 '' !== $field ? $field : 'input',
282 '' !== $field && isset( $data['params'][ $field ] ) ? (string) $data['params'][ $field ] : $message
283 );
284
285 default:
286 return AbilityError::upstream( $message, [ 'code' => (string) $code ] );
287 }
288 }
289 }
290