PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.0
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 3.5.2 All 199 releases
betterdocs / includes / Abilities / Faq / CreateFAQ.php

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

228 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Create an FAQ 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\AbilityError;
17 use WPDeveloper\BetterDocs\Abilities\Traits\ShapesFAQs;
18
19 /**
20 * Create one question and answer, optionally filed into a group.
21 *
22 * The answer is **HTML, not blocks** — an FAQ is edited in the FAQ Builder's
23 * rich-text box and printed with `the_content()`, never opened in the block
24 * editor. Markdown is the default input format because that is what a model
25 * writes; it is converted with Parsedown in safe mode.
26 *
27 * `group_name` is find-or-create: a name that matches an existing group (by
28 * slug, then by name) files the FAQ there, and a name that matches nothing
29 * creates the group. `group_id` never creates anything — an id that does not
30 * exist is a mistake, not an instruction.
31 *
32 * @since 4.9.0
33 */
34 class CreateFAQ extends AbilityBase {
35
36 use ShapesFAQs;
37
38 /**
39 * @since 4.9.0
40 */
41 public function __construct() {
42 $this->id = 'betterdocs/create-faq';
43 $this->label = __( 'Create FAQ', 'betterdocs' );
44 $this->description = __( 'Create a BetterDocs FAQ — one question and its answer — optionally in a group. The answer is markdown by default and is stored as HTML. group_name creates the group when it does not exist yet; group_id must already exist.', 'betterdocs' );
45 $this->capability = 'edit_others_docs';
46 }
47
48 /**
49 * @since 4.9.0
50 *
51 * @return array
52 */
53 public function get_annotations() {
54 return [
55 'readonly' => false,
56 'destructive' => false,
57 'idempotent' => false,
58 'priority' => 2.0,
59 'openWorldHint' => false
60 ];
61 }
62
63 /**
64 * @since 4.9.0
65 *
66 * @return array
67 */
68 public function get_input_schema() {
69 return [
70 'type' => 'object',
71 'additionalProperties' => false,
72 'required' => [ 'question', 'answer' ],
73 'properties' => [
74 'question' => [
75 'type' => 'string',
76 'description' => __( 'The question, as a visitor would ask it. Required. Stored as the FAQ\'s title, so HTML is stripped.', 'betterdocs' )
77 ],
78 'answer' => [
79 'type' => 'string',
80 'description' => __( 'The answer. Required. Markdown by default; see answer_format.', 'betterdocs' )
81 ],
82 'answer_format' => [
83 'type' => 'string',
84 'enum' => [ 'markdown', 'html' ],
85 'default' => 'markdown',
86 'description' => __( 'How to read the answer. markdown is converted to HTML with Parsedown in safe mode; html is passed through wp_kses_post. FAQ answers are HTML, never blocks.', 'betterdocs' )
87 ],
88 'group_id' => [
89 'type' => 'integer',
90 'description' => __( 'File the FAQ into this FAQ group, by id. The group must exist.', 'betterdocs' )
91 ],
92 'group_name' => [
93 'type' => 'string',
94 'description' => __( 'File the FAQ into this FAQ group, by name or slug. The group is created when nothing matches. Send group_id or group_name, not both.', 'betterdocs' )
95 ],
96 'status' => [
97 'type' => 'string',
98 'enum' => self::FAQ_STATUSES,
99 'default' => 'publish',
100 'description' => __( 'Post status. Defaults to publish, which is what the FAQ Builder does; a draft FAQ does not render.', 'betterdocs' )
101 ]
102 ],
103 'default' => []
104 ];
105 }
106
107 /**
108 * @since 4.9.0
109 *
110 * @return array
111 */
112 public function get_output_schema() {
113 return [
114 'type' => 'object',
115 'properties' => self::faq_shape_schema()
116 ];
117 }
118
119 /**
120 * @since 4.9.0
121 *
122 * @param array $input Validated input.
123 * @return array|\WP_Error
124 */
125 public function execute( $input ) {
126 $question = isset( $input['question'] ) ? trim( (string) $input['question'] ) : '';
127
128 if ( '' === $question ) {
129 return AbilityError::invalid_input( 'question', __( 'An FAQ needs a question.', 'betterdocs' ) );
130 }
131
132 // Resolved before the write, as `bd-create-term` does it (ADR-048): a
133 // group reference that cannot be honoured must not leave a stray FAQ
134 // behind.
135 $group = $this->group_ref_input( $input, true );
136
137 if ( is_wp_error( $group ) ) {
138 return $group;
139 }
140
141 $answer = $this->answer_html(
142 isset( $input['answer'] ) ? $input['answer'] : '',
143 isset( $input['answer_format'] ) ? (string) $input['answer_format'] : 'markdown'
144 );
145
146 $created = $this->dispatch(
147 'POST',
148 '/faq/create_post',
149 [
150 // Slashed on purpose: `wp_insert_post()` unslashes whatever it
151 // is given, so an unslashed answer loses every backslash in it
152 // (the same trap corrupts block attributes).
153 'post_title' => wp_slash( $question ),
154 'post_content' => wp_slash( $answer ),
155 'term_id' => null === $group ? 0 : (int) $group
156 ],
157 self::FAQ_NS
158 );
159
160 if ( is_wp_error( $created ) ) {
161 return $this->map_faq_error( $created, __( 'create an FAQ', 'betterdocs' ) );
162 }
163
164 $id = (int) $created;
165
166 if ( $id <= 0 ) {
167 return AbilityError::upstream( __( 'The FAQ was not created and BetterDocs reported no reason.', 'betterdocs' ) );
168 }
169
170 $status = $this->apply_faq_status( $id, isset( $input['status'] ) ? (string) $input['status'] : 'publish' );
171
172 if ( is_wp_error( $status ) ) {
173 return $status;
174 }
175
176 $item = $this->faq_item( $id );
177
178 if ( is_wp_error( $item ) ) {
179 return $item;
180 }
181
182 return $this->faq_shape( $item );
183 }
184
185 /**
186 * Put the FAQ into the status that was asked for.
187 *
188 * `FAQBuilder::insert_betterdocs_faq()` hard-codes `post_status =>
189 * 'publish'` and `update_betterdocs_faq()`'s status branch builds
190 * `[ 'ID', 'post_type', 'status' ]` for `wp_update_post()`, which has no
191 * `status` field and therefore changes nothing — measured. So a status other
192 * than publish is applied afterwards through `wp/v2/betterdocs_faq`, the
193 * only path that works. The FAQ is briefly published in that window; the
194 * post type is not publicly queryable and nothing links to it, but it is
195 * worth knowing.
196 *
197 * @since 4.9.0
198 *
199 * @param int $id FAQ post id.
200 * @param string $status Wanted post status.
201 * @return true|\WP_Error
202 */
203 protected function apply_faq_status( $id, $status ) {
204 if ( '' === $status || 'publish' === $status ) {
205 return true;
206 }
207
208 $post = get_post( (int) $id );
209
210 if ( $post && $status === $post->post_status ) {
211 return true;
212 }
213
214 $updated = $this->dispatch(
215 'POST',
216 '/' . self::FAQ_POST_TYPE . '/' . (int) $id,
217 [ 'status' => $status ],
218 'wp/v2'
219 );
220
221 if ( is_wp_error( $updated ) ) {
222 return $this->map_faq_error( $updated, __( 'change an FAQ\'s status', 'betterdocs' ) );
223 }
224
225 return true;
226 }
227 }
228