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

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

951 lines 30.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Core;
4
5 use WPDeveloper\BetterDocs\Utils\Base;
6
7 class Request extends Base {
8 /**
9 * Flag for already parsed or not
10 *
11 * Specially needed for those who don't update pro yet.
12 * @var boolean
13 */
14 protected static $already_parsed = false;
15
16 /**
17 * List of BetterDocs Perma Structure
18 * @var array
19 */
20 private $perma_structure = [];
21
22 /**
23 * List of BetterDocs Query Vars Agains Page Structure.
24 * @var array
25 */
26 private $query_vars = [];
27
28 /**
29 * List of Query Variables from $wp->query_vars.
30 * @var array
31 */
32 private $wp_query_vars = [];
33
34 /**
35 * Rewrite Class Reference of BetterDocs
36 * @var Rewrite
37 */
38 protected $rewrite;
39
40 /**
41 * Settings Class Reference of BetterDocs
42 * @var Settings
43 */
44 protected $settings;
45
46 public function __construct( Rewrite $rewrite, Settings $settings ) {
47 $this->rewrite = $rewrite;
48 $this->settings = $settings;
49 }
50
51 public function init() {
52 if ( is_admin() ) {
53 return;
54 }
55
56 $this->perma_structure = [
57 'is_docs' => trim( $this->rewrite->get_base_slug(), '/' ),
58 'is_docs_feed' => trim( $this->rewrite->get_base_slug(), '/' ) . '/%feed%',
59 'is_docs_category' => trim( $this->settings->get( 'category_slug', 'docs-category' ), '/' ) . '/%doc_category%',
60 'is_docs_tag' => trim( $this->settings->get( 'tag_slug', 'docs-tag' ), '/' ) . '/%doc_tag%',
61 'is_single_docs' => trim( $this->settings->get( 'permalink_structure', 'docs' ), '/' ) . '/%name%',
62 'is_docs_author' => trim( $this->rewrite->get_base_slug(), '/' ) . '/authors/%author%'
63 ];
64
65 $this->query_vars = [
66 'is_docs' => ['post_type'],
67 'is_docs_feed' => ['doc_category'],
68 'is_docs_category' => ['doc_category'],
69 'is_docs_tag' => ['doc_tag'],
70 'is_single_docs' => ['name', 'docs', 'post_type'],
71 'is_docs_author' => ['post_type', 'author']
72 ];
73
74 add_action( 'parse_request', [ $this, 'parse' ] );
75
76 /**
77 * Hook into pre_get_posts to set up taxonomy queries for category archives
78 */
79 add_action( 'pre_get_posts', [ $this, 'setup_taxonomy_query' ], 1 );
80
81 /**
82 * Hook into template_redirect to re-apply taxonomy query flags
83 * This runs after pre_get_posts to ensure the flags stick
84 */
85 add_action( 'template_redirect', [ $this, 'reapply_taxonomy_flags' ], 1 );
86
87 /**
88 * Hook into status_header to prevent 404 for valid taxonomy archives
89 */
90 add_filter( 'status_header', [ $this, 'prevent_404_status' ], 10, 2 );
91
92 /**
93 * Hook into wp to ensure tax_query is always initialized
94 * This prevents null reference errors from WPML and other plugins
95 */
96 add_action( 'wp', [ $this, 'ensure_tax_query_initialized' ], 1 );
97
98 /**
99 * This is for Backward compatibility if pro not updated.
100 */
101 add_action( 'parse_request', [ $this, 'backward_compability' ], 11 );
102
103 /**
104 * Make Compatible With Permalink Manager Plugin
105 */
106 add_filter( 'permalink_manager_detected_element_id', [ $this, 'provide_compatibility' ], 10, 3 );
107
108 /**
109 * Hook into redirect_canonical to prevent redirects for invalid category-post combinations
110 */
111 add_filter( 'redirect_canonical', [ $this, 'prevent_canonical_redirect_for_invalid_docs' ], 10, 2 );
112
113 /**
114 * Hook into template_redirect to validate category-post relationships
115 * Priority 0 to run before WordPress canonical redirect (priority 10)
116 */
117 add_action( 'template_redirect', [ $this, 'validate_single_docs_category_redirect' ], 0 );
118 }
119
120 public function provide_compatibility( $element_id, $uri_parts, $request_url ) {
121 if ( $request_url == $this->settings->get( 'docs_slug' ) ) {
122 $element_id = '';
123 }
124 return $element_id;
125 }
126
127 /**
128 * Prevent canonical redirect for invalid docs category-post combinations
129 *
130 * @param string $redirect_url The redirect URL.
131 * @param string $requested_url The requested URL.
132 * @return string|false The redirect URL or false to prevent redirect.
133 */
134 public function prevent_canonical_redirect_for_invalid_docs( $redirect_url, $requested_url ) {
135 global $wp_query;
136
137 // Only validate if doc_category is in the URL (to prevent wrong category access)
138 if ( isset( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === 'docs' &&
139 isset( $wp_query->query_vars['doc_category'] ) && isset( $wp_query->query_vars['name'] ) ) {
140
141 $doc_category = $wp_query->query_vars['doc_category'];
142 $post_name = $wp_query->query_vars['name'];
143
144 // Get the post
145 $post = get_page_by_path( $post_name, OBJECT, 'docs' );
146
147 if ( ! $post ) {
148 return false; // Post doesn't exist, show 404
149 }
150
151 // Get post's categories
152 $post_categories = wp_get_post_terms( $post->ID, 'doc_category' );
153
154 if ( empty( $post_categories ) || is_wp_error( $post_categories ) ) {
155 // Post has no categories - only allow if URL is 'uncategorized'
156 if ( $doc_category !== 'uncategorized' ) {
157 return false;
158 }
159 } else {
160 // Post has categories - check if it belongs to the category in URL
161 $category_slugs = wp_list_pluck( $post_categories, 'slug' );
162
163 // Handle hierarchical categories: check if any part of the path matches
164 $category_parts = explode('/', trim($doc_category, '/'));
165 $found_match = false;
166
167 foreach ( $category_parts as $cat_slug ) {
168 if ( in_array( $cat_slug, $category_slugs ) ) {
169 $found_match = true;
170 break;
171 }
172 }
173
174 if ( ! $found_match ) {
175 return false; // Post doesn't belong to this category, show 404
176 }
177 }
178 }
179
180 return $redirect_url;
181 }
182
183 /**
184 * Validate single docs category relationship on template_redirect and force 404 if invalid
185 */
186 public function validate_single_docs_category_redirect() {
187 global $wp_query;
188
189 // Only validate if doc_category is in the URL
190 if ( isset( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === 'docs' &&
191 isset( $wp_query->query_vars['doc_category'] ) && isset( $wp_query->query_vars['name'] ) ) {
192
193 $doc_category = $wp_query->query_vars['doc_category'];
194 $post_name = $wp_query->query_vars['name'];
195
196 // Get the post
197 $post = get_page_by_path( $post_name, OBJECT, 'docs' );
198
199 if ( ! $post ) {
200 $wp_query->set_404();
201 status_header( 404 );
202 nocache_headers();
203 return;
204 }
205
206 // Get post's categories
207 $post_categories = wp_get_post_terms( $post->ID, 'doc_category' );
208
209 if ( empty( $post_categories ) || is_wp_error( $post_categories ) ) {
210 // Post has no categories - only allow if URL is 'uncategorized'
211 if ( $doc_category !== 'uncategorized' ) {
212 $wp_query->set_404();
213 status_header( 404 );
214 nocache_headers();
215 return;
216 }
217 } else {
218 // Post has categories - check if it belongs to the category in URL
219 $category_slugs = wp_list_pluck( $post_categories, 'slug' );
220
221 // Handle hierarchical categories: check if any part of the path matches
222 $category_parts = explode('/', trim($doc_category, '/'));
223 $found_match = false;
224
225 foreach ( $category_parts as $cat_slug ) {
226 if ( in_array( $cat_slug, $category_slugs ) ) {
227 $found_match = true;
228 break;
229 }
230 }
231
232 if ( ! $found_match ) {
233 $wp_query->set_404();
234 status_header( 404 );
235 nocache_headers();
236 return;
237 }
238 }
239 }
240 }
241
242 /**
243 * Set up taxonomy query for category archives
244 * This ensures WordPress recognizes requests with doc_category or knowledge_base as taxonomy archives
245 *
246 * @param \WP_Query $query The WordPress query object
247 */
248 public function setup_taxonomy_query( $query ) {
249 if ( is_admin() || ! $query->is_main_query() ) {
250 return;
251 }
252
253 // Check if this is a doc_category request
254 if ( isset( $query->query_vars['doc_category'] ) && ! empty( $query->query_vars['doc_category'] ) ) {
255 // If this is already identified as singular, don't override it
256 if ( $query->is_singular() || $query->is_singular ) {
257
258 // Ensure it's not marked as 404
259 $query->is_404 = false;
260 return;
261 }
262
263 // Check if we have a post ID set (p query var)
264 if ( isset( $query->query_vars['p'] ) && $query->query_vars['p'] > 0 ) {
265 // Security check: if this is a private post and user can't read private docs, show 404
266 $post = get_post( $query->query_vars['p'] );
267 if ( $post && $post->post_status === 'private' && ! current_user_can( 'read_private_docs' ) ) {
268
269 $query->is_404 = true;
270 $query->is_single = false;
271 $query->is_singular = false;
272 return;
273 }
274
275 // Explicitly set this as a single post, not an archive or 404
276 $query->is_single = true;
277 $query->is_singular = true;
278 $query->is_404 = false;
279 $query->is_archive = false;
280 $query->is_tax = false;
281 return;
282 }
283
284 // Check if we have 'docs' query var (alternative to 'name')
285 if ( isset( $query->query_vars['docs'] ) && ! empty( $query->query_vars['docs'] ) ) {
286 // Explicitly set this as a single post
287 $query->is_single = true;
288 $query->is_singular = true;
289 $query->is_404 = false;
290 $query->is_archive = false;
291 $query->is_tax = false;
292 return;
293 }
294
295 // If 'name' is set, check if a post with that name exists
296 // This prevents private docs from being incorrectly treated as category archives
297 if ( isset( $query->query_vars['name'] ) && ! empty( $query->query_vars['name'] ) ) {
298 $post_exists = get_page_by_path( $query->query_vars['name'], OBJECT, 'docs' );
299
300 if ( $post_exists ) {
301 // A post exists - this is a single doc request, not a category archive
302 // Don't set taxonomy flags
303 return;
304 }
305 }
306
307 // Only set taxonomy flags if none of the above conditions are met (pure category archive)
308 if ( ( ! isset( $query->query_vars['name'] ) || empty( $query->query_vars['name'] ) ) &&
309 ( ! isset( $query->query_vars['p'] ) || $query->query_vars['p'] <= 0 ) &&
310 ( ! isset( $query->query_vars['docs'] ) || empty( $query->query_vars['docs'] ) ) ) {
311
312 // Set this as a taxonomy query
313 $query->is_tax = true;
314 $query->is_archive = true;
315 $query->is_home = false;
316 $query->is_404 = false; // Important: reset 404 flag
317
318 // Set the queried object
319 $term = get_term_by( 'slug', $query->query_vars['doc_category'], 'doc_category' );
320 if ( $term ) {
321 $query->queried_object = $term;
322 $query->queried_object_id = $term->term_id;
323
324 // Set up tax_query using proper WP_Tax_Query class
325 if ( ! isset( $query->tax_query ) || ! is_a( $query->tax_query, 'WP_Tax_Query' ) ) {
326 $tax_query_args = [
327 [
328 'taxonomy' => 'doc_category',
329 'field' => 'slug',
330 'terms' => [ $term->slug ]
331 ]
332 ];
333 $query->tax_query = new \WP_Tax_Query( $tax_query_args );
334 $query->tax_query->queried_terms = [
335 'doc_category' => [
336 'terms' => [ $term->slug ],
337 'field' => 'slug'
338 ]
339 ];
340 }
341 }
342 } else {
343
344 }
345 }
346
347 // Check if this is a knowledge_base request
348 if ( isset( $query->query_vars['knowledge_base'] ) && ! empty( $query->query_vars['knowledge_base'] ) ) {
349 // If we also have doc_category, this is a knowledge_base_category archive
350 // Otherwise, it's just a knowledge_base archive
351 if ( ! isset( $query->query_vars['doc_category'] ) ) {
352 $query->is_tax = true;
353 $query->is_archive = true;
354 $query->is_home = false;
355 $query->is_404 = false; // Important: reset 404 flag
356
357 $term = get_term_by( 'slug', $query->query_vars['knowledge_base'], 'knowledge_base' );
358 if ( $term ) {
359 $query->queried_object = $term;
360 $query->queried_object_id = $term->term_id;
361
362 // Set up tax_query using proper WP_Tax_Query class
363 if ( ! isset( $query->tax_query ) || ! is_a( $query->tax_query, 'WP_Tax_Query' ) ) {
364 $tax_query_args = [
365 [
366 'taxonomy' => 'knowledge_base',
367 'field' => 'slug',
368 'terms' => [ $term->slug ]
369 ]
370 ];
371 $query->tax_query = new \WP_Tax_Query( $tax_query_args );
372 $query->tax_query->queried_terms = [
373 'knowledge_base' => [
374 'terms' => [ $term->slug ],
375 'field' => 'slug'
376 ]
377 ];
378 }
379 }
380 }
381 }
382
383 }
384
385 /**
386 * Re-apply taxonomy flags on template_redirect
387 * This ensures the flags stick even if WordPress or other plugins reset them
388 */
389 public function reapply_taxonomy_flags() {
390 global $wp_query;
391
392 // Check if we have doc_category or knowledge_base in query vars
393 if ( isset( $wp_query->query_vars['doc_category'] ) && ! empty( $wp_query->query_vars['doc_category'] ) ) {
394 // If this is already identified as singular, don't override it
395 if ( $wp_query->is_singular() || $wp_query->is_singular ) {
396
397 // Ensure it's not marked as 404
398 $wp_query->is_404 = false;
399 return;
400 }
401
402 // Check if we have a post ID set (p query var)
403 if ( isset( $wp_query->query_vars['p'] ) && $wp_query->query_vars['p'] > 0 ) {
404
405
406 // Security check: if this is a private post and user can't read private docs, show 404
407 $post = get_post( $wp_query->query_vars['p'] );
408 if ( $post && $post->post_status === 'private' && ! current_user_can( 'read_private_docs' ) ) {
409
410 $wp_query->is_404 = true;
411 $wp_query->is_single = false;
412 $wp_query->is_singular = false;
413 return;
414 }
415
416 // Explicitly set this as a single post, not an archive or 404
417 $wp_query->is_single = true;
418 $wp_query->is_singular = true;
419 $wp_query->is_404 = false;
420 $wp_query->is_archive = false;
421 $wp_query->is_tax = false;
422 return;
423 }
424
425 // Check if we have 'docs' query var (alternative to 'name')
426 if ( isset( $wp_query->query_vars['docs'] ) && ! empty( $wp_query->query_vars['docs'] ) ) {
427
428 // Explicitly set this as a single post
429 $wp_query->is_single = true;
430 $wp_query->is_singular = true;
431 $wp_query->is_404 = false;
432 $wp_query->is_archive = false;
433 $wp_query->is_tax = false;
434 return;
435 }
436
437 // If 'name' is set, check if a post with that name exists
438 // This prevents private docs from being incorrectly treated as category archives
439 if ( isset( $wp_query->query_vars['name'] ) && ! empty( $wp_query->query_vars['name'] ) ) {
440 $post_exists = get_page_by_path( $wp_query->query_vars['name'], OBJECT, 'docs' );
441
442 if ( $post_exists ) {
443 // A post exists - this is a single doc request, not a category archive
444 // Don't set taxonomy flags
445
446 return;
447 }
448 }
449
450 // Only set taxonomy flags if none of the above conditions are met (pure category archive)
451 if ( ( ! isset( $wp_query->query_vars['name'] ) || empty( $wp_query->query_vars['name'] ) ) &&
452 ( ! isset( $wp_query->query_vars['p'] ) || $wp_query->query_vars['p'] <= 0 ) &&
453 ( ! isset( $wp_query->query_vars['docs'] ) || empty( $wp_query->query_vars['docs'] ) ) ) {
454
455 // Re-apply the taxonomy flags
456 $wp_query->is_tax = true;
457 $wp_query->is_archive = true;
458 $wp_query->is_home = false;
459 $wp_query->is_404 = false;
460
461 // Ensure the queried object is set
462 if ( ! isset( $wp_query->queried_object ) || ! $wp_query->queried_object ) {
463 $term = get_term_by( 'slug', $wp_query->query_vars['doc_category'], 'doc_category' );
464 if ( $term ) {
465 $wp_query->queried_object = $term;
466 $wp_query->queried_object_id = $term->term_id;
467
468 // Set up tax_query using proper WP_Tax_Query class
469 if ( ! isset( $wp_query->tax_query ) || ! is_a( $wp_query->tax_query, 'WP_Tax_Query' ) ) {
470 $tax_query_args = [
471 [
472 'taxonomy' => 'doc_category',
473 'field' => 'slug',
474 'terms' => [ $term->slug ]
475 ]
476 ];
477 $wp_query->tax_query = new \WP_Tax_Query( $tax_query_args );
478 $wp_query->tax_query->queried_terms = [
479 'doc_category' => [
480 'terms' => [ $term->slug ],
481 'field' => 'slug'
482 ]
483 ];
484 }
485 }
486 }
487 } else {
488
489 }
490 }
491
492 }
493
494 /**
495 * Debug template redirect to see the query state
496 */
497 /**
498 * Prevent 404 status for valid taxonomy archives
499 *
500 * @param string $status_header The HTTP status header
501 * @param int $code The HTTP status code
502 * @return string The modified status header
503 */
504 public function prevent_404_status( $status_header, $code ) {
505 global $wp_query;
506
507 // If this is a 404 but we have doc_category or knowledge_base query vars, change it to 200
508 // We check the query vars instead of is_tax because the flags get reset by WordPress
509 if ( $code == 404 && (
510 (isset($wp_query->query_vars['doc_category']) && ! empty($wp_query->query_vars['doc_category'])) ||
511 (isset($wp_query->query_vars['doc_tag']) && ! empty($wp_query->query_vars['doc_tag'])) ||
512 (isset($wp_query->query_vars['knowledge_base']) && ! empty($wp_query->query_vars['knowledge_base']) && ! isset($wp_query->query_vars['name']))
513 ) ) {
514 return 'HTTP/1.1 200 OK';
515 }
516
517 return $status_header;
518 }
519
520 /**
521 * Ensure tax_query is always initialized as an object
522 * This prevents null reference errors from WPML and other plugins
523 * Only applies to BetterDocs post type and taxonomies
524 */
525 public function ensure_tax_query_initialized() {
526 global $wp_query;
527
528 // Only apply to BetterDocs-related queries
529 $is_betterdocs_query = false;
530
531 // Check if this is a docs post type query
532 if ( isset( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === 'docs' ) {
533 $is_betterdocs_query = true;
534 }
535
536 // Check if this is a BetterDocs taxonomy query
537 if ( isset( $wp_query->query_vars['doc_category'] ) && ! empty( $wp_query->query_vars['doc_category'] ) ) {
538 $is_betterdocs_query = true;
539 }
540
541 if ( isset( $wp_query->query_vars['doc_tag'] ) && ! empty( $wp_query->query_vars['doc_tag'] ) ) {
542 $is_betterdocs_query = true;
543 }
544
545 if ( isset( $wp_query->query_vars['knowledge_base'] ) && ! empty( $wp_query->query_vars['knowledge_base'] ) ) {
546 $is_betterdocs_query = true;
547 }
548
549 // Check if queried object is a BetterDocs taxonomy term
550 if ( isset( $wp_query->queried_object ) && isset( $wp_query->queried_object->taxonomy ) ) {
551 if ( in_array( $wp_query->queried_object->taxonomy, [ 'doc_category', 'doc_tag', 'knowledge_base' ] ) ) {
552 $is_betterdocs_query = true;
553 }
554 }
555
556 // Only proceed if this is a BetterDocs-related query
557 if ( ! $is_betterdocs_query ) {
558 return;
559 }
560
561 // Only initialize if it's not already a proper WP_Tax_Query instance
562 if ( ! isset( $wp_query->tax_query ) || ! is_a( $wp_query->tax_query, 'WP_Tax_Query' ) ) {
563 // Create a proper WP_Tax_Query instance with empty queries
564 $wp_query->tax_query = new \WP_Tax_Query( [] );
565 $wp_query->tax_query->queried_terms = [];
566 }
567
568 // For WPML compatibility: if queried_object is null, set it to an empty object
569 // but only if we're actually on a taxonomy page (is_tax is true)
570 if ( ! isset( $wp_query->queried_object ) && $wp_query->is_tax ) {
571 // Create a minimal WP_Term-like object to prevent errors
572 $wp_query->queried_object = new \stdClass();
573 $wp_query->queried_object->term_id = 0;
574 $wp_query->queried_object->name = '';
575 $wp_query->queried_object->slug = '';
576 $wp_query->queried_object->term_group = 0;
577 $wp_query->queried_object->term_taxonomy_id = 0;
578 $wp_query->queried_object->taxonomy = 'doc_category';
579 $wp_query->queried_object->description = '';
580 $wp_query->queried_object->parent = 0;
581 $wp_query->queried_object->count = 0;
582 $wp_query->queried_object->filter = 'raw';
583 }
584 }
585
586 protected function is_docs( &$query_vars ) {
587 if ( ! $this->settings->get( 'builtin_doc_page', true ) ) {
588 $query_vars['post_type'] = 'page';
589 $query_vars['name'] = trim( $this->rewrite->get_base_slug(), '/' );
590 }
591
592 return $query_vars;
593 }
594
595 public function is_docs_feed( $query_vars ) {
596 global $wp_rewrite;
597 return isset( $query_vars['feed'] ) && in_array( $query_vars['feed'], $wp_rewrite->feeds );
598 }
599
600 public function is_docs_author( $query_vars ) {
601 return isset( $query_vars['author'] ) ? true : false;
602 }
603
604 protected function is_single_docs( $query_vars ) {
605 // Check for both 'name' and 'docs' query variables
606 if ( ! isset( $query_vars['name'] ) && ! isset( $query_vars['docs'] ) ) {
607 return false;
608 }
609
610 global $wpdb;
611 $name = isset( $query_vars['docs'] ) ? $query_vars['docs'] : $query_vars['name'];
612
613 // If doc_category is specified in the URL, validate that the post belongs to that category
614 if ( isset( $query_vars['doc_category'] ) ) {
615 $doc_category = $query_vars['doc_category'];
616
617 // DEBUG: Log the query vars
618
619
620 // If knowledge_base is in query vars but doc_category is empty or just contains the KB slug,
621 // skip validation as the pro plugin will handle it
622 if ( isset( $query_vars['knowledge_base'] ) && empty( trim( $doc_category, '/' ) ) ) {
623
624 return true;
625 }
626
627 // Handle hierarchical category slugs (e.g., parent/child/grandchild)
628 $category_parts = explode('/', trim($doc_category, '/'));
629 $target_category_slug = end($category_parts); // Get the last part as the target category
630
631 // First, check if the post exists
632 $_post_id = (int) $wpdb->get_var(
633 $wpdb->prepare(
634 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s LIMIT 1",
635 esc_sql( $name ),
636 'docs'
637 )
638 );
639
640 // If post exists, validate it belongs to the category in the URL
641 if ( $_post_id > 0 ) {
642
643 // When hierarchical slugs are enabled, check if post belongs to any category in the path
644 $has_category = false;
645
646 if ( $this->settings->get( 'enable_category_hierarchy_slugs' ) && count($category_parts) > 1 ) {
647
648 // Check if post belongs to ANY category in the hierarchy path
649 // For example, if URL is "update/overview", check for both "update" and "overview"
650 $category_slugs_to_check = $category_parts;
651
652 foreach ( $category_slugs_to_check as $cat_slug ) {
653
654 $cat_check = $wpdb->get_var(
655 $wpdb->prepare(
656 "SELECT COUNT(*) FROM {$wpdb->term_relationships} tr
657 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
658 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
659 WHERE tr.object_id = %d AND t.slug = %s AND tt.taxonomy = %s",
660 $_post_id,
661 esc_sql( $cat_slug ),
662 'doc_category'
663 )
664 );
665
666
667
668 if ( $cat_check > 0 ) {
669 // If knowledge_base is set, verify the category belongs to that KB
670 if ( isset( $query_vars['knowledge_base'] ) ) {
671
672 // Get the term ID for this category slug
673 $term = get_term_by( 'slug', $cat_slug, 'doc_category' );
674 if ( $term ) {
675 $term_kbs = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true );
676
677 // Check if this category belongs to the KB in the URL
678 if ( is_array( $term_kbs ) && in_array( $query_vars['knowledge_base'], $term_kbs ) ) {
679
680 $has_category = true;
681 break;
682 }
683 }
684 } else {
685
686 // No KB in URL, so any category match is valid
687 $has_category = true;
688 break;
689 }
690 }
691 }
692 } else {
693 // Non-hierarchical or single category - check only the target category
694 $has_category = $wpdb->get_var(
695 $wpdb->prepare(
696 "SELECT COUNT(*) FROM {$wpdb->term_relationships} tr
697 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
698 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
699 WHERE tr.object_id = %d AND t.slug = %s AND tt.taxonomy = %s",
700 $_post_id,
701 esc_sql( $target_category_slug ),
702 'doc_category'
703 )
704 );
705
706 // If knowledge_base is set and category was found, verify it belongs to that KB
707 if ( $has_category && isset( $query_vars['knowledge_base'] ) ) {
708 $term = get_term_by( 'slug', $target_category_slug, 'doc_category' );
709 if ( $term ) {
710 $term_kbs = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true );
711 // Only valid if category belongs to the KB in the URL
712 if ( ! is_array( $term_kbs ) || ! in_array( $query_vars['knowledge_base'], $term_kbs ) ) {
713 $has_category = false;
714 }
715 }
716 }
717 }
718
719 // Special handling for uncategorized docs
720 if ( ! $has_category && $target_category_slug === 'uncategorized' ) {
721 // Check if the post has no categories assigned at all
722 $category_count = $wpdb->get_var(
723 $wpdb->prepare(
724 "SELECT COUNT(*) FROM {$wpdb->term_relationships} tr
725 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
726 WHERE tr.object_id = %d AND tt.taxonomy = %s",
727 $_post_id,
728 'doc_category'
729 )
730 );
731
732 // If post has no categories, allow it for uncategorized URL
733 if ( $category_count == 0 ) {
734 $has_category = true;
735 }
736 }
737
738 // If post doesn't belong to the target category, check if Multiple KB is active
739 // In MKB, the same category can exist in multiple KBs, and the doc might belong to it in one KB
740 if ( ! $has_category && taxonomy_exists( 'knowledge_base' ) ) {
741 // Check if this doc belongs to multiple knowledge bases
742 $kb_count = $wpdb->get_var(
743 $wpdb->prepare(
744 "SELECT COUNT(*) FROM {$wpdb->term_relationships} tr
745 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
746 WHERE tr.object_id = %d AND tt.taxonomy = %s",
747 $_post_id,
748 'knowledge_base'
749 )
750 );
751
752 // If doc has multiple KBs, be more lenient - the pro plugin will handle the final validation
753 // based on the knowledge_base query var that gets set later
754 if ( $kb_count > 1 ) {
755 // Allow it to pass - the pro plugin's parse() method will validate the KB context
756 $has_category = true;
757 }
758 }
759
760 // If post doesn't belong to the target category, return false
761 if ( ! $has_category ) {
762
763 return false;
764 }
765
766 // If hierarchical slugs are enabled and we found a post, validate the full hierarchy
767 if ( $this->settings->get( 'enable_category_hierarchy_slugs' ) && count($category_parts) > 1 ) {
768 // Get the post's category terms
769 $post_categories = wp_get_object_terms( $_post_id, 'doc_category' );
770
771 if ( ! empty( $post_categories ) ) {
772 $found_valid_hierarchy = false;
773
774 foreach ( $post_categories as $post_category ) {
775 // Build the hierarchy path for this category
776 $hierarchy_path = [];
777 $current_term = $post_category;
778
779 // Build path from child to parent
780 while ( $current_term ) {
781 array_unshift( $hierarchy_path, $current_term->slug );
782 $current_term = $current_term->parent ? get_term( $current_term->parent, 'doc_category' ) : null;
783 }
784
785 // Check if this hierarchy matches the URL structure
786 if ( implode('/', $hierarchy_path) === $doc_category ) {
787 $found_valid_hierarchy = true;
788 break;
789 }
790 }
791
792 // If no valid hierarchy found, return false (404)
793 if ( ! $found_valid_hierarchy ) {
794 return false;
795 }
796 }
797 }
798 }
799 } else {
800 // Fallback to original behavior if no category is specified
801 $_post_id = (int) $wpdb->get_var(
802 $wpdb->prepare(
803 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s LIMIT 1",
804 esc_sql( $name ),
805 'docs'
806 )
807 );
808 }
809
810 // If knowledge_base is set but we haven't validated yet (MKB active),
811 // allow it to pass - the pro plugin will handle final validation
812 if ( isset( $query_vars['knowledge_base'] ) && ! isset( $query_vars['doc_category'] ) ) {
813 return $_post_id > 0;
814 }
815
816 return $_post_id > 0;
817 }
818
819 protected function is_docs_category( $query_vars ) {
820 $result = $this->term_exists( $query_vars, 'doc_category' );
821 return $result;
822 }
823
824 protected function is_docs_tag( $query_vars ) {
825 return $this->term_exists( $query_vars, 'doc_tag' );
826 }
827
828 protected function term_exists( $query_vars, $taxonomy ) {
829 if ( ! isset( $query_vars[ $taxonomy ] ) ) {
830 return false;
831 }
832
833 return term_exists( $query_vars[ $taxonomy ], $taxonomy );
834 }
835
836 public function set_perma_structure( $structures = [] ) {
837 $this->perma_structure = array_merge( $this->perma_structure, $structures );
838 }
839
840 public function set_query_vars( $query_vars = [] ) {
841 $this->query_vars = array_merge( $this->query_vars, $query_vars );
842 }
843
844 public function backward_compability( $wp ) {
845 if ( static::$already_parsed ) {
846 return;
847 }
848
849 $this->permalink_magic( $wp );
850 }
851
852 public function parse( $wp ) {
853 static::$already_parsed = true;
854
855 $this->perma_structure = apply_filters('docs_rewrite_rules', $this->perma_structure);
856
857 $this->permalink_magic( $wp );
858 }
859
860 protected function permalink_magic( $wp ) {
861 $this->wp_query_vars = $wp->query_vars;
862
863 if ( ! empty( $this->perma_structure ) ) {
864 $_valid = [];
865
866 foreach ( $this->perma_structure as $_type => $structure ) {
867 $_perma_vars = $this->is_perma_valid_for( $structure, $wp->request );
868
869 // $_valid = empty( $_valid ) && $_perma_vars ? [ 'type' => $_type, 'query_vars' => $_perma_vars ] : $_valid;
870 if ( ( $_perma_vars && method_exists( $this, $_type ) && call_user_func_array( [$this, $_type], [ & $_perma_vars] ) ) ) {
871
872 // dump( $_type, $_perma_vars );
873 if ( $_type === 'is_single_docs' || $_type == 'is_docs_feed' || $_type == 'is_docs_author' ) {
874 $_perma_vars['post_type'] = 'docs';
875 }
876 $_valid = ['type' => $_type, 'query_vars' => $_perma_vars];
877 }
878 }
879
880 $type = isset( $_valid['type'] ) ? $_valid['type'] : '';
881 $query_vars = isset( $_valid['query_vars'] ) ? $_valid['query_vars'] : [];
882
883 if ( ! empty( $type ) ) {
884 unset( $this->query_vars[ $type ] );
885 array_map(
886 function ( $_vars ) use ( &$wp ) {
887 array_map(
888 function ( $_var ) use ( &$wp ) {
889 unset( $wp->query_vars[ $_var ] );
890 },
891 $_vars
892 );
893 },
894 $this->query_vars
895 );
896 }
897
898 $wp->query_vars = is_array( $query_vars ) ? array_merge( $wp->query_vars, $query_vars ) : $wp->query_vars;
899
900 // Fallback
901 if ( ! empty( $_valid ) ) {
902 unset( $wp->query_vars['attachment'] );
903 }
904 }
905 }
906
907 /**
908 * This method is responsible for checking a structure is valid again a request.
909 *
910 * @param string $structure
911 * @param string $request
912 * @return array|bool
913 */
914 private function is_perma_valid_for( $structure, $request ) {
915 if ( empty( $structure ) ) {
916 return false;
917 }
918
919 $_tags = explode( '/', trim( $structure, '/' ) );
920 $_replace_matched_tags = [];
921
922 $_replace_tags = array_filter(
923 $_tags,
924 function ( $item ) use ( &$_replace_matched_tags ) {
925 $_is_valid = strpos( $item, '%' ) !== false;
926 if ( $_is_valid ) {
927 $_replace_matched_tags[] = trim( $item, '%' );
928 }
929 return $_is_valid;
930 }
931 );
932
933 $_perma_structure = preg_quote( $structure, '/' );
934 $_perma_structure = str_replace( $_replace_tags, '([^\/]+)', $_perma_structure );
935
936 preg_match( "/^$_perma_structure$/", $request, $matches );
937
938 if ( empty( $matches ) || ! is_array( $matches ) ) {
939 return false;
940 }
941
942 if ( count( $matches ) === 1 ) {
943 return [ 'post_type' => 'docs' ];
944 }
945
946 unset( $matches[0] );
947
948 return array_combine( $_replace_matched_tags, $matches );
949 }
950 }
951