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

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

358 lines 10.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
9 use WP_Post;
10 use WPDeveloper\BetterDocs\Utils\Base;
11 use WPDeveloper\BetterDocs\Utils\Database;
12
13 class Rewrite extends Base {
14 protected $settings;
15 protected $database;
16
17 public function __construct( Settings $settings, Database $database ) {
18 $this->settings = $settings;
19 $this->database = $database;
20 }
21
22 public function init() {
23 add_action( 'init', [ $this, 'rules' ] );
24 add_action( 'betterdocs::settings::saved', [ $this, 'save_permalink_structure' ], 2, 3 );
25 add_action( 'template_redirect', [ $this, 'handle_pagination_redirect' ] );
26 add_filter( 'term_link', [ $this, 'taxonomy_term_link' ], 10, 3 );
27 }
28
29 /**
30 * Swap a BetterDocs taxonomy base slug for its WPML-translated value in term links.
31 *
32 * doc_tag / doc_category are registered with the default-language slug, so
33 * WordPress' core get_term_link() always builds `/docs-tag/{slug}/` etc. When
34 * WPML translates the "URL <taxonomy> tax slug" string, rewrite the base segment
35 * to the active language's slug so term links match the translated archive URL.
36 * No-op for other taxonomies, when the slug isn't translated, or when the base
37 * segment isn't present (e.g. MKB category permalinks that omit it).
38 *
39 * @param string $termlink
40 * @param \WP_Term $term
41 * @param string $taxonomy
42 * @return string
43 */
44 public function taxonomy_term_link( $termlink, $term, $taxonomy ) {
45 if ( ! is_string( $termlink ) ) {
46 return $termlink;
47 }
48
49 $slug_map = [
50 'doc_tag' => trim( $this->settings->get( 'tag_slug', 'docs-tag' ), '/' ),
51 'doc_category' => trim( $this->settings->get( 'category_slug', 'docs-category' ), '/' ),
52 ];
53
54 if ( ! isset( $slug_map[ $taxonomy ] ) || $slug_map[ $taxonomy ] === '' ) {
55 return $termlink;
56 }
57
58 $default = $slug_map[ $taxonomy ];
59 $translated = \WPDeveloper\BetterDocs\Utils\Helper::wpml_translated_tax_slug( $taxonomy, $default );
60
61 if ( $translated === $default ) {
62 return $termlink;
63 }
64
65 return preg_replace( '#/' . preg_quote( $default, '#' ) . '/#', '/' . $translated . '/', $termlink, 1 );
66 }
67
68 /**
69 * Handle pagination redirect
70 * Redirects pagination attempts to main archive
71 */
72 public function handle_pagination_redirect() {
73 if ( ! is_post_type_archive( 'docs' ) ) {
74 return;
75 }
76
77 $paged = get_query_var( 'paged' );
78 if ( $paged > 1 ) {
79 global $wp_query;
80 $wp_query->set_404();
81 status_header( 404 );
82 nocache_headers();
83 }
84 }
85
86 public function remove_knowledge_base_placeholder( $permalink ) {
87 $permalink_array = $this->permalink_structure( $permalink, 'arraywithpercent' );
88 $permalink_array = array_filter(
89 $permalink_array,
90 function ( $item ) {
91 return $item !== '%knowledge_base%';
92 }
93 );
94
95 return trailingslashit( implode( '/', $permalink_array ) );
96 }
97
98 /**
99 * This method is hooked with an action called 'betterdocs::settings::saved'
100 *
101 * @since 2.5.0
102 *
103 * @param bool $_saved
104 * @param array $_settings
105 * @param array $_old_settings
106 *
107 * @return void
108 */
109 public function save_permalink_structure( $_saved, $_settings, $_old_settings ) {
110 if ( ! isset( $_settings['permalink_structure'] ) ) {
111 return;
112 }
113
114 $_permalink_structure = $_settings['permalink_structure'];
115
116 /**
117 * TODO: Let's re-check later to optimize it.
118 */
119 if ( ! betterdocs()->is_pro_active() ) {
120 $_permalink_structure = $this->remove_knowledge_base_placeholder( $_permalink_structure );
121 }
122
123 if ( $_permalink_structure !== $_old_settings['permalink_structure'] ) {
124 $this->settings->save( 'permalink_structure', $this->permalink_structure( $_permalink_structure ) );
125 }
126
127 $old_docs_page = isset( $_old_settings['docs_page'] ) ? $_old_settings['docs_page'] : '';
128 $current_docs_page = isset( $_settings['docs_page'] ) ? $_settings['docs_page'] : '';
129
130 /**
131 * This block of code decides whether it needs to be flushed or not.
132 * Flush happens after register the post type.
133 */
134 switch ( true ) {
135 case $_permalink_structure !== $_old_settings['permalink_structure']:
136 case $_settings['docs_slug'] !== $_old_settings['docs_slug']:
137 case $_settings['builtin_doc_page'] !== $_old_settings['builtin_doc_page']:
138 case $current_docs_page !== $old_docs_page:
139 case $_settings['tag_slug'] !== $_old_settings['tag_slug']:
140 case $_settings['category_slug'] !== $_old_settings['category_slug']:
141 $this->database->set_transient( 'betterdocs_flush_rewrite_rules', true );
142 break;
143 }
144 }
145
146 public function permalink_structure( $structure = '', $output = 'string' ) {
147 if ( empty( $structure ) ) {
148 $structure = $this->settings->get( 'permalink_structure', 'docs' );
149 }
150
151 $docs_slug = $this->get_base_slug();
152
153 $_structure_array = explode( '%', $structure );
154 if ( $_structure_array[0] == '/' ) {
155 $structure = $docs_slug . $structure;
156 } elseif ( $_structure_array[0] == '' ) {
157 $structure = $docs_slug . '/' . $structure;
158 }
159
160 $structure = trim( $structure, '/' );
161
162 if ( $output === 'string' ) {
163 return $structure;
164 }
165
166 if ( $output === 'array' ) {
167 $structure = explode( '/', $structure );
168 $structure = array_map(
169 function ( $item ) {
170 return trim( $item, '/%' );
171 },
172 $structure
173 );
174 }
175
176 if ( $output === 'arraywithpercent' ) {
177 $structure = explode( '/', $structure );
178 $structure = array_map(
179 function ( $item ) {
180 return trim( $item, '/' );
181 },
182 $structure
183 );
184 }
185
186 return $structure;
187 }
188
189 public function hierarchy_based_slug_switch() {
190 $hierarchy_slug = $this->settings->get( 'enable_category_hierarchy_slugs', false );
191 return $hierarchy_slug;
192 }
193
194 public function get_base_slug() {
195 $docs_slug = $this->settings->get( 'docs_slug', 'docs' );
196 $docs_page = $this->settings->get( 'docs_page', 0 );
197 $builtin_doc_page = $this->settings->get( 'builtin_doc_page', true );
198
199 if ( ! $builtin_doc_page && $docs_page !== 0 ) {
200 $post_info = get_post( $docs_page );
201 $docs_slug = $post_info instanceof WP_Post ? $post_info->post_name : $docs_slug;
202 }
203
204 $docs_slug = trim( $docs_slug, '/' );
205
206 return $docs_slug;
207 }
208
209 public function normalzie_doc_perma_structure( $structure ) {
210 $structure = $this->permalink_structure( $structure, 'arraywithpercent' );
211 $structure = array_reduce(
212 $structure,
213 function ( $carry, $item ) {
214 if ( strpos( $item, '%' ) !== false ) {
215 $carry[] = $item;
216 } elseif ( $carry && strpos( end( $carry ), '%' ) === false ) {
217 $carry[ count( $carry ) - 1 ] .= "/$item";
218 } else {
219 $carry[] = $item;
220 }
221
222 return $carry;
223 },
224 []
225 );
226
227 $structure[] = '%docs%';
228
229 $group = array_filter(
230 $structure,
231 function ( $item ) {
232 return strpos( $item, '%' ) === 0;
233 }
234 );
235
236 return [
237 'raw' => $structure,
238 'group' => $group
239 ];
240 }
241
242 public function make_regex( $segments ) {
243 return array_reduce(
244 $segments,
245 function ( $carry, $item ) {
246 $carry .= ( strpos( $item, '%' ) !== false ? '([^/]+)' : $item ) . '/';
247 return $carry;
248 },
249 '^'
250 ) . '?$';
251 }
252
253 public function make_query( $segments ) {
254 $query = [];
255 $matchId = 1;
256
257 foreach ( $segments as $segment ) {
258 $query[] = trim( $segment, '%' ) . '=$matches[' . $matchId . ']';
259 ++$matchId;
260 }
261
262 return 'index.php?' . implode( '&', $query ) . '&post_type=docs';
263 }
264
265 public function rules() {
266 /**
267 * docs can be dynamic / as its a root slug
268 *
269 * https://url/docs/kb-slug
270 * https://url/docs/kb-slug/category-slug
271 * https://url/docs-category/category-slug
272 *
273 * Docs Visit:
274 * docs can be dynamic / as its a root slug
275 * or docs can be change by settings.
276 *
277 * https://url/docs/(docs-slug)
278 * https://url/docs/(cat-slug)/(docs-slug)
279 * https://url/docs/(kb-slug)/(docs-slug)
280 * https://url/docs/(kb-slug)/(cat-slug)/(docs-slug)
281 * https://url/docs/(cat-slug)/(kb-slug)/(docs-slug)
282 */
283
284 $base = $this->get_base_slug();
285 $hierarchy_slug = $this->hierarchy_based_slug_switch();
286
287 // Add rule to handle pagination attempts
288 $this->add_rewrite_rule(
289 '^' . $base . '/page/([0-9]+)/?$',
290 'index.php?post_type=docs',
291 'top'
292 );
293
294 $base = $this->get_base_slug();
295 $this->add_rewrite_rule( '^' . $base . '/(feed|rdf|rss|rss2|atom)/?$', 'index.php?post_type=docs&feed=$matches[1]' );
296 $this->add_rewrite_rule( '^' . $base . '/authors/([0-9]+)/?$', 'index.php?post_type=docs&author=$matches[1]' );
297 $this->add_rewrite_rule( '^' . $base . '/authors/([0-9]+)/page/([0-9]+)/?$', 'index.php?post_type=docs&author=$matches[1]&page=$matches[2]' );
298
299 /**
300 * This code of blocks used to determine single docs permalink.
301 */
302 $_docs_perma_struct = betterdocs()->settings->get( 'permalink_structure', 'docs' );
303 $_normalized_structure = $this->normalzie_doc_perma_structure( $_docs_perma_struct );
304
305 if( $hierarchy_slug ) { // register hierarchy based slug rewrite rule, for single doc permalink
306 // When MKB or WPML is used, the actual base for single docs might differ from the generic $base
307 $perma_base = isset( $_normalized_structure['raw'][0] ) && strpos( $_normalized_structure['raw'][0], '%' ) === false
308 ? $_normalized_structure['raw'][0]
309 : $base;
310
311 $this->add_rewrite_rule( '^' . $perma_base . '/(.+?)/([^/]+)(?:/([0-9]+))?/?$', 'index.php?doc_category=$matches[1]&docs=$matches[2]&post_type=docs', 'top' );
312
313 // Removed flush_rewrite_rules() from here. Calling flush_rewrite_rules() on the 'init' hook causes
314 // infinite database updates and destroys custom rules when WPML or MKB dynamically filters keys.
315 }
316
317 $_feed_regex = $_normalized_structure['raw'];
318 $_feed_regex[] = '(feed|rdf|rss|rss2|atom)';
319 $_feed_group = $_normalized_structure['group'];
320 $_feed_group[] = '%feed%';
321
322 $this->add_rewrite_rule( $this->make_regex( $_feed_regex ), $this->make_query( $_feed_group ) );
323
324 $this->add_rewrite_rule(
325 $this->make_regex( $_normalized_structure['raw'] ),
326 $this->make_query( $_normalized_structure['group'] )
327 );
328
329 $this->add_rewrite_rule( '^' . $base . '/(feed|rdf|rss|rss2|atom)/?$', 'index.php?post_type=docs&feed=$matches[1]' );
330 }
331
332 public function add_rewrite_rule( $regex, $query, $after = 'top' ) {
333 add_rewrite_rule( $regex, $query, $after );
334 }
335
336 public function docs_type_rewrite( $rewrite, $slug ) {
337 $permalink = betterdocs()->settings->get( 'permalink_structure', 'docs/' );
338 $permalink_structure = $this->permalink_structure( $permalink, 'array' );
339 $permalink = apply_filters( 'betterdocs_docs_type_rewrite_permalink', $permalink, $slug, $permalink_structure );
340
341 /**
342 * TODO: Let's re-check later for optimization.
343 */
344 if ( ! betterdocs()->is_pro_active() ) {
345 $permalink = $this->remove_knowledge_base_placeholder( $permalink );
346 }
347
348 return apply_filters(
349 'betterdocs_docs_rewrite',
350 [
351 'slug' => trim( $permalink, '/' ),
352 'with_front' => false
353 ],
354 $slug
355 );
356 }
357 }
358