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

487 lines 15.1 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 * This is for Backward compatibility if pro not updated.
78 */
79 add_action( 'parse_request', [ $this, 'backward_compability' ], 11 );
80
81 /**
82 * Make Compatible With Permalink Manager Plugin
83 */
84 add_filter( 'permalink_manager_detected_element_id', [ $this, 'provide_compatibility' ], 10, 3 );
85
86 /**
87 * Hook into redirect_canonical to prevent redirects for invalid category-post combinations
88 */
89 add_filter( 'redirect_canonical', [ $this, 'prevent_canonical_redirect_for_invalid_docs' ], 10, 2 );
90
91 /**
92 * Hook into template_redirect to validate category-post relationships
93 * Priority 0 to run before WordPress canonical redirect (priority 10)
94 */
95 add_action( 'template_redirect', [ $this, 'validate_single_docs_category_redirect' ], 0 );
96 }
97
98 public function provide_compatibility( $element_id, $uri_parts, $request_url ) {
99 if ( $request_url == $this->settings->get( 'docs_slug' ) ) {
100 $element_id = '';
101 }
102 return $element_id;
103 }
104
105 /**
106 * Prevent canonical redirect for invalid docs category-post combinations
107 *
108 * @param string $redirect_url The redirect URL.
109 * @param string $requested_url The requested URL.
110 * @return string|false The redirect URL or false to prevent redirect.
111 */
112 public function prevent_canonical_redirect_for_invalid_docs( $redirect_url, $requested_url ) {
113 global $wp_query;
114
115 // Check if this is a single docs query with category
116 if ( isset( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === 'docs' &&
117 isset( $wp_query->query_vars['doc_category'] ) && isset( $wp_query->query_vars['docs'] ) ) {
118
119 $doc_category = $wp_query->query_vars['doc_category'];
120 $post_name = $wp_query->query_vars['docs'];
121
122 // Handle hierarchical category slugs
123 $category_parts = explode('/', trim($doc_category, '/'));
124 $target_category_slug = end($category_parts);
125
126 global $wpdb;
127
128 // Check if post exists and belongs to the specified category
129 $post_id = (int) $wpdb->get_var(
130 $wpdb->prepare(
131 "SELECT p.ID FROM {$wpdb->posts} p
132 INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
133 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
134 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
135 WHERE p.post_name = %s AND p.post_type = %s AND t.slug = %s AND tt.taxonomy = %s
136 LIMIT 1",
137 esc_sql( $post_name ),
138 'docs',
139 esc_sql( $target_category_slug ),
140 'doc_category'
141 )
142 );
143
144 // If hierarchical slugs are enabled and we found a post, validate the full hierarchy
145 if ( $post_id > 0 && $this->settings->get( 'enable_category_hierarchy_slugs' ) && count($category_parts) > 1 ) {
146 $post_categories = wp_get_object_terms( $post_id, 'doc_category' );
147
148 if ( ! empty( $post_categories ) ) {
149 $found_valid_hierarchy = false;
150
151 foreach ( $post_categories as $post_category ) {
152 // Build the hierarchy path for this category
153 $hierarchy_path = [];
154 $current_term = $post_category;
155
156 // Build path from child to parent
157 while ( $current_term ) {
158 array_unshift( $hierarchy_path, $current_term->slug );
159 $current_term = $current_term->parent ? get_term( $current_term->parent, 'doc_category' ) : null;
160 }
161
162 // Check if this hierarchy matches the URL structure
163 if ( implode('/', $hierarchy_path) === $doc_category ) {
164 $found_valid_hierarchy = true;
165 break;
166 }
167 }
168
169 // If no valid hierarchy found, force 404
170 if ( ! $found_valid_hierarchy ) {
171 $post_id = 0;
172 }
173 }
174 }
175
176 // If no valid post found, prevent redirect (we'll show 404 instead)
177 if ( $post_id === 0 ) {
178 return false;
179 }
180 }
181
182 return $redirect_url;
183 }
184
185 /**
186 * Validate single docs category relationship on template_redirect and force 404 if invalid
187 */
188 public function validate_single_docs_category_redirect() {
189 global $wp_query;
190
191 // Check if this is a single docs query with category
192 if ( isset( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === 'docs' &&
193 isset( $wp_query->query_vars['doc_category'] ) && isset( $wp_query->query_vars['docs'] ) ) {
194
195 $doc_category = $wp_query->query_vars['doc_category'];
196 $post_name = $wp_query->query_vars['docs'];
197
198 // Handle hierarchical category slugs
199 $category_parts = explode('/', trim($doc_category, '/'));
200 $target_category_slug = end($category_parts);
201
202 global $wpdb;
203
204 // Check if post exists and belongs to the specified category
205 $post_id = (int) $wpdb->get_var(
206 $wpdb->prepare(
207 "SELECT p.ID FROM {$wpdb->posts} p
208 INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
209 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
210 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
211 WHERE p.post_name = %s AND p.post_type = %s AND t.slug = %s AND tt.taxonomy = %s
212 LIMIT 1",
213 esc_sql( $post_name ),
214 'docs',
215 esc_sql( $target_category_slug ),
216 'doc_category'
217 )
218 );
219
220 // If hierarchical slugs are enabled and we found a post, validate the full hierarchy
221 if ( $post_id > 0 && $this->settings->get( 'enable_category_hierarchy_slugs' ) && count($category_parts) > 1 ) {
222 $post_categories = wp_get_object_terms( $post_id, 'doc_category' );
223
224 if ( ! empty( $post_categories ) ) {
225 $found_valid_hierarchy = false;
226
227 foreach ( $post_categories as $post_category ) {
228 // Build the hierarchy path for this category
229 $hierarchy_path = [];
230 $current_term = $post_category;
231
232 // Build path from child to parent
233 while ( $current_term ) {
234 array_unshift( $hierarchy_path, $current_term->slug );
235 $current_term = $current_term->parent ? get_term( $current_term->parent, 'doc_category' ) : null;
236 }
237
238 // Check if this hierarchy matches the URL structure
239 if ( implode('/', $hierarchy_path) === $doc_category ) {
240 $found_valid_hierarchy = true;
241 break;
242 }
243 }
244
245 // If no valid hierarchy found, force 404
246 if ( ! $found_valid_hierarchy ) {
247 $post_id = 0;
248 }
249 }
250 }
251
252 // If no valid post found, force 404
253 if ( $post_id === 0 ) {
254 $wp_query->set_404();
255 status_header( 404 );
256 nocache_headers();
257 }
258 }
259 }
260
261 protected function is_docs( &$query_vars ) {
262 if ( ! $this->settings->get( 'builtin_doc_page', true ) ) {
263 $query_vars['post_type'] = 'page';
264 $query_vars['name'] = trim( $this->rewrite->get_base_slug(), '/' );
265 }
266
267 return $query_vars;
268 }
269
270 public function is_docs_feed( $query_vars ) {
271 global $wp_rewrite;
272 return isset( $query_vars['feed'] ) && in_array( $query_vars['feed'], $wp_rewrite->feeds );
273 }
274
275 public function is_docs_author( $query_vars ) {
276 return isset( $query_vars['author'] ) ? true : false;
277 }
278
279 protected function is_single_docs( $query_vars ) {
280 // Check for both 'name' and 'docs' query variables
281 if ( ! isset( $query_vars['name'] ) && ! isset( $query_vars['docs'] ) ) {
282 return false;
283 }
284
285 global $wpdb;
286 $name = isset( $query_vars['docs'] ) ? $query_vars['docs'] : $query_vars['name'];
287
288 // If doc_category is specified in the URL, validate that the post belongs to that category
289 if ( isset( $query_vars['doc_category'] ) ) {
290 $doc_category = $query_vars['doc_category'];
291
292 // Handle hierarchical category slugs (e.g., parent/child/grandchild)
293 $category_parts = explode('/', trim($doc_category, '/'));
294 $target_category_slug = end($category_parts); // Get the last part as the target category
295
296 // Check if post exists and belongs to the specified category (or its hierarchy)
297 $_post_id = (int) $wpdb->get_var(
298 $wpdb->prepare(
299 "SELECT p.ID FROM {$wpdb->posts} p
300 INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
301 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
302 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
303 WHERE p.post_name = %s AND p.post_type = %s AND t.slug = %s AND tt.taxonomy = %s
304 LIMIT 1",
305 esc_sql( $name ),
306 'docs',
307 esc_sql( $target_category_slug ),
308 'doc_category'
309 )
310 );
311
312 // If hierarchical slugs are enabled and we found a post, validate the full hierarchy
313 if ( $_post_id > 0 && $this->settings->get( 'enable_category_hierarchy_slugs' ) && count($category_parts) > 1 ) {
314 // Get the post's category terms
315 $post_categories = wp_get_object_terms( $_post_id, 'doc_category' );
316
317 if ( ! empty( $post_categories ) ) {
318 $found_valid_hierarchy = false;
319
320 foreach ( $post_categories as $post_category ) {
321 // Build the hierarchy path for this category
322 $hierarchy_path = [];
323 $current_term = $post_category;
324
325 // Build path from child to parent
326 while ( $current_term ) {
327 array_unshift( $hierarchy_path, $current_term->slug );
328 $current_term = $current_term->parent ? get_term( $current_term->parent, 'doc_category' ) : null;
329 }
330
331 // Check if this hierarchy matches the URL structure
332 if ( implode('/', $hierarchy_path) === $doc_category ) {
333 $found_valid_hierarchy = true;
334 break;
335 }
336 }
337
338 // If no valid hierarchy found, return false (404)
339 if ( ! $found_valid_hierarchy ) {
340 $_post_id = 0;
341 }
342 }
343 }
344 } else {
345 // Fallback to original behavior if no category is specified
346 $_post_id = (int) $wpdb->get_var(
347 $wpdb->prepare(
348 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s LIMIT 1",
349 esc_sql( $name ),
350 'docs'
351 )
352 );
353 }
354
355 return $_post_id > 0;
356 }
357
358 protected function is_docs_category( $query_vars ) {
359 return $this->term_exists( $query_vars, 'doc_category' );
360 }
361
362 protected function is_docs_tag( $query_vars ) {
363 return $this->term_exists( $query_vars, 'doc_tag' );
364 }
365
366 protected function term_exists( $query_vars, $taxonomy ) {
367 if ( ! isset( $query_vars[ $taxonomy ] ) ) {
368 return false;
369 }
370
371 return term_exists( $query_vars[ $taxonomy ], $taxonomy );
372 }
373
374 public function set_perma_structure( $structures = [] ) {
375 $this->perma_structure = array_merge( $this->perma_structure, $structures );
376 }
377
378 public function set_query_vars( $query_vars = [] ) {
379 $this->query_vars = array_merge( $this->query_vars, $query_vars );
380 }
381
382 public function backward_compability( $wp ) {
383 if ( static::$already_parsed ) {
384 return;
385 }
386
387 $this->permalink_magic( $wp );
388 }
389
390 public function parse( $wp ) {
391 static::$already_parsed = true;
392
393 $this->perma_structure = apply_filters('docs_rewrite_rules', $this->perma_structure);
394
395 $this->permalink_magic( $wp );
396 }
397
398 protected function permalink_magic( $wp ) {
399 $this->wp_query_vars = $wp->query_vars;
400
401 if ( ! empty( $this->perma_structure ) ) {
402 $_valid = [];
403
404 foreach ( $this->perma_structure as $_type => $structure ) {
405 $_perma_vars = $this->is_perma_valid_for( $structure, $wp->request );
406
407 // $_valid = empty( $_valid ) && $_perma_vars ? [ 'type' => $_type, 'query_vars' => $_perma_vars ] : $_valid;
408 if ( ( $_perma_vars && method_exists( $this, $_type ) && call_user_func_array( [$this, $_type], [ & $_perma_vars] ) ) ) {
409 // dump( $_type, $_perma_vars );
410 if ( $_type === 'is_single_docs' || $_type == 'is_docs_feed' || $_type == 'is_docs_author' ) {
411 $_perma_vars['post_type'] = 'docs';
412 }
413 $_valid = ['type' => $_type, 'query_vars' => $_perma_vars];
414 }
415 }
416
417 $type = isset( $_valid['type'] ) ? $_valid['type'] : '';
418 $query_vars = isset( $_valid['query_vars'] ) ? $_valid['query_vars'] : [];
419
420 if ( ! empty( $type ) ) {
421 unset( $this->query_vars[ $type ] );
422 array_map(
423 function ( $_vars ) use ( &$wp ) {
424 array_map(
425 function ( $_var ) use ( &$wp ) {
426 unset( $wp->query_vars[ $_var ] );
427 },
428 $_vars
429 );
430 },
431 $this->query_vars
432 );
433 }
434
435 $wp->query_vars = is_array( $query_vars ) ? array_merge( $wp->query_vars, $query_vars ) : $wp->query_vars;
436 // Fallback
437 if ( ! empty( $_valid ) ) {
438 unset( $wp->query_vars['attachment'] );
439 }
440 }
441 }
442
443 /**
444 * This method is responsible for checking a structure is valid again a request.
445 *
446 * @param string $structure
447 * @param string $request
448 * @return array|bool
449 */
450 private function is_perma_valid_for( $structure, $request ) {
451 if ( empty( $structure ) ) {
452 return false;
453 }
454
455 $_tags = explode( '/', trim( $structure, '/' ) );
456 $_replace_matched_tags = [];
457
458 $_replace_tags = array_filter(
459 $_tags,
460 function ( $item ) use ( &$_replace_matched_tags ) {
461 $_is_valid = strpos( $item, '%' ) !== false;
462 if ( $_is_valid ) {
463 $_replace_matched_tags[] = trim( $item, '%' );
464 }
465 return $_is_valid;
466 }
467 );
468
469 $_perma_structure = preg_quote( $structure, '/' );
470 $_perma_structure = str_replace( $_replace_tags, '([^\/]+)', $_perma_structure );
471
472 preg_match( "/^$_perma_structure$/", $request, $matches );
473
474 if ( empty( $matches ) || ! is_array( $matches ) ) {
475 return false;
476 }
477
478 if ( count( $matches ) === 1 ) {
479 return [ 'post_type' => 'docs' ];
480 }
481
482 unset( $matches[0] );
483
484 return array_combine( $_replace_matched_tags, $matches );
485 }
486 }
487