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
← All changes | includes/Core/FAQBuilder.php +237 -62 4.6.14.9.2 View file →
@@ -11,8 +11,9 @@
11 11 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
12 12
13 13 use WP_Error;
14 14 use WP_Query;
15 +use WP_REST_Request;
15 16 use WPDeveloper\BetterDocs\Utils\Base;
16 17 use WPDeveloper\BetterDocs\Utils\Helper;
17 18
18 19 class FAQBuilder extends Base {
@@ -33,8 +34,24 @@
33 34 */
34 35 public $product_category = 'betterdocs_product_faq_category';
35 36
36 37 /**
38 + * Post meta recording which FAQ Builder tab an FAQ belongs to: 'product' or
39 + * 'general'.
40 + *
41 + * A General and a Product FAQ are the same post type, distinguished only by which
42 + * taxonomy their group lives in — so an FAQ with NO group had no scope at all, and
43 + * an ungrouped Product FAQ (its group deleted, or created without one) was
44 + * indistinguishable from a General one. That's why it used to surface under the
45 + * General tab's "Uncategorized". Recording the scope on the post lets each tab own
46 + * its own Uncategorized bucket.
47 + */
48 + const SCOPE_META = '_betterdocs_faq_scope';
49 +
50 + /** Option flag for the one-time backfill of SCOPE_META on pre-existing FAQs. */
51 + const SCOPE_BACKFILL_OPTION = 'betterdocs_faq_scope_backfilled';
52 +
53 + /**
37 54 * Term meta on a Product FAQ group holding the product_cat term IDs the
38 55 * group is assigned to. Products in those categories inherit the group.
39 56 */
40 57 const GROUP_PRODUCT_CATS_META = '_betterdocs_faq_group_product_cats';
@@ -76,8 +93,16 @@
76 93 add_action( 'created_betterdocs_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
77 94 add_action( 'created_betterdocs_product_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
78 95 add_action( 'rest_api_init', [ $this, 'register_api_endpoint' ] );
79 96 add_action( 'rest_api_init', [ $this, 'register_category_count_fields' ] );
97 +
98 + // Keep each FAQ's scope ('general' | 'product') in sync however its group is
99 + // assigned — the Builder, the sample-FAQ generator, an import, or the classic
100 + // post editor all end up here — so an FAQ that later loses its group is still
101 + // known to belong to its own tab's "Uncategorized" bucket.
102 + add_action( 'set_object_terms', [ $this, 'sync_faq_scope_meta' ], 10, 4 );
103 + // One-time backfill for FAQs that predate the scope meta.
104 + add_action( 'admin_init', [ $this, 'maybe_backfill_faq_scope' ] );
80 105 add_action( 'rest_betterdocs_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
81 106 add_action( 'rest_betterdocs_product_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
82 107
83 108 // Classic FAQ Group list (edit-tags.php?taxonomy=betterdocs_faq_category):
@@ -467,9 +492,9 @@
467 492 if ( ! check_ajax_referer( 'faq_cat_order_nonce', 'nonce', false ) ) {
468 493 wp_send_json_error( __( 'Nonce Failed', 'betterdocs' ) );
469 494 }
470 495
471 - if ( ! current_user_can( 'edit_others_posts' ) ) {
496 + if ( ! $this->can_manage_faqs() ) {
472 497 wp_send_json_error( __( 'You don\'t have permission to manage FAQ groups.', 'betterdocs' ) );
473 498 }
474 499
475 500 $base_index = isset( $_POST['base_index'] ) ? intval( $_POST['base_index'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
@@ -542,8 +567,39 @@
542 567
543 568 return $pieces;
544 569 }
545 570
571 + /**
572 + * Whether the current user may manage FAQ groups and FAQs.
573 + *
574 + * Two capabilities, either of which is enough. Nothing that could reach
575 + * these routes before can be turned away now — the gate only widens.
576 + *
577 + * - `edit_others_posts` is the original gate, kept verbatim for backward
578 + * compatibility: every site that granted FAQ Builder access by granting a
579 + * core editor-level role keeps working exactly as it did.
580 + * - `edit_others_docs` is the BetterDocs-side answer, added in 4.9.0. The
581 + * `betterdocs_faq` post type is registered with
582 + * `capability_type => [ 'doc', 'docs' ]` and `map_meta_cap => true`, so
583 + * WordPress already governs individual FAQ posts with the docs capability
584 + * family — the routes were the one place that asked for a *core* post
585 + * capability instead. A documentation-only role (BetterDocs' own `editor`
586 + * bucket, or anything Pro's `article_roles` grants) could edit every FAQ
587 + * post through `wp/v2/betterdocs_faq` and still get a 403 from the FAQ
588 + * Builder's own API.
589 + *
590 + * It is also what lines the REST routes up with the MCP FAQ abilities, which
591 + * are gated on `edit_others_docs`: an agent that may create an FAQ through
592 + * `bd-create-faq` reaches these same routes underneath.
593 + *
594 + * @since 4.9.0
595 + *
596 + * @return bool
597 + */
598 + private function can_manage_faqs() {
599 + return current_user_can( 'edit_others_posts' ) || current_user_can( 'edit_others_docs' );
600 + }
601 +
546 602 public function register_api_endpoint() {
547 603 register_rest_route(
548 604 $this->namespace,
549 605 '/faq/sample_data',
@@ -550,9 +606,9 @@
550 606 [
551 607 'methods' => [ 'POST' ],
552 608 'callback' => [ $this, 'create_faq_sample' ],
553 609 'permission_callback' => function () {
554 - return current_user_can( 'edit_others_posts' );
610 + return $this->can_manage_faqs();
555 611 }
556 612 ]
557 613 );
558 614
@@ -562,9 +618,9 @@
562 618 [
563 619 'methods' => [ 'GET' ],
564 620 'callback' => [ $this, 'fetch_faq_posts' ],
565 621 'permission_callback' => function () {
566 - return current_user_can( 'edit_others_posts' );
622 + return $this->can_manage_faqs();
567 623 }
568 624 ]
569 625 );
570 626
@@ -574,9 +630,9 @@
574 630 [
575 631 'methods' => [ 'POST' ],
576 632 'callback' => [ $this, 'create_faq_category' ],
577 633 'permission_callback' => function () {
578 - return current_user_can( 'edit_others_posts' );
634 + return $this->can_manage_faqs();
579 635 }
580 636 ]
581 637 );
582 638
@@ -586,9 +642,9 @@
586 642 [
587 643 'methods' => [ 'POST' ],
588 644 'callback' => [ $this, 'update_faq_category' ],
589 645 'permission_callback' => function () {
590 - return current_user_can( 'edit_others_posts' );
646 + return $this->can_manage_faqs();
591 647 }
592 648 ]
593 649 );
594 650
@@ -598,9 +654,9 @@
598 654 [
599 655 'methods' => [ 'POST' ],
600 656 'callback' => [ $this, 'delete_faq_category' ],
601 657 'permission_callback' => function () {
602 - return current_user_can( 'edit_others_posts' );
658 + return $this->can_manage_faqs();
603 659 }
604 660 ]
605 661 );
606 662
@@ -610,9 +666,9 @@
610 666 [
611 667 'methods' => [ 'POST' ],
612 668 'callback' => [ $this, 'create_betterdocs_faq' ],
613 669 'permission_callback' => function () {
614 - return current_user_can( 'edit_others_posts' );
670 + return $this->can_manage_faqs();
615 671 }
616 672 ]
617 673 );
618 674
@@ -622,9 +678,9 @@
622 678 [
623 679 'methods' => [ 'POST' ],
624 680 'callback' => [ $this, 'update_betterdocs_faq' ],
625 681 'permission_callback' => function () {
626 - return current_user_can( 'edit_others_posts' );
682 + return $this->can_manage_faqs();
627 683 }
628 684 ]
629 685 );
630 686
@@ -634,9 +690,9 @@
634 690 [
635 691 'methods' => [ 'POST' ],
636 692 'callback' => [ $this, 'delete_betterdocs_faq' ],
637 693 'permission_callback' => function () {
638 - return current_user_can( 'edit_others_posts' );
694 + return $this->can_manage_faqs();
639 695 }
640 696 ]
641 697 );
642 698
@@ -646,9 +702,9 @@
646 702 [
647 703 'methods' => [ 'POST' ],
648 704 'callback' => [ $this, 'update_category_status' ],
649 705 'permission_callback' => function () {
650 - return current_user_can( 'edit_others_posts' );
706 + return $this->can_manage_faqs();
651 707 }
652 708 ]
653 709 );
654 710
@@ -658,9 +714,9 @@
658 714 [
659 715 'methods' => [ 'POST' ],
660 716 'callback' => [ $this, 'update_faq_category_order' ],
661 717 'permission_callback' => function () {
662 - return current_user_can( 'edit_others_posts' );
718 + return $this->can_manage_faqs();
663 719 }
664 720 ]
665 721 );
666 722
@@ -670,9 +726,9 @@
670 726 [
671 727 'methods' => [ 'POST' ],
672 728 'callback' => [ $this, 'update_faq_order_by_category' ],
673 729 'permission_callback' => function () {
674 - return current_user_can( 'edit_others_posts' );
730 + return $this->can_manage_faqs();
675 731 }
676 732 ]
677 733 );
678 734
@@ -682,9 +738,9 @@
682 738 [
683 739 'methods' => [ 'POST' ],
684 740 'callback' => [ $this, 'update_faq_order_preference' ],
685 741 'permission_callback' => function () {
686 - return current_user_can( 'edit_others_posts' );
742 + return $this->can_manage_faqs();
687 743 }
688 744 ]
689 745 );
690 746
@@ -694,9 +750,9 @@
694 750 [
695 751 'methods' => [ 'GET' ],
696 752 'callback' => [ $this, 'get_uncategorised_faq' ],
697 753 'permission_callback' => function () {
698 - return current_user_can( 'edit_others_posts' );
754 + return $this->can_manage_faqs();
699 755 }
700 756 ]
701 757 );
702 758
@@ -706,9 +762,9 @@
706 762 [
707 763 'methods' => [ 'GET' ],
708 764 'callback' => [ $this, 'category_search' ],
709 765 'permission_callback' => function () {
710 - return current_user_can( 'edit_others_posts' );
766 + return $this->can_manage_faqs();
711 767 },
712 768 'args' => [
713 769 'title' => [
714 770 'type' => 'string',
@@ -772,9 +828,10 @@
772 828 $group_icon_url,
773 829 $slug,
774 830 $taxonomy,
775 831 (array) $params->get_param( 'product_cats' ),
776 - (array) $params->get_param( 'products' )
832 + (array) $params->get_param( 'products' ),
833 + $this->all_products_param( $params )
777 834 );
778 835
779 836 if ( is_wp_error( $result ) ) {
780 837 return $result;
@@ -812,9 +869,10 @@
812 869 $this->save_group_assignments(
813 870 $term_id,
814 871 $taxonomy,
815 872 (array) $params->get_param( 'product_cats' ),
816 - (array) $params->get_param( 'products' )
873 + (array) $params->get_param( 'products' ),
874 + $this->all_products_param( $params )
817 875 );
818 876 }
819 877 return true;
820 878 }
@@ -837,9 +895,9 @@
837 895 return true;
838 896 }
839 897 }
840 898
841 - public function insert_betterdocs_faq_category( $title, $description, $icon_url, $slug = '', $taxonomy = 'betterdocs_faq_category', $product_cats = [], $products = [] ) {
899 + public function insert_betterdocs_faq_category( $title, $description, $icon_url, $slug = '', $taxonomy = 'betterdocs_faq_category', $product_cats = [], $products = [], $all_products = false ) {
842 900 $insert_term = wp_insert_term(
843 901 $title,
844 902 $taxonomy,
845 903 [
@@ -853,9 +911,9 @@
853 911 } else {
854 912 $term_id = isset( $insert_term['term_id'] ) ? $insert_term['term_id'] : 0;
855 913 if( $term_id != 0 ) {
856 914 update_term_meta($term_id, 'faq_group_icon', $icon_url);
857 - $this->save_group_assignments( $term_id, $taxonomy, $product_cats, $products );
915 + $this->save_group_assignments( $term_id, $taxonomy, $product_cats, $products, $all_products );
858 916 }
859 917 // Return the new term id so the admin can jump to its pagination
860 918 // page and highlight it (mirrors the new-FAQ reveal flow).
861 919 return (int) $term_id;
@@ -862,9 +920,13 @@
862 920 }
863 921 }
864 922
865 923 /**
866 - * Persist a Product FAQ group's product-category and product assignments.
924 + * Persist a Product FAQ group's targeting. Three mutually-exclusive scopes:
925 + * - all products → GROUP_ALL_PRODUCTS_META, shown on every product page;
926 + * - specific cats/products → GROUP_PRODUCT_CATS_META / GROUP_PRODUCTS_META;
927 + * - none → the group is hidden on the storefront.
928 + *
867 929 * No-op for any taxonomy other than the Product FAQ groups taxonomy.
868 930 *
869 931 * @param int $term_id
870 932 * @param string $taxonomy
@@ -869,14 +931,26 @@
869 931 * @param int $term_id
870 932 * @param string $taxonomy
871 933 * @param array $product_cats Incoming product_cat term IDs.
872 934 * @param array $products Incoming product post IDs.
935 + * @param bool $all_products When true, show on ALL products and clear the cat/product
936 + * targets (the scopes are mutually exclusive).
873 937 */
874 - private function save_group_assignments( $term_id, $taxonomy, $product_cats, $products ) {
938 + private function save_group_assignments( $term_id, $taxonomy, $product_cats, $products, $all_products = false ) {
875 939 if ( $taxonomy !== $this->product_category ) {
876 940 return;
877 941 }
878 942
943 + // "Show on all products" wins and clears the specific targets, so a store-wide
944 + // group (e.g. the generated "Shipping, Returns & Payments") can be switched to
945 + // specific categories/products and back, from the same modal.
946 + if ( $all_products ) {
947 + update_term_meta( $term_id, self::GROUP_ALL_PRODUCTS_META, true );
948 + delete_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META );
949 + delete_term_meta( $term_id, self::GROUP_PRODUCTS_META );
950 + return;
951 + }
952 +
879 953 $cat_ids = [];
880 954 foreach ( (array) $product_cats as $cid ) {
881 955 $cid = (int) $cid;
882 956 if ( $cid > 0 && term_exists( $cid, 'product_cat' ) ) {
@@ -905,15 +979,21 @@
905 979 } else {
906 980 update_term_meta( $term_id, self::GROUP_PRODUCTS_META, $product_ids );
907 981 }
908 982
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 - }
983 + // Explicitly not "all products" → drop the match-all flag.
984 + delete_term_meta( $term_id, self::GROUP_ALL_PRODUCTS_META );
914 985 }
915 986
987 + /**
988 + * Read + normalize the `all_products` REST param (accepts '1'/'0'/true/false).
989 + *
990 + * @return bool
991 + */
992 + private function all_products_param( $params ) {
993 + return filter_var( $params->get_param( 'all_products' ), FILTER_VALIDATE_BOOLEAN );
994 + }
995 +
916 996 public function update_faq_category_order( $params ) {
917 997 $faq_category_order = $params->get_param( 'faq_category_order' );
918 998 $faq_category_order = json_decode( $faq_category_order, true );
919 999
@@ -934,8 +1014,20 @@
934 1014 'post_status' => 'publish'
935 1015 ]
936 1016 );
937 1017
1018 + // Stamp the scope from the tab this FAQ was created in. Doing it here (and not
1019 + // only in sync_faq_scope_meta) is what makes an FAQ created WITHOUT a group land
1020 + // in the right tab's "Uncategorized" — with no terms assigned, set_object_terms
1021 + // never fires.
1022 + if ( $post && ! is_wp_error( $post ) ) {
1023 + update_post_meta(
1024 + $post,
1025 + self::SCOPE_META,
1026 + $taxonomy === $this->product_category ? 'product' : 'general'
1027 + );
1028 + }
1029 +
938 1030 if ( $term_id ) {
939 1031 $set_terms = wp_set_object_terms( $post, $term_id, $taxonomy );
940 1032 if ( is_wp_error( $set_terms ) ) {
941 1033 return $set_terms;
@@ -1131,62 +1223,145 @@
1131 1223
1132 1224 return $faq;
1133 1225 }
1134 1226
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 - )
1227 + /**
1228 + * Record an FAQ's scope whenever its group is (re)assigned, so the scope survives
1229 + * the group being deleted. Fires for every path that assigns terms.
1230 + *
1231 + * @param int $object_id Post ID.
1232 + * @param array $terms Terms assigned (unused).
1233 + * @param array $tt_ids Term-taxonomy IDs.
1234 + * @param string $taxonomy Taxonomy the terms belong to.
1235 + */
1236 + public function sync_faq_scope_meta( $object_id, $terms, $tt_ids, $taxonomy ) {
1237 + if ( get_post_type( $object_id ) !== $this->post_type ) {
1238 + return;
1239 + }
1240 +
1241 + if ( $taxonomy === $this->product_category && ! empty( $tt_ids ) ) {
1242 + update_post_meta( $object_id, self::SCOPE_META, 'product' );
1243 + return;
1244 + }
1245 +
1246 + // Assigning a General group makes it a General FAQ — but only claim it if it
1247 + // isn't already a Product FAQ that merely also carries a General term.
1248 + if ( $taxonomy === $this->category && ! empty( $tt_ids ) ) {
1249 + if ( get_post_meta( $object_id, self::SCOPE_META, true ) !== 'product' ) {
1250 + update_post_meta( $object_id, self::SCOPE_META, 'general' );
1251 + }
1252 + }
1253 + }
1254 +
1255 + /**
1256 + * One-time backfill: stamp the scope onto FAQs created before SCOPE_META existed.
1257 + * An FAQ holding a Product group is 'product'; everything else is 'general'.
1258 + * Ungrouped legacy FAQs have no way to be identified as product ones, so they stay
1259 + * in the General tab, which is exactly where they are today.
1260 + */
1261 + public function maybe_backfill_faq_scope() {
1262 + if ( get_option( self::SCOPE_BACKFILL_OPTION ) ) {
1263 + return;
1264 + }
1265 +
1266 + $product_faqs = get_posts(
1267 + [
1268 + 'post_type' => $this->post_type,
1269 + 'post_status' => 'any',
1270 + 'posts_per_page' => -1,
1271 + 'fields' => 'ids',
1272 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- one-time backfill.
1273 + 'tax_query' => [
1274 + [
1275 + 'taxonomy' => $this->product_category,
1276 + 'operator' => 'EXISTS',
1277 + ],
1278 + ],
1279 + ]
1146 1280 );
1147 1281
1282 + foreach ( $product_faqs as $faq_id ) {
1283 + update_post_meta( $faq_id, self::SCOPE_META, 'product' );
1284 + }
1285 +
1286 + update_option( self::SCOPE_BACKFILL_OPTION, 1 );
1287 + }
1288 +
1289 + /**
1290 + * FAQs with no group, for the tab that asked. Each tab owns its own bucket:
1291 + * the Product tab lists ungrouped FAQs scoped to 'product', the General tab lists
1292 + * everything else that is ungrouped (so an ungrouped Product FAQ no longer leaks
1293 + * into General).
1294 + */
1295 + public function get_uncategorised_faq( $request = null ) {
1296 + $taxonomy = $request instanceof WP_REST_Request ? $this->resolve_taxonomy( $request ) : $this->category;
1297 + $is_product = ( $taxonomy === $this->product_category );
1298 +
1299 + $term_ids = function ( $tax ) {
1300 + $terms = get_terms( [ 'taxonomy' => $tax, 'hide_empty' => false ] );
1301 + return is_wp_error( $terms ) ? [] : array_map(
1302 + function ( $term ) {
1303 + return $term->term_id;
1304 + },
1305 + $terms
1306 + );
1307 + };
1308 +
1309 + // "Ungrouped" always means: no group in THIS tab's taxonomy.
1148 1310 $tax_query = [
1149 1311 'relation' => 'AND',
1150 1312 [
1151 - 'taxonomy' => $this->category,
1313 + 'taxonomy' => $taxonomy,
1152 1314 'field' => 'term_id',
1153 - 'terms' => $available_terms,
1315 + 'terms' => $term_ids( $taxonomy ),
1154 1316 'operator' => 'NOT IN'
1155 1317 ]
1156 1318 ];
1157 1319
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(
1320 + $meta_query = [];
1321 +
1322 + if ( $is_product ) {
1323 + // Product tab: only FAQs that belong to the Product side.
1324 + $meta_query[] = [
1325 + 'key' => self::SCOPE_META,
1326 + 'value' => 'product',
1327 + ];
1328 + } else {
1329 + // General tab: exclude anything scoped to Product…
1330 + $meta_query[] = [
1331 + 'relation' => 'OR',
1166 1332 [
1167 - 'taxonomy' => $this->product_category,
1168 - 'hide_empty' => false
1169 - ]
1170 - )
1171 - );
1333 + 'key' => self::SCOPE_META,
1334 + 'compare' => 'NOT EXISTS',
1335 + ],
1336 + [
1337 + 'key' => self::SCOPE_META,
1338 + 'value' => 'product',
1339 + 'compare' => '!=',
1340 + ],
1341 + ];
1172 1342
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 - ];
1343 + // …and, as before, anything still holding a Product group (belt and braces
1344 + // for FAQs whose scope meta never got written).
1345 + $product_terms = $term_ids( $this->product_category );
1346 + if ( ! empty( $product_terms ) ) {
1347 + $tax_query[] = [
1348 + 'taxonomy' => $this->product_category,
1349 + 'field' => 'term_id',
1350 + 'terms' => $product_terms,
1351 + 'operator' => 'NOT IN'
1352 + ];
1353 + }
1180 1354 }
1181 1355
1182 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- intentional NOT-IN scan to find FAQs without any category assignment.
1356 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query, WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- intentional NOT-IN scan to find FAQs without any group.
1183 1357 $posts = get_posts(
1184 1358 [
1185 1359 'post_type' => 'betterdocs_faq',
1186 - 'post_status' => current_user_can( 'edit_others_posts' ) ? [ 'publish', 'draft' ] : 'publish',
1360 + 'post_status' => $this->can_manage_faqs() ? [ 'publish', 'draft' ] : 'publish',
1187 1361 'posts_per_page' => -1,
1188 - 'tax_query' => $tax_query
1362 + 'tax_query' => $tax_query,
1363 + 'meta_query' => $meta_query,
1189 1364 ]
1190 1365 );
1191 1366
1192 1367 // get_posts() returns raw WP_Post objects without a `meta` property. The