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 / Traits / ShapesTerms.php

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

316 lines 9.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 term shape the Terms abilities answer with, and the knowledge-base
4 * assignment they share.
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 * Four tools, one term object.
20 *
21 * `knowledge_bases` is reported as **slugs**, not ids, because that is what the
22 * `doc_category_knowledge_base` meta stores and what every reader of it
23 * compares against — including the permalink builder, which is why assigning a
24 * knowledge base changes a category's URL.
25 *
26 * @since 4.9.0
27 */
28 trait ShapesTerms {
29
30 // `self::TERM_TAXONOMIES` and `self::KB_META_KEY` are declared on
31 // `Abilities\AbilityBase`, which every user of this trait extends: PHP 7.4
32 // traits cannot carry constants.
33
34 /**
35 * `taxonomy` as every one of the four declares it.
36 *
37 * @since 4.9.0
38 *
39 * @return array
40 */
41 protected static function taxonomy_schema() {
42 return [
43 'type' => 'string',
44 'enum' => self::TERM_TAXONOMIES,
45 'description' => __( 'Which taxonomy: doc_category, doc_tag or glossaries. Knowledge bases have their own tools. Glossary terms need the enable_glossaries setting on. Required.', 'betterdocs' )
46 ];
47 }
48
49 /**
50 * The term summary all four answer with.
51 *
52 * @since 4.9.0
53 *
54 * @param array $term A `wp/v2/<taxonomy>` item.
55 * @param string $taxonomy Taxonomy name.
56 * @return array
57 */
58 protected function term_shape( array $term, $taxonomy ) {
59 $out = [
60 'id' => isset( $term['id'] ) ? (int) $term['id'] : 0,
61 'taxonomy' => (string) $taxonomy,
62 'name' => isset( $term['name'] ) ? (string) $term['name'] : '',
63 'slug' => isset( $term['slug'] ) ? (string) $term['slug'] : '',
64 'description' => isset( $term['description'] ) ? (string) $term['description'] : '',
65 'parent' => isset( $term['parent'] ) ? (int) $term['parent'] : 0,
66 // WordPress counts **published** posts here, so a category holding
67 // only drafts reports 0.
68 'count' => isset( $term['count'] ) ? (int) $term['count'] : 0,
69 'url' => isset( $term['link'] ) ? (string) $term['link'] : '',
70 'knowledge_bases' => $this->kb_slugs( $term )
71 ];
72
73 // BetterDocs' own REST fields, present on `doc_category` only.
74 if ( isset( $term['doc_category_order'] ) && null !== $term['doc_category_order'] ) {
75 $out['doc_category_order'] = (int) $term['doc_category_order'];
76 }
77
78 if ( isset( $term['total_docs_count'] ) && null !== $term['total_docs_count'] ) {
79 $out['total_docs_count'] = (int) $term['total_docs_count'];
80 }
81
82 // Glossary terms carry their own three metas, and their description is
83 // one of them rather than the term column (ADR-061). `status` is reported
84 // as the readable `publish` / `draft` the tools accept, not the stored
85 // `'1'` / `'0'`.
86 if ( 'glossaries' === $taxonomy ) {
87 $meta = isset( $term['meta'] ) && is_array( $term['meta'] ) ? $term['meta'] : [];
88
89 $stored_status = $this->first_meta_value( $meta, 'status' );
90
91 $out['status'] = '0' === $stored_status ? 'draft' : 'publish';
92 $out['order'] = (int) $this->first_meta_value( $meta, 'order' );
93
94 $description = $this->first_meta_value( $meta, self::GLOSSARY_DESCRIPTION_META );
95
96 if ( '' !== $description ) {
97 $out['description'] = $description;
98 }
99 }
100
101 return $out;
102 }
103
104 /**
105 * One term-meta value, whichever shape the controller reported it in.
106 *
107 * A registered single meta reads back as a scalar, but the glossary feature
108 * also rewrites these keys into a one-element array in its own REST filter,
109 * so both shapes reach this method.
110 *
111 * @since 4.9.0
112 *
113 * @param array $meta The item's `meta` map.
114 * @param string $key Meta key.
115 * @return string
116 */
117 protected function first_meta_value( array $meta, $key ) {
118 if ( ! isset( $meta[ $key ] ) ) {
119 return '';
120 }
121
122 $value = $meta[ $key ];
123
124 if ( is_array( $value ) ) {
125 $value = reset( $value );
126 }
127
128 return is_scalar( $value ) ? (string) $value : '';
129 }
130
131 /**
132 * JSON Schema for {@see self::term_shape()}.
133 *
134 * @since 4.9.0
135 *
136 * @return array
137 */
138 protected static function term_shape_schema() {
139 return [
140 'id' => [ 'type' => 'integer' ],
141 'taxonomy' => [ 'type' => 'string' ],
142 'name' => [ 'type' => 'string' ],
143 'slug' => [ 'type' => 'string' ],
144 'description' => [ 'type' => 'string' ],
145 'parent' => [ 'type' => 'integer' ],
146 'count' => [ 'type' => 'integer' ],
147 'url' => [ 'type' => 'string' ],
148 'knowledge_bases' => [
149 'type' => 'array',
150 'items' => [ 'type' => 'string' ]
151 ],
152 // Absent on `doc_tag`, which registers neither REST field.
153 'doc_category_order' => [ 'type' => 'integer' ],
154 'total_docs_count' => [ 'type' => 'integer' ],
155 // `glossaries` only.
156 'status' => [ 'type' => 'string' ],
157 'order' => [ 'type' => 'integer' ]
158 ];
159 }
160
161 /**
162 * The knowledge-base slugs a term item carries.
163 *
164 * @since 4.9.0
165 *
166 * @param array $term A `wp/v2/<taxonomy>` item.
167 * @return string[]
168 */
169 protected function kb_slugs( array $term ) {
170 $raw = isset( $term['meta'][ self::KB_META_KEY ] ) ? $term['meta'][ self::KB_META_KEY ] : [];
171
172 if ( ! is_array( $raw ) ) {
173 $raw = '' === $raw || null === $raw ? [] : [ $raw ];
174 }
175
176 $slugs = [];
177
178 foreach ( $raw as $slug ) {
179 if ( is_scalar( $slug ) && '' !== (string) $slug ) {
180 $slugs[] = (string) $slug;
181 }
182 }
183
184 return array_values( array_unique( $slugs ) );
185 }
186
187 /**
188 * Turn knowledge-base references into the slugs the meta key stores.
189 *
190 * Slugs, not ids: `doc_category_knowledge_base` holds slugs, and its
191 * registered sanitiser drops anything numeric precisely so an id written here
192 * cannot
193 * masquerade as one.
194 *
195 * @since 4.9.0
196 *
197 * @param array $refs Knowledge-base ids, slugs or names.
198 * @return string[]|\WP_Error
199 */
200 protected function kb_slugs_for( array $refs ) {
201 $available = $this->taxonomy_available( 'knowledge_base' );
202
203 if ( is_wp_error( $available ) ) {
204 return $available;
205 }
206
207 $ids = $this->resolve_terms( $refs, 'knowledge_base', false );
208
209 if ( is_wp_error( $ids ) ) {
210 return $ids;
211 }
212
213 $slugs = [];
214
215 foreach ( $ids as $id ) {
216 $summary = $this->term_summary( $id, 'knowledge_base' );
217
218 if ( null !== $summary ) {
219 $slugs[] = $summary['slug'];
220 }
221 }
222
223 return array_values( array_unique( $slugs ) );
224 }
225
226 /**
227 * Load a term, refusing an id from the wrong taxonomy.
228 *
229 * @since 4.9.0
230 *
231 * @param int $id Term id.
232 * @param string $taxonomy Taxonomy the caller named.
233 * @return object|\WP_Error
234 */
235 protected function require_term( $id, $taxonomy ) {
236 $id = (int) $id;
237 $term = $id > 0 ? get_term( $id, $taxonomy ) : null;
238
239 if ( ! $term || is_wp_error( $term ) ) {
240 return AbilityError::not_found( $this->term_object_name( $taxonomy ), $id );
241 }
242
243 return $term;
244 }
245
246 /**
247 * Translate a `wp/v2/<taxonomy>` refusal into the typed vocabulary.
248 *
249 * @since 4.9.0
250 *
251 * @param \WP_Error $error What the controller returned.
252 * @param string $taxonomy Taxonomy in play.
253 * @param string $what What the caller was trying to do, as a phrase.
254 * @return \WP_Error
255 */
256 protected function map_term_error( \WP_Error $error, $taxonomy, $what ) {
257 $code = $error->get_error_code();
258 $message = $error->get_error_message();
259 $data = (array) $error->get_error_data();
260
261 $object = get_taxonomy( $taxonomy );
262
263 switch ( $code ) {
264 case 'rest_term_invalid':
265 case 'rest_term_invalid_id':
266 return AbilityError::not_found( $this->term_object_name( $taxonomy ), isset( $data['id'] ) ? $data['id'] : 0 );
267
268 case 'rest_cannot_create':
269 return AbilityError::capability_missing(
270 $object && isset( $object->cap->edit_terms ) ? (string) $object->cap->edit_terms : 'manage_doc_terms',
271 $what
272 );
273
274 case 'rest_cannot_update':
275 // The membership meta key carries its own `auth_callback`,
276 // so this one code covers two different capabilities and the
277 // answer has to say which.
278 if ( isset( $data['key'] ) && self::KB_META_KEY === $data['key'] ) {
279 return AbilityError::capability_missing( 'manage_knowledge_base_terms', __( 'assign knowledge bases to a doc category', 'betterdocs' ) );
280 }
281
282 return AbilityError::capability_missing(
283 $object && isset( $object->cap->edit_terms ) ? (string) $object->cap->edit_terms : 'edit_doc_terms',
284 $what
285 );
286
287 case 'rest_cannot_delete':
288 return AbilityError::capability_missing(
289 $object && isset( $object->cap->delete_terms ) ? (string) $object->cap->delete_terms : 'delete_doc_terms',
290 $what
291 );
292
293 case 'rest_forbidden':
294 case 'rest_forbidden_context':
295 return AbilityError::capability_missing(
296 $object && isset( $object->cap->manage_terms ) ? (string) $object->cap->manage_terms : 'manage_doc_terms',
297 $what
298 );
299
300 case 'rest_trash_not_supported':
301 return AbilityError::conflict( $message, [ 'object' => $this->term_object_name( $taxonomy ) ] );
302
303 case 'rest_invalid_param':
304 $field = isset( $data['params'] ) && is_array( $data['params'] ) ? (string) key( $data['params'] ) : '';
305
306 return AbilityError::invalid_input(
307 '' !== $field ? $field : 'input',
308 '' !== $field && isset( $data['params'][ $field ] ) ? (string) $data['params'][ $field ] : $message
309 );
310
311 default:
312 return AbilityError::upstream( $message, [ 'code' => (string) $code ] );
313 }
314 }
315 }
316