PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.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 / Core / Glossaries.php

Glossaries.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.0, at includes/Core/Glossaries.php

979 lines 29.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Core;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 // Glossary-by-term and glossary admin filters require tax_query and meta_key.
9 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
10 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
11 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
12
13 use WP_Query;
14 use WP_Error;
15 use WPDeveloper\BetterDocs\Utils\Base;
16 use WPDeveloper\BetterDocs\Utils\Helper;
17
18 class Glossaries extends Base {
19 /**
20 * REST API namespace
21 * @var string
22 */
23 private $namespace = 'betterdocs';
24 public $post_type = 'docs';
25 public $category = 'glossaries';
26
27 /**
28 *
29 * Initialize the class and start calling our hooks and filters
30 *
31 * @since 1.0.0
32 *
33 */
34 public function __construct() {
35 add_action( 'init', [ $this, 'register_post' ] );
36 // fires after a new betterdocs_glossaries is created
37 add_action( 'created_glossaries', [ $this, 'action_created_betterdocs_glossaries' ], 10, 2 );
38 add_action( 'rest_api_init', [ $this, 'register_api_endpoint' ] );
39 add_action( 'rest_api_init', [ $this, 'register_glossary_rest_fields' ] );
40 add_action( 'rest_glossaries_query', array( $this, 'glossaries_orderby_meta' ), 10, 2 );
41 add_action( 'rest_glossaries_query', array( $this, 'disable_language_filtering_for_admin_rest' ), 5, 2 );
42 // Ensure meta fields are properly exposed in REST API
43 add_filter( 'rest_prepare_glossaries', array( $this, 'add_meta_to_rest_response' ), 10, 3 );
44 // Enqueue Scripts
45 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
46 // Ensure existing glossaries have proper status
47 add_action( 'admin_init', [ $this, 'ensure_glossaries_have_status' ] );
48 }
49
50 public function register_post() {
51 // Register term meta fields for glossaries taxonomy
52 // Force register without duplicate checks to ensure they're properly registered
53 register_term_meta(
54 $this->category,
55 'status',
56 [
57 'show_in_rest' => true,
58 'single' => true,
59 'type' => 'string', // Changed to string to match React expectation
60 'default' => '1',
61 'sanitize_callback' => 'sanitize_text_field'
62 ]
63 );
64
65 register_term_meta(
66 $this->category,
67 'order',
68 [
69 'show_in_rest' => true,
70 'single' => true,
71 'type' => 'string', // Changed to string for consistency
72 'default' => '0',
73 'sanitize_callback' => 'sanitize_text_field'
74 ]
75 );
76
77 register_term_meta(
78 $this->category,
79 'glossary_term_description',
80 [
81 'show_in_rest' => true,
82 'single' => true,
83 'type' => 'string',
84 'default' => '',
85 'sanitize_callback' => 'wp_kses_post'
86 ]
87 );
88 }
89
90 public function enqueue( $hook ) {
91 if ( $hook === 'betterdocs_page_betterdocs-glossaries' ) {
92 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/css/faq.css' );
93
94 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/js/glossaries.js' );
95
96 // removing emoji support
97 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
98 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
99
100 betterdocs()->assets->localize(
101 'betterdocs-admin-glossaries',
102 'betterdocs',
103 [
104 'dir_url' => BETTERDOCS_ABSURL,
105 'rest_url' => esc_url_raw( rest_url() ),
106 'free_version' => betterdocs()->version,
107 'nonce' => wp_create_nonce( 'wp_rest' ),
108 'current_language' => Helper::get_current_language(),
109 'is_multilingual' => Helper::is_multilingual_active()
110 ]
111 );
112 }
113 }
114
115 public function output() {
116 betterdocs()->views->get( 'admin/glossaries' );
117 }
118
119 /**
120 * Ensure existing glossaries have proper status meta
121 * This fixes the issue where existing glossaries appear disabled
122 */
123 public function ensure_glossaries_have_status() {
124 // Run this every time in admin to ensure status is properly set
125 // Get all glossaries terms
126 $all_terms = get_terms( array(
127 'taxonomy' => 'glossaries',
128 'hide_empty' => false,
129 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- intentional WPML/Polylang language bypass.
130 ) );
131
132 if ( ! empty( $all_terms ) && ! is_wp_error( $all_terms ) ) {
133 foreach ( $all_terms as $term ) {
134 $current_status = get_term_meta( $term->term_id, 'status', true );
135
136 // If status is empty or not set, set it to enabled ('1')
137 if ( empty( $current_status ) || $current_status === '' ) {
138 update_term_meta( $term->term_id, 'status', '1' );
139 }
140
141 // Also ensure order meta exists
142 $current_order = get_term_meta( $term->term_id, 'order', true );
143 if ( empty( $current_order ) || $current_order === '' ) {
144 update_term_meta( $term->term_id, 'order', '0' );
145 }
146 }
147 }
148 }
149
150
151 /**
152 * Default the taxonomy's terms' order if it's not set.
153 *
154 * @param string $tax_slug The taxonomy's slug.
155 */
156 public function action_created_betterdocs_glossaries( $term_id ) {
157 $order = $this->get_max_taxonomy_order( 'glossaries' );
158 // update_term_meta( $term_id, 'order', $order++ );
159 update_term_meta( $term_id, 'status', '1' ); // Set as string
160 update_term_meta( $term_id, 'order', '0' ); // Also set order
161 }
162
163 /**
164 * Default the taxonomy's terms' order if it's not set.
165 *
166 * @param string $tax_slug The taxonomy's slug.
167 */
168 public function default_term_order( $tax_slug ) {
169 $terms = get_terms(
170 [
171 'taxonomy' => $tax_slug,
172 'hide_empty' => false,
173 ]
174 );
175
176 $order = $this->get_max_taxonomy_order( $tax_slug );
177
178 foreach ( $terms as $term ) {
179 if ( ! get_term_meta( $term->term_id, 'order', true ) ) {
180 update_term_meta( $term->term_id, 'order', $order );
181 ++$order;
182 }
183 }
184 }
185
186 /**
187 * Get the maximum order for this taxonomy. This will be applied to terms that don't have a tax position.
188 */
189 private function get_max_taxonomy_order( $tax_slug ) {
190 global $wpdb;
191
192 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live max-order needed when assigning new terms; cache would be stale.
193 $max_term_order = $wpdb->get_col(
194 $wpdb->prepare(
195 "SELECT MAX( CAST( tm.meta_value AS UNSIGNED ) )
196 FROM $wpdb->terms t
197 JOIN $wpdb->term_taxonomy tt ON t.term_id = tt.term_id AND tt.taxonomy = %s
198 JOIN $wpdb->termmeta tm ON tm.term_id = t.term_id WHERE tm.meta_key = 'order'",
199 $tax_slug
200 )
201 );
202
203 $max_term_order = is_array( $max_term_order ) ? current( $max_term_order ) : 0;
204
205 return (int) $max_term_order === 0 || empty( $max_term_order ) ? 1 : (int) $max_term_order + 1;
206 }
207
208 /**
209 * Re-Order the taxonomies based on the order value.
210 *
211 * @param array $pieces Array of SQL query clauses.
212 * @param array $taxonomies Array of taxonomy names.
213 * @param array $args Array of term query args.
214 */
215 public function set_tax_order( $pieces, $taxonomies, $args ) {
216 foreach ( $taxonomies as $taxonomy ) {
217 global $wpdb;
218
219 if ( $taxonomy === 'betterdocs_glossaries' ) {
220 $join_statement = " LEFT JOIN $wpdb->termmeta AS term_meta ON t.term_id = term_meta.term_id AND term_meta.meta_key = 'order'";
221
222 if ( ! $this->does_substring_exist( $pieces['join'], $join_statement ) ) {
223 $pieces['join'] .= $join_statement;
224 }
225
226 $pieces['orderby'] = 'ORDER BY CAST( term_meta.meta_value AS UNSIGNED )';
227 }
228 }
229
230 return $pieces;
231 }
232
233 /**
234 * Order the taxonomies on the front end.
235 */
236 public function front_end_order_terms() {
237 if ( ! is_admin() ) {
238 add_filter( 'terms_clauses', [ $this, 'set_tax_order' ], 10, 3 );
239 }
240 }
241
242 /**
243 * Check if a substring exists inside a string.
244 *
245 * @param string $string The main string (haystack) we're searching in.
246 * @param string $substring The substring we're searching for.
247 *
248 * @return bool True if substring exists, else false.
249 */
250 protected function does_substring_exist( $string, $substring ) {
251 return strstr( $string, $substring ) !== false;
252 }
253
254 public function register_api_endpoint() {
255 register_rest_route(
256 $this->namespace,
257 '/glossary/sample_data',
258 [
259 'methods' => [ 'POST' ],
260 'callback' => [ $this, 'create_glossary_sample' ],
261 'permission_callback' => function () {
262 return current_user_can( 'edit_others_posts' );
263 }
264 ]
265 );
266
267 register_rest_route(
268 $this->namespace,
269 '/glossary/posts/(?P<type>\S+)',
270 [
271 'methods' => [ 'GET' ],
272 'callback' => [ $this, 'fetch_faq_posts' ],
273 'permission_callback' => '__return_true'
274 ]
275 );
276
277 register_rest_route(
278 $this->namespace,
279 '/glossary/create_glossary',
280 [
281 'methods' => [ 'POST' ],
282 'callback' => [ $this, 'create_glossaries' ],
283 'permission_callback' => function () {
284 return current_user_can( 'edit_others_posts' );
285 }
286 ]
287 );
288
289 register_rest_route(
290 $this->namespace,
291 '/glossary/update_glossary',
292 [
293 'methods' => [ 'POST' ],
294 'callback' => [ $this, 'update_glossaries' ],
295 'permission_callback' => function () {
296 return current_user_can( 'edit_others_posts' );
297 }
298 ]
299 );
300
301 register_rest_route(
302 $this->namespace,
303 '/glossary/delete_glossary',
304 [
305 'methods' => [ 'POST' ],
306 'callback' => [ $this, 'delete_glossaries' ],
307 'permission_callback' => function () {
308 return current_user_can( 'edit_others_posts' );
309 }
310 ]
311 );
312
313 register_rest_route(
314 $this->namespace,
315 '/glossary/glossary_status',
316 [
317 'methods' => [ 'POST' ],
318 'callback' => [ $this, 'update_glossary_status' ],
319 'permission_callback' => function () {
320 return current_user_can( 'edit_others_posts' );
321 }
322 ]
323 );
324
325 register_rest_route(
326 $this->namespace,
327 '/glossary/glossaries_order',
328 [
329 'methods' => [ 'POST' ],
330 'callback' => [ $this, 'update_glossaries_order' ],
331 'permission_callback' => function () {
332 return current_user_can( 'edit_others_posts' );
333 }
334 ]
335 );
336
337 register_rest_route(
338 $this->namespace,
339 '/glossary/update_order_by_glossary',
340 [
341 'methods' => [ 'POST' ],
342 'callback' => [ $this, 'update_faq_order_by_glossary' ],
343 'permission_callback' => function () {
344 return current_user_can( 'edit_others_posts' );
345 }
346 ]
347 );
348
349 register_rest_route(
350 $this->namespace,
351 '/glossary/glossary_search',
352 [
353 'methods' => [ 'GET' ],
354 'callback' => [ $this, 'glossary_search' ],
355 'permission_callback' => '__return_true',
356 'args' => array(
357 'title' => array(
358 'type' => 'string',
359 'required' => true
360 ),
361 ),
362 ]
363 );
364
365 register_rest_route(
366 $this->namespace,
367 '/glossary/glossary_count',
368 [
369 'methods' => [ 'GET' ],
370 'callback' => [ $this, 'get_glossary_count' ],
371 'permission_callback' => function () {
372 return current_user_can( 'edit_others_posts' );
373 }
374 ]
375 );
376
377 register_rest_route(
378 $this->namespace,
379 '/glossary/check_existing',
380 [
381 'methods' => [ 'POST' ],
382 'callback' => [ $this, 'check_existing_glossaries' ],
383 'permission_callback' => function () {
384 return current_user_can( 'edit_others_posts' );
385 },
386 'args' => array(
387 'terms' => array(
388 'type' => 'array',
389 'required' => true,
390 'items' => array( 'type' => 'string' ),
391 ),
392 ),
393 ]
394 );
395 register_rest_route(
396 $this->namespace,
397 '/glossary/get_glossaries',
398 [
399 'methods' => [ 'GET' ],
400 'callback' => [ $this, 'get_glossaries' ],
401 'permission_callback' => function () {
402 return current_user_can( 'edit_others_posts' );
403 }
404 ]
405 );
406 }
407
408 public function create_glossary_sample( $params ) {
409 $sample_data = json_decode( $params->get_param( 'sample_data' ), true );
410 foreach ( $sample_data as $key => $value ) {
411 $insert_term = wp_insert_term(
412 $key,
413 'glossaries'
414 );
415 if ( $insert_term ) {
416 foreach ( $value['posts'] as $key => $value ) {
417 $this->insert_betterdocs_faq( $value['post_title'], $value['post_content'], $insert_term['term_id'] );
418 }
419 }
420 }
421 return true;
422 }
423
424 public function create_glossaries( $params ) {
425 $title = $params->get_param( 'title' );
426 $description = $params->get_param( 'description' );
427 $slug = $params->get_param( 'slug' );
428 $language = $params->get_param( 'language' );
429
430 // Create the term
431 $new_term = wp_insert_term(
432 $title,
433 'glossaries',
434 [
435 'slug' => $slug,
436 ]
437 );
438
439 if ( is_wp_error( $new_term ) ) {
440 return ['status' => 'failed', 'data' => $new_term];
441 }
442
443 // Set the custom field description
444 $term_id = $new_term['term_id'];
445 update_term_meta( $term_id, 'glossary_term_description', $description ); //phpcs:ignore inline styles are need for the front-end
446
447 // Set language for multilingual plugins
448 if ( $language && Helper::is_multilingual_active() ) {
449 // WPML Support
450 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
451 global $sitepress;
452 if ( $sitepress && method_exists( $sitepress, 'set_element_language_details' ) ) {
453 $sitepress->set_element_language_details( $term_id, 'tax_glossaries', null, $language );
454 }
455 }
456 // Polylang Support
457 elseif ( function_exists( 'pll_set_term_language' ) ) {
458 pll_set_term_language( $term_id, $language );
459 }
460 }
461
462 $new_term = $this->glossary_term_in_rest_api_schema($term_id, $description);
463 return ['status' => 'success', 'data' => $new_term];
464 }
465
466 /**
467 * Form Glossary Term In Rest Api Schema Format
468 *
469 * @param int $term_id
470 * @param string $meta_description
471 * @return array
472 */
473 public function glossary_term_in_rest_api_schema( $term_id, $meta_description = '' ) {
474 $term = get_term_by('id', $term_id, 'glossaries');
475 return [
476 'count' => $term->count,
477 'description' => $term->description,
478 'glossary_term_description' => $meta_description, //get the current description
479 'id' => $term->term_id,
480 'link' => get_permalink($term->term_id),
481 'meta' => [
482 'status' => get_term_meta( $term->term_id, 'status', true )
483 ],
484 'name' => $term->name,
485 'parent' => $term->parent,
486 'slug' => $term->slug,
487 'taxonomy' => $term->taxonomy
488 ];
489 }
490
491 public function update_glossaries( $request ) {
492 $term_id = $request->get_param( 'term_id' );
493 $title = $request->get_param( 'title' );
494 $description = $request->get_param( 'description' );
495 $description = ( $description !== 'undefined' ) ? $description : '';
496 $slug = $request->get_param( 'slug' );
497 $language = $request->get_param( 'language' );
498
499 // Verify that we're updating the correct language version of the term
500 if ( $language && Helper::is_multilingual_active() ) {
501 $term_language = null;
502
503 // WPML Support
504 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
505 global $sitepress;
506 if ( $sitepress && method_exists( $sitepress, 'get_element_language_details' ) ) {
507 $lang_details = $sitepress->get_element_language_details( $term_id, 'tax_glossaries' );
508 $term_language = $lang_details ? $lang_details->language_code : null;
509 }
510 }
511 // Polylang Support
512 elseif ( function_exists( 'pll_get_term_language' ) ) {
513 $term_language = pll_get_term_language( $term_id );
514 }
515
516 // Only update if the term belongs to the current language
517 if ( $term_language && $term_language !== $language ) {
518 return ['status' => 'failed', 'data' => new \WP_Error( 'wrong_language', 'Cannot update term from different language' )];
519 }
520 }
521
522 // Check if there's old data in the default description field and transfer it to the custom field
523 $old_description = get_term_field( 'description', $term_id, 'glossaries' );
524 if ( ! empty( $old_description ) && empty( get_term_meta( $term_id, 'glossary_term_description', true ) ) ) {
525 update_term_meta( $term_id, 'glossary_term_description', wp_kses_post( $old_description ) );
526 wp_update_term( $term_id, $this->glossaries, [ 'description' => '' ] );
527 }
528
529 // Update the term
530 $update = wp_update_term(
531 $term_id,
532 'glossaries',
533 [
534 'name' => $title,
535 'slug' => $slug,
536 ]
537 );
538
539 if ( is_wp_error( $update ) ) {
540 return ['status' => 'failed', 'data' => $update];
541 } else {
542 // Update the custom field description
543 update_term_meta( $term_id, 'glossary_term_description', $description );
544 return ['status' => 'success', 'data' => $this->glossary_term_in_rest_api_schema($term_id, $description)];
545 }
546 }
547
548 public function save_glossary_term_meta( $term_id, $tt_id ) {
549 if ( isset( $_POST['glossary_term_description'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
550 $description = wp_kses_post( wp_unslash( $_POST['glossary_term_description'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
551 update_term_meta( $term_id, 'glossary_term_description', $description );
552 }
553 }
554
555 public function register_glossary_rest_fields() {
556 register_rest_field(
557 'glossaries',
558 'glossary_term_description',
559 [
560 'get_callback' => function ( $term ) {
561 return get_term_meta( $term['id'], 'glossary_term_description', true );
562 },
563 'update_callback' => null,
564 'schema' => [
565 'description' => __( 'Glossary Term Description', 'betterdocs' ),
566 'type' => 'string',
567 'context' => [ 'view', 'edit' ],
568 ],
569 ]
570 );
571 }
572
573 public function delete_glossaries( $params ) {
574 $term_id = $params->get_param( 'term_id' );
575 $delete = wp_delete_term( $term_id, 'glossaries' );
576
577 if ( is_wp_error( $delete ) ) {
578 return $delete;
579 } else {
580 return true;
581 }
582 }
583
584
585 public function insert_betterdocs_glossaries( $title, $description, $slug = '' ) {
586 $insert_term = wp_insert_term(
587 $title,
588 'glossaries',
589 [
590 'slug' => $slug,
591 'description' => $description
592 ]
593 );
594
595 if ( is_wp_error( $insert_term ) ) {
596 return $insert_term;
597 } else {
598 return true;
599 }
600 }
601
602 public function update_glossaries_order( $params ) {
603 $glossaries_order = $params->get_param( 'glossaries_order' );
604 $glossaries_order = json_decode( $glossaries_order, true );
605
606 foreach ( $glossaries_order as $order_data ) {
607 if ( (int) $order_data['current_position'] != (int) $order_data['updated_position'] ) {
608 update_term_meta( $order_data['id'], 'order', ( (int) $order_data['updated_position'] ) );
609 }
610 }
611 return true;
612 }
613
614 public function insert_betterdocs_faq( $post_title, $post_content, $term_id ) {
615 $post = wp_insert_post(
616 [
617 'post_type' => 'betterdocs_faq',
618 'post_title' => wp_strip_all_tags( $post_title ),
619 'post_content' => $post_content,
620 'post_status' => 'publish'
621 ]
622 );
623
624 if ( $term_id ) {
625 $set_terms = wp_set_object_terms( $post, $term_id, 'glossaries' );
626 if ( is_wp_error( $set_terms ) ) {
627 return $set_terms;
628 } else {
629 return $this->update_faq_order_on_insert( $term_id, $post );
630 }
631 } else {
632 return $post;
633 }
634 }
635
636 public function update_faq_order_on_insert( $term_id, $post ) {
637 $term_meta = get_term_meta( $term_id, '_betterdocs_faq_order' );
638 if ( ! empty( $term_meta ) ) {
639 $term_meta_arr = explode( ',', $term_meta[0] );
640 if ( ! in_array( $post, $term_meta_arr ) ) {
641 array_unshift( $term_meta_arr, $post );
642 $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
643 return update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
644 }
645 } else {
646 return update_term_meta( $term_id, '_betterdocs_faq_order', $post );
647 }
648 }
649
650 /**
651 * Update _betterdocs_faq_order meta when new post created
652 */
653
654 public function update_faq_order_by_glossary( $params ) {
655 $term_id = $params->get_param( 'term_id' );
656 $posts = $params->get_param( 'posts' );
657 return update_term_meta( $term_id, '_betterdocs_faq_order', $posts );
658 }
659
660 public function create_betterdocs_faq( $params ) {
661 $post_title = $params->get_param( 'post_title' );
662 $post_content = $params->get_param( 'post_content' );
663 $term_id = $params->get_param( 'term_id' );
664 return $this->insert_betterdocs_faq( $post_title, $post_content, $term_id );
665 }
666
667 public function update_betterdocs_faq( $params ) {
668 $post_id = $params->get_param( 'post_id' );
669 $post_title = $params->get_param( 'post_title' );
670 $post_content = $params->get_param( 'post_content' );
671 $status = $params->get_param( 'status' );
672 $term_id = $params->get_param( 'term_id' );
673 if ( $status ) {
674 $data = [
675 'post_type' => 'betterdocs_faq',
676 'ID' => $post_id,
677 'status' => $status
678 ];
679 } else {
680 $data = [
681 'post_type' => 'betterdocs_faq',
682 'ID' => $post_id,
683 'post_title' => $post_title,
684 'post_content' => $post_content
685 ];
686
687 if ( $term_id ) {
688 $data['tax_input'] = [
689 'betterdocs_glossaries' => $term_id
690 ];
691
692 $term_meta = get_term_meta( $term_id, '_betterdocs_faq_order' );
693 $term_meta_arr = explode( ',', $term_meta[0] );
694 if ( ! in_array( $post_id, $term_meta_arr ) ) {
695 array_unshift( $term_meta_arr, $post_id );
696 $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
697 update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
698 }
699 }
700 }
701
702 return wp_update_post( $data );
703 }
704
705 public function delete_betterdocs_faq( $params ) {
706 $post_id = $params->get_param( 'post_id' );
707 return wp_delete_post( $post_id );
708 }
709
710 public function faq_post_loop( $args ) {
711 $posts = [];
712 $query = new WP_Query( $args );
713 if ( $query->have_posts() ) :
714 while ( $query->have_posts() ) :
715 $query->the_post();
716 $posts[ get_the_ID() ]['title'] = get_the_title();
717 $posts[ get_the_ID() ]['content'] = get_the_content();
718 endwhile;
719 endif;
720
721 return $posts;
722 }
723
724 public function update_glossary_status( $params ) {
725 $term_id = $params->get_param( 'term_id' );
726 $status = $params->get_param( 'status' );
727
728 // Ensure status is a string ('0' or '1')
729 $status = $status ? '1' : '0';
730
731 $result = update_term_meta( $term_id, 'status', $status );
732
733 // Return success response with updated status
734 return array(
735 'success' => $result !== false,
736 'term_id' => $term_id,
737 'status' => $status,
738 'message' => $result !== false ? 'Status updated successfully' : 'Failed to update status'
739 );
740 }
741
742 public function fetch_faq_posts( $params ) {
743 $faq = [];
744 $type = $params->get_param( 'type' );
745
746 if ( $type == 'category' ) {
747 $term_args = [
748 'taxonomy' => 'glossaries',
749 'hide_empty' => false,
750 ];
751
752 // Add language filtering if multilingual plugin is active and we should apply filtering
753 $current_language = Helper::get_current_language();
754 if ( $current_language && Helper::is_multilingual_active() && Helper::should_apply_language_filtering() ) {
755 // For WPML and Polylang, use 'lang' parameter
756 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) || function_exists( 'pll_current_language' ) ) {
757 $term_args['lang'] = $current_language;
758 }
759 }
760
761 $taxonomy_objects = get_terms( $term_args );
762
763 if ( $taxonomy_objects && ! is_wp_error( $taxonomy_objects ) ) :
764 foreach ( $taxonomy_objects as $term ) :
765 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- core glossary-by-term query; tax filtering is required functionality.
766 $args = [
767 'post_type' => 'betterdocs_faq',
768 'post_status' => 'publish',
769 'post_per_page' => -1,
770 'tax_query' => [
771 [
772 'taxonomy' => 'glossaries',
773 'field' => 'term_id',
774 'terms' => $term->term_id
775 ]
776 ]
777 ];
778
779 $posts = $this->faq_post_loop( $args );
780
781 $faq[ $term->slug ] = [
782 (array) $term,
783 'posts' => $posts
784 ];
785 endforeach;
786 endif;
787 } else {
788 $args = [
789 'post_type' => 'betterdocs_faq',
790 'post_status' => 'publish',
791 'post_per_page' => -1
792 ];
793 $posts = $this->faq_post_loop( $args );
794 $faq['posts'] = $posts;
795 }
796
797 return $faq;
798 }
799
800
801 public function glossary_search( $request ) {
802
803 $title = $request['title'];
804 $lang = $request['lang'] ?? null;
805
806 // Perform the taxonomy search
807 $taxonomy_args = array(
808 'name__like' => $title,
809 'taxonomy' => 'glossaries',
810 'hide_empty' => false
811 );
812
813 // Add language filtering if multilingual plugin is active
814 $language_to_use = $lang ?: Helper::get_current_language();
815 if ( $language_to_use && Helper::is_multilingual_active() ) {
816 // For WPML and Polylang, use 'lang' parameter
817 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) || function_exists( 'pll_current_language' ) ) {
818 $taxonomy_args['lang'] = $language_to_use;
819 }
820 }
821
822 $taxonomies = get_terms( $taxonomy_args );
823
824 if ( ! empty( $taxonomies ) ) {
825 $result = array();
826 foreach ( $taxonomies as $taxonomy ) {
827 $result[] = array(
828 'id' => $taxonomy->term_id,
829 'count' => $taxonomy->count,
830 'description' => $taxonomy->description,
831 'name' => $taxonomy->name,
832 'slug' => $taxonomy->slug
833 // Add more fields as needed
834 );
835 }
836 // Return the taxonomy data
837 return $result;
838 } else {
839 // Taxonomy not found
840 return new WP_Error( 'taxonomy_not_found', 'Taxonomy not found.', array( 'status' => 404 ) );
841 }
842 }
843
844 public function disable_language_filtering_for_admin_rest( $args, $request ) {
845 // Check if this is an admin REST request for glossaries management
846 if ( is_user_logged_in() && current_user_can( 'edit_others_posts' ) ) {
847 // Check if language parameter is explicitly provided in the request
848 $lang_param = $request->get_param( 'lang' );
849
850 if ( $lang_param ) {
851 // If language is specified, use it for filtering
852 $args['lang'] = $lang_param;
853 } else {
854 // If no language specified, remove language filtering to show all
855 unset( $args['lang'] );
856 // Add a temporary filter to bypass language restrictions
857 add_filter( 'get_terms', array( $this, 'ensure_all_glossaries_in_admin' ), 10, 3 );
858 }
859 }
860
861 return $args;
862 }
863
864 public function ensure_all_glossaries_in_admin( $terms, $taxonomies, $args ) {
865 // Only apply to glossaries taxonomy in admin context
866 if ( in_array( 'glossaries', (array) $taxonomies ) && is_user_logged_in() && current_user_can( 'edit_others_posts' ) ) {
867 // Remove this filter to prevent infinite loops
868 remove_filter( 'get_terms', array( $this, 'ensure_all_glossaries_in_admin' ), 10 );
869
870 // Get all glossaries terms without language filtering
871 $all_args = $args;
872 unset( $all_args['lang'] );
873 $all_args['suppress_filters'] = true; // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- admin glossary management requires bypassing all language filters.
874
875 $all_terms = get_terms( $all_args );
876
877 // Re-add the filter for future calls
878 add_filter( 'get_terms', array( $this, 'ensure_all_glossaries_in_admin' ), 10, 3 );
879
880 return is_wp_error( $all_terms ) ? $terms : $all_terms;
881 }
882
883 return $terms;
884 }
885
886 /**
887 * Add meta fields to REST API response
888 */
889 public function add_meta_to_rest_response( $response, $term, $request ) {
890 // Ensure meta fields are properly included
891 $meta = get_term_meta( $term->term_id );
892
893 // Format meta data as expected by React component
894 $response->data['meta'] = array();
895
896 if ( isset( $meta['status'] ) ) {
897 $response->data['meta']['status'] = $meta['status'];
898 } else {
899 $response->data['meta']['status'] = array( '1' ); // Default to enabled
900 }
901
902 if ( isset( $meta['order'] ) ) {
903 $response->data['meta']['order'] = $meta['order'];
904 } else {
905 $response->data['meta']['order'] = array( '0' ); // Default order
906 }
907
908 if ( isset( $meta['glossary_term_description'] ) ) {
909 $response->data['meta']['glossary_term_description'] = $meta['glossary_term_description'];
910 } else {
911 $response->data['meta']['glossary_term_description'] = array( '' );
912 }
913
914 return $response;
915 }
916
917 public function glossaries_orderby_meta( $args, $request ) {
918 if ( $args['taxonomy'] === 'glossaries' ) {
919 $args['orderby'] = 'meta_value_num';
920 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- ordering by status meta is required UX.
921 $args['meta_key'] = 'status';
922 }
923 return $args;
924 }
925
926 public function get_glossary_count( $request ) {
927 $options = get_option( 'store_glossary_count' );
928 return rest_ensure_response( $options );
929 }
930
931 /**
932 * Given a list of candidate glossary term names, return which ones already
933 * exist in the `glossaries` taxonomy. Used by the bulk Define-with-AI flow
934 * to surface conflicts before generation rather than at save time.
935 */
936 public function check_existing_glossaries( $request ) {
937 $terms = $request->get_param( 'terms' );
938 if ( ! is_array( $terms ) ) {
939 return rest_ensure_response( array( 'existing' => array() ) );
940 }
941
942 $existing = array();
943 $seen = array();
944
945 foreach ( $terms as $term ) {
946 if ( ! is_string( $term ) ) {
947 continue;
948 }
949 $trimmed = trim( $term );
950 if ( $trimmed === '' ) {
951 continue;
952 }
953 $key = strtolower( $trimmed );
954 if ( isset( $seen[ $key ] ) ) {
955 continue;
956 }
957 $seen[ $key ] = true;
958
959 if ( term_exists( $trimmed, 'glossaries' ) ) {
960 $existing[] = $trimmed;
961 }
962 }
963
964 return rest_ensure_response( array( 'existing' => $existing ) );
965 }
966 public function get_glossaries( $request ) {
967 $taxo = get_taxonomies(
968 array(
969 'name' => array(
970 'glossaries'
971 )
972 ),
973 'objects'
974 );
975
976 return rest_ensure_response( $taxo );
977 }
978 }
979