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/REST/DocCategories.php +58 -9 4.4.04.9.2 View file →
@@ -1,10 +1,15 @@
1 1 <?php
2 -
2 +// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- core docs taxonomy REST endpoints; meta filtering required.
3 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- endpoint exposes user-driven exclusion.
4 +// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table identifiers (WP-provided) and dynamic %s placeholders are intentional.
5 +// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- the uncategorized-docs scan needs raw SQL because WP_Query has no NOT-IN-via-subquery primitive.
6 +// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- list is rebuilt per-request from live post/term state; cache would mask uncategorized status changes.
3 7 namespace WPDeveloper\BetterDocs\REST;
4 8
5 9 use stdClass;
6 10 use WPDeveloper\BetterDocs\Core\BaseAPI;
11 +use WPDeveloper\BetterDocs\Utils\Helper;
7 12
8 13 class DocCategories extends BaseAPI {
9 14 public function permission_check(): bool {
10 15 return true;
@@ -13,9 +18,9 @@
13 18
14 19 public function register() {
15 20 $this->get( 'doc-categories', array( $this, 'get_response' ), array(
16 21 'password' => array(
17 - 'description' => __( 'The password for password-protected docs.' ),
22 + 'description' => __( 'The password for password-protected docs.', 'betterdocs' ),
18 23 'type' => 'string'
19 24 )
20 25 ) );
21 26 $this->get( 'doc-categories-kb', array( $this, 'doc_categories_kb_response' ) );
@@ -58,8 +63,39 @@
58 63
59 64 return $terms;
60 65 }
61 66
67 + /**
68 + * Reorder a list of doc rows according to a saved id sequence.
69 + * Ids in $saved_order keep that order; ids not present are appended.
70 + *
71 + * @param array $docs Doc data rows (each has an 'id' key).
72 + * @param array $saved_order Ordered list of post ids from `_docs_order_<lang>`.
73 + * @return array
74 + */
75 + private function sort_by_saved_order( $docs, $saved_order ) {
76 + if ( empty( $docs ) || empty( $saved_order ) ) {
77 + return $docs;
78 + }
79 +
80 + $saved_order = array_map( 'intval', (array) $saved_order );
81 + $position = array_flip( $saved_order );
82 + $tail_index = count( $saved_order );
83 +
84 + $sorted = $docs;
85 + usort( $sorted, function ( $a, $b ) use ( $position, &$tail_index ) {
86 + $a_id = isset( $a['id'] ) ? (int) $a['id'] : 0;
87 + $b_id = isset( $b['id'] ) ? (int) $b['id'] : 0;
88 +
89 + $a_pos = isset( $position[ $a_id ] ) ? $position[ $a_id ] : PHP_INT_MAX;
90 + $b_pos = isset( $position[ $b_id ] ) ? $position[ $b_id ] : PHP_INT_MAX;
91 +
92 + return $a_pos <=> $b_pos;
93 + } );
94 +
95 + return $sorted;
96 + }
97 +
62 98 private function convert_terms_to_array_of_std_objects( $payload ) {
63 99 $terms = array();
64 100
65 101 foreach ( $payload as $term ) {
@@ -158,9 +194,9 @@
158 194 $posts = betterdocs()->query->get_posts( $query_args, true );
159 195 $response[ $term->term_id ] = array();
160 196
161 197 if ( ! $posts->have_posts() ) {
162 - wp_reset_query();
198 + wp_reset_postdata();
163 199 }
164 200 while ( $posts->have_posts() ):
165 201 $posts->the_post();
166 202 $post_obj = get_post( get_the_ID() );
@@ -177,22 +213,35 @@
177 213 array_push( $response[ $term->term_id ], $data );
178 214 endwhile;
179 215
180 216 wp_reset_postdata();
181 - wp_reset_query();
217 + wp_reset_query(); // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- explicit global WP_Query reset after a custom loop; wp_reset_postdata() above only restores post data.
218 +
219 + // WP_Query's `orderby=post__in` is stripped by some plugins/filters
220 + // (notably WPML on REST requests), so apply the saved order in PHP
221 + // here as the source of truth. Posts not in the saved order are
222 + // appended at the end in their existing query order.
223 + $response[ $term->term_id ] = $this->sort_by_saved_order(
224 + $response[ $term->term_id ],
225 + betterdocs()->query->get_docs_order_by_terms( $term->term_id )
226 + );
182 227 }
183 228
184 229 /**
185 230 * Uncategories Docs
186 231 */
187 - // Build secure query for uncategorized docs with proper post status filtering
232 + // Build secure query for uncategorized docs with proper post status filtering.
233 + // $wpdb->posts / $wpdb->term_relationships / $wpdb->term_taxonomy are WP-provided
234 + // table identifiers (safe to interpolate). Dynamic %s placeholder count is built
235 + // from a fixed-shape $post_status array.
188 236 $post_status_placeholders = implode( ',', array_fill( 0, count( $post_status ), '%s' ) );
189 - $_post__not_in_query = $wpdb->prepare(
237 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- WP table identifiers; placeholders dynamically generated to match $post_status size.
238 + $_post__not_in_query = $wpdb->prepare(
190 239 "SELECT ID as post_id from $wpdb->posts WHERE post_type = %s AND post_status IN ($post_status_placeholders) AND post_status != 'trash' AND post_status != 'auto-draft' AND ID NOT IN ( SELECT object_id as post_id FROM $wpdb->term_relationships WHERE term_taxonomy_id IN ( SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s ) )",
191 240 array_merge( array( 'docs' ), $post_status, array( 'doc_category' ) )
192 241 );
193 242
194 - $_post__not_in = $wpdb->get_col( $_post__not_in_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
243 + $_post__not_in = $wpdb->get_col( $_post__not_in_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is prepared above.
195 244
196 245 if ( ! empty( $_post__not_in ) ) {
197 246 $uncategorized_docs = array();
198 247 $uncategorized_query_args = array(
@@ -208,9 +257,9 @@
208 257
209 258 $_uncategorized_docs_query = new \WP_Query( $uncategorized_query_args );
210 259
211 260 if ( ! $_uncategorized_docs_query->have_posts() ) {
212 - wp_reset_query();
261 + wp_reset_postdata();
213 262 }
214 263 while ( $_uncategorized_docs_query->have_posts() ):
215 264 $_uncategorized_docs_query->the_post();
216 265 $post_obj = get_post( get_the_ID() );
@@ -227,9 +276,9 @@
227 276 array_push( $uncategorized_docs, $data );
228 277 endwhile;
229 278
230 279 wp_reset_postdata();
231 - wp_reset_query();
280 + wp_reset_postdata();
232 281
233 282 $response[ 'uncategorized' ] = $uncategorized_docs;
234 283 }
235 284