PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.6.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.6.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 / FAQBuilder.php

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

1,294 lines 41.7 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 // FAQ-by-category lookups require tax_query / meta_key filters by design.
9 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
10 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
11 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
12
13 use WP_Error;
14 use WP_Query;
15 use WPDeveloper\BetterDocs\Utils\Base;
16 use WPDeveloper\BetterDocs\Utils\Helper;
17
18 class FAQBuilder extends Base {
19 /**
20 * REST API namespace
21 * @var string
22 */
23 private $namespace = 'betterdocs';
24 public $post_type = 'betterdocs_faq';
25 public $category = 'betterdocs_faq_category';
26
27 /**
28 * Product FAQ groups taxonomy. Shares the betterdocs_faq post type with the
29 * General FAQ groups but is a separate taxonomy so the two never mix in the
30 * admin or on the front end. Used by the WooCommerce Product FAQ tab.
31 *
32 * @var string
33 */
34 public $product_category = 'betterdocs_product_faq_category';
35
36 /**
37 * Term meta on a Product FAQ group holding the product_cat term IDs the
38 * group is assigned to. Products in those categories inherit the group.
39 */
40 const GROUP_PRODUCT_CATS_META = '_betterdocs_faq_group_product_cats';
41
42 /**
43 * Term meta on a Product FAQ group holding the individual product IDs the
44 * group is assigned to directly.
45 */
46 const GROUP_PRODUCTS_META = '_betterdocs_faq_group_products';
47
48 /**
49 * Term meta flagging a Product FAQ group as shown on every product (no
50 * per-product/per-category targeting). Set on the store-aware sample groups
51 * so they render storefront-wide immediately; cleared the moment the owner
52 * saves explicit category/product assignments.
53 */
54 const GROUP_ALL_PRODUCTS_META = '_betterdocs_faq_group_all_products';
55
56 /**
57 * Per-request cache of draft FAQ counts keyed by taxonomy → [ term_id => count ].
58 * Lets the betterdocs_draft_count REST field resolve every term from a single
59 * grouped query instead of one WP_Query per term (N+1).
60 *
61 * @var array<string, array<int, int>>
62 */
63 protected $draft_count_cache = [];
64
65 /**
66 *
67 * Initialize the class and start calling our hooks and filters
68 *
69 * @since 1.0.0
70 *
71 */
72 public function __construct() {
73 // assign default admin capabilities for docs, doc terms, doc tags, knowledge base
74 add_action( 'init', [ $this, 'register_post' ] );
75 // fires after a new betterdocs_faq_category is created
76 add_action( 'created_betterdocs_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
77 add_action( 'created_betterdocs_product_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
78 add_action( 'rest_api_init', [ $this, 'register_api_endpoint' ] );
79 add_action( 'rest_api_init', [ $this, 'register_category_count_fields' ] );
80 add_action( 'rest_betterdocs_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
81 add_action( 'rest_betterdocs_product_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
82
83 // Classic FAQ Group list (edit-tags.php?taxonomy=betterdocs_faq_category):
84 // enable drag-and-drop ordering + persist it, mirroring the classic Doc
85 // Categories screen.
86 add_action( 'admin_enqueue_scripts', [ $this, 'classic_category_scripts' ] );
87 add_action( 'wp_ajax_update_faq_cat_order', [ $this, 'ajax_update_category_order' ] );
88 add_action( 'admin_head', [ $this, 'order_admin_terms' ] );
89 }
90
91 /**
92 * Expose a per-group draft FAQ count on the betterdocs_faq_category REST
93 * response so the FAQ Builder can show "N questions • M draft". The term
94 * `count` only tracks published FAQs (via _update_post_term_count), so the
95 * draft count is computed here.
96 *
97 * @since 4.4.0
98 */
99 public function register_category_count_fields() {
100 foreach ( [ $this->category, $this->product_category ] as $taxonomy ) {
101 register_rest_field(
102 $taxonomy,
103 'betterdocs_draft_count',
104 [
105 'get_callback' => function ( $term ) {
106 $counts = $this->get_draft_counts_by_term( $term['taxonomy'] );
107 return isset( $counts[ (int) $term['id'] ] ) ? (int) $counts[ (int) $term['id'] ] : 0;
108 },
109 'schema' => [
110 'type' => 'integer',
111 'context' => [ 'view', 'edit' ],
112 ],
113 ]
114 );
115 }
116 }
117
118 /**
119 * Draft FAQ counts for every term in a taxonomy, keyed by term_id.
120 *
121 * Computed once per request with a single grouped query and memoized, so the
122 * betterdocs_draft_count REST field no longer fires a WP_Query per term when
123 * a category list is assembled (avoids the N+1 on large group counts).
124 *
125 * @param string $taxonomy
126 * @return array<int, int>
127 */
128 protected function get_draft_counts_by_term( $taxonomy ) {
129 if ( isset( $this->draft_count_cache[ $taxonomy ] ) ) {
130 return $this->draft_count_cache[ $taxonomy ];
131 }
132
133 global $wpdb;
134
135 $rows = $wpdb->get_results(
136 $wpdb->prepare(
137 "SELECT tt.term_id, COUNT( p.ID ) AS draft_count
138 FROM {$wpdb->posts} p
139 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
140 INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
141 WHERE p.post_type = %s
142 AND p.post_status = 'draft'
143 AND tt.taxonomy = %s
144 GROUP BY tt.term_id",
145 $this->post_type,
146 $taxonomy
147 )
148 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
149
150 $counts = [];
151 foreach ( (array) $rows as $row ) {
152 $counts[ (int) $row->term_id ] = (int) $row->draft_count;
153 }
154
155 return $this->draft_count_cache[ $taxonomy ] = $counts;
156 }
157
158 public function output() {
159 betterdocs()->views->get( 'admin/faq-builder' );
160 }
161
162 /**
163 *
164 * Register post type and taxonomies
165 *
166 * @since 1.0.0
167 *
168 */
169 public function register_post() {
170 /**
171 * Register category taxonomy
172 */
173 $category_labels = [
174 'name' => __( 'FAQ Categories', 'betterdocs' ),
175 'singular_name' => __( 'FAQ Category', 'betterdocs' ),
176 'all_items' => __( 'FAQ Categories', 'betterdocs' ),
177 'parent_item' => __( 'Parent FAQ Category', 'betterdocs' ),
178 'parent_item_colon' => __( 'Parent FAQ Category:', 'betterdocs' ),
179 'edit_item' => __( 'Edit Category', 'betterdocs' ),
180 'update_item' => __( 'Update Category', 'betterdocs' ),
181 'add_new_item' => __( 'Add New FAQ Category', 'betterdocs' ),
182 'new_item_name' => __( 'New FAQ Category Name', 'betterdocs' ),
183 'menu_name' => __( 'Categories', 'betterdocs' )
184 ];
185
186 $category_args = [
187 'hierarchical' => true,
188 'public' => false,
189 'labels' => $category_labels,
190 'show_ui' => true,
191 'show_admin_column' => true,
192 'query_var' => true,
193 'show_in_rest' => true,
194 'has_archive' => false,
195 'rewrite' => false,
196 'capabilities' => [
197 'manage_terms' => 'manage_doc_terms',
198 'edit_terms' => 'edit_doc_terms',
199 'delete_terms' => 'delete_doc_terms',
200 'assign_terms' => 'edit_docs'
201 ]
202 ];
203
204 register_taxonomy( $this->category, [ $this->post_type ], $category_args );
205 register_term_meta( $this->category, 'order', [ 'show_in_rest' => true ] );
206 register_term_meta( $this->category, 'status', [ 'show_in_rest' => true ] );
207 register_term_meta( $this->category, '_betterdocs_faq_order', [ 'show_in_rest' => true ] );
208 register_term_meta( $this->category, 'faq_group_icon', [ 'show_in_rest' => true ]);
209
210 /**
211 * Register the Product FAQ groups taxonomy (WooCommerce tab). It mirrors
212 * the General FAQ category taxonomy and shares the betterdocs_faq post
213 * type, but is kept separate so Product FAQ groups never appear in the
214 * General FAQ Builder tab.
215 */
216 $product_category_labels = [
217 'name' => __( 'Product FAQ Categories', 'betterdocs' ),
218 'singular_name' => __( 'Product FAQ Category', 'betterdocs' ),
219 'all_items' => __( 'Product FAQ Categories', 'betterdocs' ),
220 'parent_item' => __( 'Parent Product FAQ Category', 'betterdocs' ),
221 'parent_item_colon' => __( 'Parent Product FAQ Category:', 'betterdocs' ),
222 'edit_item' => __( 'Edit Product FAQ Category', 'betterdocs' ),
223 'update_item' => __( 'Update Product FAQ Category', 'betterdocs' ),
224 'add_new_item' => __( 'Add New Product FAQ Category', 'betterdocs' ),
225 'new_item_name' => __( 'New Product FAQ Category Name', 'betterdocs' ),
226 'menu_name' => __( 'Product FAQ Categories', 'betterdocs' )
227 ];
228
229 $product_category_args = $category_args;
230 $product_category_args['labels'] = $product_category_labels;
231 $product_category_args['show_admin_column'] = false;
232
233 register_taxonomy( $this->product_category, [ $this->post_type ], $product_category_args );
234 register_term_meta( $this->product_category, 'order', [ 'show_in_rest' => true ] );
235 register_term_meta( $this->product_category, 'status', [ 'show_in_rest' => true ] );
236 register_term_meta( $this->product_category, '_betterdocs_faq_order', [ 'show_in_rest' => true ] );
237 register_term_meta( $this->product_category, 'faq_group_icon', [ 'show_in_rest' => true ] );
238
239 // Product FAQ group assignments: which WooCommerce product categories and
240 // which individual products this group is shown on. Stored on the group
241 // term so the assignment lives in the Create/Update FAQ Group screen.
242 $assignment_meta_args = [
243 'single' => true,
244 'type' => 'array',
245 'show_in_rest' => [
246 'schema' => [
247 'type' => 'array',
248 'items' => [ 'type' => 'integer' ],
249 ],
250 ],
251 ];
252 register_term_meta( $this->product_category, self::GROUP_PRODUCT_CATS_META, $assignment_meta_args );
253 register_term_meta( $this->product_category, self::GROUP_PRODUCTS_META, $assignment_meta_args );
254 register_term_meta(
255 $this->product_category,
256 self::GROUP_ALL_PRODUCTS_META,
257 [
258 'single' => true,
259 'type' => 'boolean',
260 'show_in_rest' => true,
261 ]
262 );
263
264 /**
265 * Register post type
266 */
267 $labels = [
268 'name' => __( 'BetterDocs FAQ', 'betterdocs' ),
269 'singular_name' => __( 'BetterDocs FAQ', 'betterdocs' ),
270 'menu_name' => __( 'FAQ', 'betterdocs' ),
271 'name_admin_bar' => __( 'FAQ', 'betterdocs' ),
272 'add_new' => __( 'Add New', 'betterdocs' ),
273 'add_new_item' => __( 'Add New FAQ', 'betterdocs' ),
274 'new_item' => __( 'New FAQ', 'betterdocs' ),
275 'edit_item' => __( 'Edit FAQ', 'betterdocs' ),
276 'view_item' => __( 'View FAQ', 'betterdocs' ),
277 'all_items' => __( 'All FAQ', 'betterdocs' ),
278 'search_items' => __( 'Search FAQ', 'betterdocs' ),
279 'parent_item_colorn' => null,
280 'not_found' => __( 'No FAQ found', 'betterdocs' ),
281 'not_found_in_trash' => __( 'No FAQ found in trash', 'betterdocs' )
282 ];
283
284 $args = [
285 'labels' => $labels,
286 'description' => __( 'Add new faq from here', 'betterdocs' ),
287 'public' => false,
288 'public_queryable' => true,
289 'exclude_from_search' => false,
290 'show_ui' => true,
291 'show_in_menu' => false,
292 'query_var' => true,
293 'capability_type' => [ 'doc', 'docs' ],
294 'hierarchical' => true,
295 'map_meta_cap' => true,
296 'has_archive' => false,
297 'rewrite' => false,
298 'show_in_rest' => true,
299 'menu_icon' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg' ),
300 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'author', 'revisions', 'custom-fields', 'comments' ]
301 ];
302
303 register_post_type( $this->post_type, $args );
304 register_post_meta( $this->post_type, 'faq_open_by_default', [ 'show_in_rest' => true, 'single' => true ] );
305 }
306
307 /**
308 * Default the taxonomy's terms' order if it's not set.
309 *
310 * @param string $tax_slug The taxonomy's slug.
311 */
312 public function action_created_betterdocs_faq_category( $term_id ) {
313 $term = get_term( $term_id );
314 $taxonomy = ( $term && ! is_wp_error( $term ) ) ? $term->taxonomy : $this->category;
315 $order = $this->get_max_taxonomy_order( $taxonomy );
316 update_term_meta( $term_id, 'order', $order++ );
317 update_term_meta( $term_id, 'status', 1 );
318 }
319
320 /**
321 * Default the taxonomy's terms' order if it's not set.
322 *
323 * @param string $tax_slug The taxonomy's slug.
324 */
325 public function default_term_order( $tax_slug ) {
326 $terms = get_terms(
327 [
328 'taxonomy' => $tax_slug,
329 'hide_empty' => false,
330 ]
331 );
332 $order = $this->get_max_taxonomy_order( $tax_slug );
333
334 foreach ( $terms as $term ) {
335 if ( ! get_term_meta( $term->term_id, 'order', true ) ) {
336 update_term_meta( $term->term_id, 'order', $order );
337 ++$order;
338 }
339 }
340 }
341
342 /**
343 * Get the maximum order for this taxonomy. This will be applied to terms that don't have a tax position.
344 */
345 private function get_max_taxonomy_order( $tax_slug ) {
346 global $wpdb;
347 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live max-order needed when assigning new terms; cache would be stale.
348 $max_term_order = $wpdb->get_col(
349 $wpdb->prepare(
350 "SELECT MAX( CAST( tm.meta_value AS UNSIGNED ) )
351 FROM $wpdb->terms t
352 JOIN $wpdb->term_taxonomy tt ON t.term_id = tt.term_id AND tt.taxonomy = %s
353 JOIN $wpdb->termmeta tm ON tm.term_id = t.term_id WHERE tm.meta_key = 'order'",
354 $tax_slug
355 )
356 );
357
358 $max_term_order = is_array( $max_term_order ) ? current( $max_term_order ) : 0;
359
360 return (int) $max_term_order === 0 || empty( $max_term_order ) ? 1 : (int) $max_term_order + 1;
361 }
362
363 /**
364 * Re-Order the taxonomies based on the order value.
365 *
366 * @param array $pieces Array of SQL query clauses.
367 * @param array $taxonomies Array of taxonomy names.
368 * @param array $args Array of term query args.
369 */
370 public function set_tax_order( $pieces, $taxonomies, $args ) {
371 // Only force the manual drag-drop `order` meta when the FAQ Builder
372 // header dropdown is on "default". For name/count/id sort modes, let
373 // get_terms() keep its own orderby so the front end matches the builder.
374 if ( betterdocs()->query->get_faq_order_key() !== 'default' ) {
375 return $pieces;
376 }
377
378 foreach ( $taxonomies as $taxonomy ) {
379 global $wpdb;
380
381 if ( $taxonomy === 'betterdocs_faq_category' ) {
382 $join_statement = " LEFT JOIN $wpdb->termmeta AS term_meta ON t.term_id = term_meta.term_id AND term_meta.meta_key = 'order'";
383
384 if ( ! $this->does_substring_exist( $pieces['join'], $join_statement ) ) {
385 $pieces['join'] .= $join_statement;
386 }
387
388 $pieces['orderby'] = 'ORDER BY CAST( term_meta.meta_value AS UNSIGNED )';
389 }
390 }
391
392 return $pieces;
393 }
394
395 /**
396 * Order the taxonomies on the front end.
397 */
398 public function front_end_order_terms() {
399 if ( ! is_admin() ) {
400 add_filter( 'terms_clauses', [ $this, 'set_tax_order' ], 10, 3 );
401 }
402 }
403
404 /**
405 * Check if a substring exists inside a string.
406 *
407 * @param string $string The main string (haystack) we're searching in.
408 * @param string $substring The substring we're searching for.
409 *
410 * @return bool True if substring exists, else false.
411 */
412 protected function does_substring_exist( $string, $substring ) {
413 return strstr( $string, $substring ) !== false;
414 }
415
416 /**
417 * Load the shared drag-and-drop sorter on the classic FAQ Group list
418 * (edit-tags.php?taxonomy=betterdocs_faq_category). Reuses the generic,
419 * config-driven admin/js/category-edit.js — the same script the classic Doc
420 * Categories screen uses — pointed at the FAQ category ordering AJAX action.
421 *
422 * jquery + jquery-ui-sortable are declared explicitly so `.sortable()` is
423 * always defined (the bare script reported jQuery/Sortable as undefined here
424 * because no sorter was enqueued on this screen at all).
425 *
426 * @param string $hook Current admin page hook.
427 */
428 public function classic_category_scripts( $hook ) {
429 if ( 'edit-tags.php' !== $hook ) {
430 return;
431 }
432
433 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
434 if ( ! $screen || $screen->taxonomy !== $this->category ) {
435 return;
436 }
437
438 betterdocs()->assets->enqueue(
439 'betterdocs-category-edit',
440 'admin/js/category-edit.js',
441 [ 'jquery', 'jquery-ui-sortable' ]
442 );
443
444 betterdocs()->assets->localize(
445 'betterdocs-category-edit',
446 'betterdocsCategorySorting',
447 [
448 'action' => 'update_faq_cat_order',
449 'selector' => '.taxonomy-' . $this->category,
450 'ajaxurl' => admin_url( 'admin-ajax.php' ),
451 'nonce' => wp_create_nonce( 'faq_cat_order_nonce' ),
452 // Paged only sets the ordering base offset (the AJAX write itself
453 // is nonce-protected), so read it directly from the WP term-list
454 // pagination link so cross-page ordering stays correct.
455 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
456 'per_page_id' => "edit_{$this->category}_per_page",
457 ]
458 );
459 }
460
461 /**
462 * Persist FAQ Group order from the classic drag-and-drop sorter. Writes the
463 * `order` term meta that the React FAQ Builder and the front end already read,
464 * so all three stay in sync. Mirrors PostType::update_category_order.
465 */
466 public function ajax_update_category_order() {
467 if ( ! check_ajax_referer( 'faq_cat_order_nonce', 'nonce', false ) ) {
468 wp_send_json_error( __( 'Nonce Failed', 'betterdocs' ) );
469 }
470
471 if ( ! current_user_can( 'edit_others_posts' ) ) {
472 wp_send_json_error( __( 'You don\'t have permission to manage FAQ groups.', 'betterdocs' ) );
473 }
474
475 $base_index = isset( $_POST['base_index'] ) ? intval( $_POST['base_index'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
476
477 if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {
478 $ordering = array_filter(
479 array_map(
480 function ( $item ) {
481 if ( is_array( $item ) && isset( $item['term_id'], $item['order'] ) ) {
482 return [
483 'term_id' => intval( $item['term_id'] ),
484 'order' => intval( $item['order'] ),
485 ];
486 }
487 return null;
488 },
489 wp_unslash( $_POST['data'] ) // phpcs:ignore
490 )
491 );
492 } else {
493 $ordering = [];
494 }
495
496 foreach ( $ordering as $order_data ) {
497 update_term_meta( $order_data['term_id'], 'order', $order_data['order'] + $base_index );
498 }
499
500 wp_send_json_success( __( 'Successfully updated.', 'betterdocs' ) );
501 }
502
503 /**
504 * Order the classic FAQ Group list by the manual `order` term meta so the
505 * drag-and-drop order persists across reloads. Seeds the meta for any terms
506 * missing it first (e.g. groups created before ordering existed). Mirrors
507 * PostType::order_terms for the Doc Categories screen.
508 */
509 public function order_admin_terms() {
510 global $current_screen;
511
512 if (
513 ! isset( $_GET['orderby'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
514 $current_screen &&
515 ! empty( $current_screen->base ) &&
516 $current_screen->base === 'edit-tags' &&
517 $current_screen->taxonomy === $this->category
518 ) {
519 $this->default_term_order( $this->category );
520 add_filter( 'terms_clauses', [ $this, 'admin_order_terms_clauses' ], 10, 3 );
521 }
522 }
523
524 /**
525 * terms_clauses filter (classic FAQ Group admin list only): force ORDER BY
526 * the `order` term meta. Unlike set_tax_order this is not gated on the
527 * builder's saved order key — the classic screen is always manual ordering.
528 */
529 public function admin_order_terms_clauses( $pieces, $taxonomies, $args ) {
530 if ( ! in_array( $this->category, (array) $taxonomies, true ) ) {
531 return $pieces;
532 }
533
534 global $wpdb;
535 $join_statement = " LEFT JOIN $wpdb->termmeta AS bd_faq_order ON t.term_id = bd_faq_order.term_id AND bd_faq_order.meta_key = 'order'";
536
537 if ( ! $this->does_substring_exist( $pieces['join'], $join_statement ) ) {
538 $pieces['join'] .= $join_statement;
539 }
540
541 $pieces['orderby'] = 'ORDER BY CAST( bd_faq_order.meta_value AS UNSIGNED )';
542
543 return $pieces;
544 }
545
546 public function register_api_endpoint() {
547 register_rest_route(
548 $this->namespace,
549 '/faq/sample_data',
550 [
551 'methods' => [ 'POST' ],
552 'callback' => [ $this, 'create_faq_sample' ],
553 'permission_callback' => function () {
554 return current_user_can( 'edit_others_posts' );
555 }
556 ]
557 );
558
559 register_rest_route(
560 $this->namespace,
561 '/faq/posts/(?P<type>\S+)',
562 [
563 'methods' => [ 'GET' ],
564 'callback' => [ $this, 'fetch_faq_posts' ],
565 'permission_callback' => function () {
566 return current_user_can( 'edit_others_posts' );
567 }
568 ]
569 );
570
571 register_rest_route(
572 $this->namespace,
573 '/faq/create_category',
574 [
575 'methods' => [ 'POST' ],
576 'callback' => [ $this, 'create_faq_category' ],
577 'permission_callback' => function () {
578 return current_user_can( 'edit_others_posts' );
579 }
580 ]
581 );
582
583 register_rest_route(
584 $this->namespace,
585 '/faq/update_category',
586 [
587 'methods' => [ 'POST' ],
588 'callback' => [ $this, 'update_faq_category' ],
589 'permission_callback' => function () {
590 return current_user_can( 'edit_others_posts' );
591 }
592 ]
593 );
594
595 register_rest_route(
596 $this->namespace,
597 '/faq/delete_category',
598 [
599 'methods' => [ 'POST' ],
600 'callback' => [ $this, 'delete_faq_category' ],
601 'permission_callback' => function () {
602 return current_user_can( 'edit_others_posts' );
603 }
604 ]
605 );
606
607 register_rest_route(
608 $this->namespace,
609 '/faq/create_post',
610 [
611 'methods' => [ 'POST' ],
612 'callback' => [ $this, 'create_betterdocs_faq' ],
613 'permission_callback' => function () {
614 return current_user_can( 'edit_others_posts' );
615 }
616 ]
617 );
618
619 register_rest_route(
620 $this->namespace,
621 '/faq/update_post',
622 [
623 'methods' => [ 'POST' ],
624 'callback' => [ $this, 'update_betterdocs_faq' ],
625 'permission_callback' => function () {
626 return current_user_can( 'edit_others_posts' );
627 }
628 ]
629 );
630
631 register_rest_route(
632 $this->namespace,
633 '/faq/delete_post',
634 [
635 'methods' => [ 'POST' ],
636 'callback' => [ $this, 'delete_betterdocs_faq' ],
637 'permission_callback' => function () {
638 return current_user_can( 'edit_others_posts' );
639 }
640 ]
641 );
642
643 register_rest_route(
644 $this->namespace,
645 '/faq/category_status',
646 [
647 'methods' => [ 'POST' ],
648 'callback' => [ $this, 'update_category_status' ],
649 'permission_callback' => function () {
650 return current_user_can( 'edit_others_posts' );
651 }
652 ]
653 );
654
655 register_rest_route(
656 $this->namespace,
657 '/faq/category_order',
658 [
659 'methods' => [ 'POST' ],
660 'callback' => [ $this, 'update_faq_category_order' ],
661 'permission_callback' => function () {
662 return current_user_can( 'edit_others_posts' );
663 }
664 ]
665 );
666
667 register_rest_route(
668 $this->namespace,
669 '/faq/update_order_by_category',
670 [
671 'methods' => [ 'POST' ],
672 'callback' => [ $this, 'update_faq_order_by_category' ],
673 'permission_callback' => function () {
674 return current_user_can( 'edit_others_posts' );
675 }
676 ]
677 );
678
679 register_rest_route(
680 $this->namespace,
681 '/faq/order',
682 [
683 'methods' => [ 'POST' ],
684 'callback' => [ $this, 'update_faq_order_preference' ],
685 'permission_callback' => function () {
686 return current_user_can( 'edit_others_posts' );
687 }
688 ]
689 );
690
691 register_rest_route(
692 $this->namespace,
693 '/faq/uncategorised',
694 [
695 'methods' => [ 'GET' ],
696 'callback' => [ $this, 'get_uncategorised_faq' ],
697 'permission_callback' => function () {
698 return current_user_can( 'edit_others_posts' );
699 }
700 ]
701 );
702
703 register_rest_route(
704 $this->namespace,
705 '/faq/category_search',
706 [
707 'methods' => [ 'GET' ],
708 'callback' => [ $this, 'category_search' ],
709 'permission_callback' => function () {
710 return current_user_can( 'edit_others_posts' );
711 },
712 'args' => [
713 'title' => [
714 'type' => 'string',
715 'required' => true,
716 'sanitize_callback' => 'sanitize_text_field'
717 ],
718 'taxonomy' => [
719 'type' => 'string',
720 'required' => false,
721 'sanitize_callback' => 'sanitize_text_field'
722 ]
723 ]
724 ]
725 );
726 }
727
728 public function create_faq_sample( $params ) {
729 $sample_data = json_decode( $params->get_param( 'sample_data' ), true );
730 foreach ( $sample_data as $key => $value ) {
731 $insert_term = wp_insert_term(
732 $key,
733 'betterdocs_faq_category'
734 );
735 if ( $insert_term ) {
736 foreach ( $value['posts'] as $key => $value ) {
737 $this->insert_betterdocs_faq( $value['post_title'], $value['post_content'], $insert_term['term_id'] );
738 }
739 }
740 }
741 return true;
742 }
743
744 /**
745 * Resolve the taxonomy from a request, whitelisted to the General and
746 * Product FAQ category taxonomies. Defaults to the General taxonomy so
747 * existing callers (which never send a taxonomy) keep their behavior.
748 *
749 * @param \WP_REST_Request $params
750 * @return string
751 */
752 private function resolve_taxonomy( $params ) {
753 $taxonomy = $params->get_param( 'taxonomy' );
754 return in_array( $taxonomy, [ $this->category, $this->product_category ], true )
755 ? $taxonomy
756 : $this->category;
757 }
758
759 public function create_faq_category( $params ) {
760 // The WP term `name` column is varchar(200); cap the length so an
761 // over-long title can't trigger a DB insert error (the React form caps
762 // this too, this is the server-side guard).
763 $title = mb_substr( (string) $params->get_param( 'title' ), 0, 200 );
764 $description = $params->get_param( 'description' );
765 $description = ( $description !== 'undefined' ) ? $description : '';
766 $slug = $params->get_param( 'slug' );
767 $group_icon_url = $params->get_param('group_icon_url');
768 $taxonomy = $this->resolve_taxonomy( $params );
769 $result = $this->insert_betterdocs_faq_category(
770 $title,
771 $description,
772 $group_icon_url,
773 $slug,
774 $taxonomy,
775 (array) $params->get_param( 'product_cats' ),
776 (array) $params->get_param( 'products' )
777 );
778
779 if ( is_wp_error( $result ) ) {
780 return $result;
781 }
782
783 return rest_ensure_response( array( 'success' => true, 'term_id' => (int) $result ) );
784 }
785
786 public function update_faq_category( $params ) {
787 $term_id = $params->get_param( 'term_id' );
788 // Cap at the varchar(200) term-name limit (server-side guard).
789 $title = mb_substr( (string) $params->get_param( 'title' ), 0, 200 );
790 $description = $params->get_param( 'description' );
791 $group_icon_url = $params->get_param('group_icon_url');
792 $description = ( $description !== 'undefined' ) ? $description : '';
793 $slug = $params->get_param( 'slug' );
794 $taxonomy = $this->resolve_taxonomy( $params );
795 $update = wp_update_term(
796 $term_id,
797 $taxonomy,
798 [
799 'name' => $title,
800 'slug' => $slug,
801 'description' => $description
802 ]
803 );
804
805 if ( is_wp_error( $update ) ) {
806 return $update;
807 } else {
808 $term_id = isset( $update['term_id'] ) ? $update['term_id'] : 0;
809 if( $term_id != 0 ) {
810 $previous_icon_url = get_term_meta($term_id, 'faq_group_icon', true);
811 update_term_meta($term_id, 'faq_group_icon', $group_icon_url, $previous_icon_url);
812 $this->save_group_assignments(
813 $term_id,
814 $taxonomy,
815 (array) $params->get_param( 'product_cats' ),
816 (array) $params->get_param( 'products' )
817 );
818 }
819 return true;
820 }
821 }
822
823 public function delete_faq_category( $params ) {
824 $term_id = $params->get_param( 'term_id' );
825 $delete_all_docs = $params->get_param('with_all_post');
826 $taxonomy = $this->resolve_taxonomy( $params );
827
828 if ( $delete_all_docs ) {
829 Helper::delete_specific_faq_posts_by_faq_category( $term_id, $taxonomy );
830 }
831
832 $delete = wp_delete_term( $term_id, $taxonomy );
833
834 if ( is_wp_error( $delete ) ) {
835 return $delete;
836 } else {
837 return true;
838 }
839 }
840
841 public function insert_betterdocs_faq_category( $title, $description, $icon_url, $slug = '', $taxonomy = 'betterdocs_faq_category', $product_cats = [], $products = [] ) {
842 $insert_term = wp_insert_term(
843 $title,
844 $taxonomy,
845 [
846 'slug' => $slug,
847 'description' => $description
848 ]
849 );
850
851 if ( is_wp_error( $insert_term ) ) {
852 return $insert_term;
853 } else {
854 $term_id = isset( $insert_term['term_id'] ) ? $insert_term['term_id'] : 0;
855 if( $term_id != 0 ) {
856 update_term_meta($term_id, 'faq_group_icon', $icon_url);
857 $this->save_group_assignments( $term_id, $taxonomy, $product_cats, $products );
858 }
859 // Return the new term id so the admin can jump to its pagination
860 // page and highlight it (mirrors the new-FAQ reveal flow).
861 return (int) $term_id;
862 }
863 }
864
865 /**
866 * Persist a Product FAQ group's product-category and product assignments.
867 * No-op for any taxonomy other than the Product FAQ groups taxonomy.
868 *
869 * @param int $term_id
870 * @param string $taxonomy
871 * @param array $product_cats Incoming product_cat term IDs.
872 * @param array $products Incoming product post IDs.
873 */
874 private function save_group_assignments( $term_id, $taxonomy, $product_cats, $products ) {
875 if ( $taxonomy !== $this->product_category ) {
876 return;
877 }
878
879 $cat_ids = [];
880 foreach ( (array) $product_cats as $cid ) {
881 $cid = (int) $cid;
882 if ( $cid > 0 && term_exists( $cid, 'product_cat' ) ) {
883 $cat_ids[] = $cid;
884 }
885 }
886 $cat_ids = array_values( array_unique( $cat_ids ) );
887
888 $product_ids = [];
889 foreach ( (array) $products as $pid ) {
890 $pid = (int) $pid;
891 if ( $pid > 0 && 'product' === get_post_type( $pid ) ) {
892 $product_ids[] = $pid;
893 }
894 }
895 $product_ids = array_values( array_unique( $product_ids ) );
896
897 if ( empty( $cat_ids ) ) {
898 delete_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META );
899 } else {
900 update_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META, $cat_ids );
901 }
902
903 if ( empty( $product_ids ) ) {
904 delete_term_meta( $term_id, self::GROUP_PRODUCTS_META );
905 } else {
906 update_term_meta( $term_id, self::GROUP_PRODUCTS_META, $product_ids );
907 }
908
909 // Choosing explicit targets re-scopes a previously "all products" group
910 // (e.g. a store-aware sample group), so drop the match-all flag.
911 if ( ! empty( $cat_ids ) || ! empty( $product_ids ) ) {
912 delete_term_meta( $term_id, self::GROUP_ALL_PRODUCTS_META );
913 }
914 }
915
916 public function update_faq_category_order( $params ) {
917 $faq_category_order = $params->get_param( 'faq_category_order' );
918 $faq_category_order = json_decode( $faq_category_order, true );
919
920 foreach ( $faq_category_order as $order_data ) {
921 if ( (int) $order_data['current_position'] != (int) $order_data['updated_position'] ) {
922 update_term_meta( $order_data['id'], 'order', ( (int) $order_data['updated_position'] ) );
923 }
924 }
925 return true;
926 }
927
928 public function insert_betterdocs_faq( $post_title, $post_content, $term_id, $taxonomy = 'betterdocs_faq_category' ) {
929 $post = wp_insert_post(
930 [
931 'post_type' => 'betterdocs_faq',
932 'post_title' => wp_strip_all_tags( $post_title ),
933 'post_content' => $post_content,
934 'post_status' => 'publish'
935 ]
936 );
937
938 if ( $term_id ) {
939 $set_terms = wp_set_object_terms( $post, $term_id, $taxonomy );
940 if ( is_wp_error( $set_terms ) ) {
941 return $set_terms;
942 }
943 $this->update_faq_order_on_insert( $term_id, $post );
944 }
945
946 // Always return the new post ID so the admin can reveal/highlight the
947 // created FAQ (the grouped path previously returned the term-meta result).
948 return $post;
949 }
950
951 public function update_faq_order_on_insert( $term_id, $post ) {
952 // QA-008: read the single stored value and strip blanks so the very first
953 // insert (no existing meta) doesn't explode(',', null) into a corrupt [''].
954 $existing = (string) get_term_meta( $term_id, '_betterdocs_faq_order', true );
955 $term_meta_arr = array_filter( array_map( 'trim', explode( ',', $existing ) ), 'strlen' );
956 if ( ! in_array( (string) $post, $term_meta_arr, true ) ) {
957 array_unshift( $term_meta_arr, $post );
958 $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
959 return update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
960 }
961 }
962
963 /**
964 * Update _betterdocs_faq_order meta when new post created
965 */
966
967 public function update_faq_order_by_category( $params ) {
968 $term_id = $params->get_param( 'term_id' );
969 $posts = $params->get_param( 'posts' );
970 return update_term_meta( $term_id, '_betterdocs_faq_order', $posts );
971 }
972
973 /**
974 * Persist the FAQ Builder header order dropdown so the front end can mirror
975 * it. Stored as a single global preference in the `betterdocs_faq_order`
976 * option and read back by Query::get_faq_order_key().
977 */
978 public function update_faq_order_preference( $params ) {
979 $order = $params->get_param( 'order' );
980 $allowed = array( 'default', 'most_recent', 'least_recent', 'a_to_z', 'z_to_a', 'most_questions' );
981
982 if ( ! in_array( $order, $allowed, true ) ) {
983 $order = 'default';
984 }
985
986 update_option( 'betterdocs_faq_order', $order );
987
988 return rest_ensure_response( array( 'success' => true, 'order' => $order ) );
989 }
990
991 public function create_betterdocs_faq( $params ) {
992 $post_title = $params->get_param( 'post_title' );
993 $post_content = $params->get_param( 'post_content' );
994 $term_id = $params->get_param( 'term_id' );
995 $taxonomy = $this->resolve_taxonomy( $params );
996 return $this->insert_betterdocs_faq( $post_title, $post_content, $term_id, $taxonomy );
997 }
998
999 public function update_betterdocs_faq( $params ) {
1000 $post_id = (int) $params->get_param( 'post_id' );
1001
1002 // QA-001: never let an arbitrary post ID be retyped/overwritten through
1003 // this endpoint. Only operate on posts that are already BetterDocs FAQs.
1004 if ( ! $post_id || get_post_type( $post_id ) !== $this->post_type ) {
1005 return new WP_Error( 'betterdocs_invalid_faq', __( 'Invalid FAQ ID.', 'betterdocs' ), [ 'status' => 404 ] );
1006 }
1007
1008 $post_title = $params->get_param( 'post_title' );
1009 $post_content = $params->get_param( 'post_content' );
1010 $status = $params->get_param( 'status' );
1011 $term_id = (int) $params->get_param( 'term_id' );
1012 $taxonomy = $this->resolve_taxonomy( $params );
1013 if ( $status ) {
1014 $data = [
1015 'post_type' => 'betterdocs_faq',
1016 'ID' => $post_id,
1017 'status' => $status
1018 ];
1019 } else {
1020 $data = [
1021 'post_type' => 'betterdocs_faq',
1022 'ID' => $post_id,
1023 // QA-009: sanitize server-side — the editor posts raw TinyMCE
1024 // HTML. Title is plain text; content keeps post-safe HTML only
1025 // (scripts / event handlers stripped) even for unfiltered_html users.
1026 'post_title' => sanitize_text_field( $post_title ),
1027 'post_content' => wp_kses_post( $post_content ),
1028 ];
1029
1030 // QA-007: only assign a term that actually exists in the resolved FAQ
1031 // taxonomy — an invalid term_id would silently orphan the FAQ.
1032 if ( $term_id && term_exists( $term_id, $taxonomy ) ) {
1033 $data['tax_input'] = [
1034 $taxonomy => [ $term_id ],
1035 ];
1036
1037 // QA-008: build the order list defensively. On the first update the
1038 // term has no _betterdocs_faq_order meta; the old code did
1039 // explode(',', null) → [''] which corrupted the order with a blank
1040 // entry (plus a PHP 8.1 "null to explode" deprecation). Read the
1041 // single value and drop blanks instead.
1042 $existing = (string) get_term_meta( $term_id, '_betterdocs_faq_order', true );
1043 $term_meta_arr = array_filter( array_map( 'trim', explode( ',', $existing ) ), 'strlen' );
1044 if ( ! in_array( (string) $post_id, $term_meta_arr, true ) ) {
1045 array_unshift( $term_meta_arr, $post_id );
1046 $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
1047 update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
1048 }
1049 }
1050 }
1051
1052 return wp_update_post( $data );
1053 }
1054
1055 public function delete_betterdocs_faq( $params ) {
1056 $post_id = (int) $params->get_param( 'post_id' );
1057
1058 // QA-001: refuse to delete anything that isn't a BetterDocs FAQ.
1059 if ( ! $post_id || get_post_type( $post_id ) !== $this->post_type ) {
1060 return new WP_Error( 'betterdocs_invalid_faq', __( 'Invalid FAQ ID.', 'betterdocs' ), [ 'status' => 404 ] );
1061 }
1062
1063 return wp_delete_post( $post_id );
1064 }
1065
1066 public function faq_post_loop( $args ) {
1067 $posts = [];
1068 $query = new WP_Query( $args );
1069 if ( $query->have_posts() ) :
1070 while ( $query->have_posts() ) :
1071 $query->the_post();
1072 $posts[ get_the_ID() ]['title'] = get_the_title();
1073 $posts[ get_the_ID() ]['content'] = get_the_content();
1074 endwhile;
1075 endif;
1076
1077 return $posts;
1078 }
1079
1080 public function update_category_status( $params ) {
1081 $term_id = $params->get_param( 'term_id' );
1082 $status = $params->get_param( 'status' );
1083 return update_term_meta( $term_id, 'status', $status );
1084 }
1085
1086 public function fetch_faq_posts( $params ) {
1087 $faq = [];
1088 $type = $params->get_param( 'type' );
1089
1090 if ( $type == 'category' ) {
1091 $taxonomy_objects = get_terms(
1092 [
1093 'taxonomy' => 'betterdocs_faq_category',
1094 'hide_empty' => false
1095 ]
1096 );
1097
1098 if ( $taxonomy_objects && ! is_wp_error( $taxonomy_objects ) ) :
1099 foreach ( $taxonomy_objects as $term ) :
1100 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- core FAQ-by-category query; tax filtering is required functionality.
1101 $args = [
1102 'post_type' => 'betterdocs_faq',
1103 'post_status' => 'publish',
1104 'post_per_page' => -1,
1105 'tax_query' => [
1106 [
1107 'taxonomy' => 'betterdocs_faq_category',
1108 'field' => 'term_id',
1109 'terms' => $term->term_id
1110 ]
1111 ]
1112 ];
1113
1114 $posts = $this->faq_post_loop( $args );
1115
1116 $faq[ $term->slug ] = [
1117 (array) $term,
1118 'posts' => $posts
1119 ];
1120 endforeach;
1121 endif;
1122 } else {
1123 $args = [
1124 'post_type' => 'betterdocs_faq',
1125 'post_status' => 'publish',
1126 'post_per_page' => -1
1127 ];
1128 $posts = $this->faq_post_loop( $args );
1129 $faq['posts'] = $posts;
1130 }
1131
1132 return $faq;
1133 }
1134
1135 public function get_uncategorised_faq() {
1136 $available_terms = array_map(
1137 function ( $term ) {
1138 return $term->term_id;
1139 },
1140 get_terms(
1141 [
1142 'taxonomy' => $this->category,
1143 'hide_empty' => false
1144 ]
1145 )
1146 );
1147
1148 $tax_query = [
1149 'relation' => 'AND',
1150 [
1151 'taxonomy' => $this->category,
1152 'field' => 'term_id',
1153 'terms' => $available_terms,
1154 'operator' => 'NOT IN'
1155 ]
1156 ];
1157
1158 // Keep Product FAQ posts out of the General "Uncategorized" group: they
1159 // have no General category, so without this they would leak into the
1160 // General FAQ Builder tab.
1161 $product_terms = array_map(
1162 function ( $term ) {
1163 return $term->term_id;
1164 },
1165 get_terms(
1166 [
1167 'taxonomy' => $this->product_category,
1168 'hide_empty' => false
1169 ]
1170 )
1171 );
1172
1173 if ( ! empty( $product_terms ) ) {
1174 $tax_query[] = [
1175 'taxonomy' => $this->product_category,
1176 'field' => 'term_id',
1177 'terms' => $product_terms,
1178 'operator' => 'NOT IN'
1179 ];
1180 }
1181
1182 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- intentional NOT-IN scan to find FAQs without any category assignment.
1183 $posts = get_posts(
1184 [
1185 'post_type' => 'betterdocs_faq',
1186 'post_status' => current_user_can( 'edit_others_posts' ) ? [ 'publish', 'draft' ] : 'publish',
1187 'posts_per_page' => -1,
1188 'tax_query' => $tax_query
1189 ]
1190 );
1191
1192 // get_posts() returns raw WP_Post objects without a `meta` property. The
1193 // FAQ Builder reads `faq.meta.faq_open_by_default` to seed the "Keep It
1194 // Open By default" switcher, so attach it here to mirror the scalar shape
1195 // the wp/v2 endpoint returns for categorized FAQs ('' when unset, '1'
1196 // when enabled). Without this the value is undefined and the switcher
1197 // defaults to ON for every uncategorized FAQ.
1198 foreach ( $posts as $post ) {
1199 $post->meta = [
1200 'faq_open_by_default' => get_post_meta( $post->ID, 'faq_open_by_default', true ),
1201 ];
1202 }
1203
1204 return $posts;
1205 }
1206
1207 public function category_search( $request ) {
1208
1209 $title = $request['title'];
1210 $taxonomy = $this->resolve_taxonomy( $request );
1211
1212 // Perform the taxonomy search
1213 $taxonomy_args = [
1214 'name__like' => $title,
1215 'taxonomy' => $taxonomy,
1216 'hide_empty' => false
1217 ];
1218
1219 $taxonomies = get_terms( $taxonomy_args );
1220
1221 if ( ! empty( $taxonomies ) ) {
1222 $result = [];
1223 foreach ( $taxonomies as $taxonomy ) {
1224 $result[] = [
1225 'id' => $taxonomy->term_id,
1226 'count' => $taxonomy->count,
1227 'description' => $taxonomy->description,
1228 'name' => $taxonomy->name,
1229 'slug' => $taxonomy->slug
1230 // Add more fields as needed
1231 ];
1232 }
1233 // Return the taxonomy data
1234 return $result;
1235 } else {
1236 // Taxonomy not found
1237 return new WP_Error( 'taxonomy_not_found', 'Taxonomy not found.', [ 'status' => 404 ] );
1238 }
1239 }
1240
1241 public function faq_category_orderby_meta( $args, $request ) {
1242 if ( ! in_array( $args['taxonomy'], [ $this->category, $this->product_category ], true ) ) {
1243 return $args;
1244 }
1245
1246 // Order (and therefore paginate) the whole group list server-side to
1247 // match the FAQ Builder header dropdown, so page boundaries are cut on
1248 // the same sort the rows are shown in — otherwise page 1 could end on
1249 // "W" and page 2 start on "B". The admin passes the order key explicitly
1250 // (independent of when the preference option finishes saving); fall back
1251 // to the saved preference, then to the manual drag-drop order.
1252 $order_key = $request->get_param( 'betterdocs_faq_order' );
1253 if ( empty( $order_key ) ) {
1254 $order_key = get_option( 'betterdocs_faq_order', 'default' );
1255 }
1256
1257 switch ( $order_key ) {
1258 case 'most_recent':
1259 $args['orderby'] = 'term_id';
1260 $args['order'] = 'DESC';
1261 unset( $args['meta_key'] );
1262 break;
1263 case 'least_recent':
1264 $args['orderby'] = 'term_id';
1265 $args['order'] = 'ASC';
1266 unset( $args['meta_key'] );
1267 break;
1268 case 'a_to_z':
1269 $args['orderby'] = 'name';
1270 $args['order'] = 'ASC';
1271 unset( $args['meta_key'] );
1272 break;
1273 case 'z_to_a':
1274 $args['orderby'] = 'name';
1275 $args['order'] = 'DESC';
1276 unset( $args['meta_key'] );
1277 break;
1278 case 'most_questions':
1279 $args['orderby'] = 'count';
1280 $args['order'] = 'DESC';
1281 unset( $args['meta_key'] );
1282 break;
1283 case 'default':
1284 default:
1285 $args['orderby'] = 'meta_value_num';
1286 $args['meta_key'] = 'order';
1287 $args['order'] = 'ASC';
1288 break;
1289 }
1290
1291 return $args;
1292 }
1293 }
1294