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/Rewrite.php +307 -188 3.5.04.9.2 View file →
@@ -1,238 +1,357 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Core;
2 3
3 -namespace WPDeveloper\BetterDocs\Core;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 7
8 +
5 9 use WP_Post;
6 10 use WPDeveloper\BetterDocs\Utils\Base;
7 11 use WPDeveloper\BetterDocs\Utils\Database;
8 12
9 13 class Rewrite extends Base {
10 - protected $settings;
11 - protected $database;
14 + protected $settings;
15 + protected $database;
12 16
13 - public function __construct( Settings $settings, Database $database ) {
14 - $this->settings = $settings;
15 - $this->database = $database;
16 - }
17 + public function __construct( Settings $settings, Database $database ) {
18 + $this->settings = $settings;
19 + $this->database = $database;
20 + }
17 21
18 - public function init() {
19 - add_action( 'init', [$this, 'rules'] );
20 - add_action( 'betterdocs::settings::saved', [$this, 'save_permalink_structure'], 2, 3 );
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 + }
22 28
23 - public function remove_knowledge_base_placeholder( $permalink ) {
24 - $permalink_array = $this->permalink_structure( $permalink, 'arraywithpercent' );
25 - $permalink_array = array_filter( $permalink_array, function ( $item ) {
26 - return $item !== '%knowledge_base%';
27 - } );
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 + }
28 48
29 - return trailingslashit( implode( '/', $permalink_array ) );
30 - }
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 + ];
31 53
32 - /**
33 - * This method is hooked with an action called 'betterdocs::settings::saved'
34 - *
35 - * @since 2.5.0
36 - *
37 - * @param bool $_saved
38 - * @param array $_settings
39 - * @param array $_old_settings
40 - *
41 - * @return void
42 - */
43 - public function save_permalink_structure( $_saved, $_settings, $_old_settings ) {
44 - if ( ! isset($_settings['permalink_structure']) ) {
45 - return;
46 - }
54 + if ( ! isset( $slug_map[ $taxonomy ] ) || $slug_map[ $taxonomy ] === '' ) {
55 + return $termlink;
56 + }
47 57
48 - $_permalink_structure = $_settings['permalink_structure'];
58 + $default = $slug_map[ $taxonomy ];
59 + $translated = \WPDeveloper\BetterDocs\Utils\Helper::wpml_translated_tax_slug( $taxonomy, $default );
49 60
50 - /**
51 - * TODO: Let's re-check later to optimize it.
52 - */
53 - if ( ! betterdocs()->is_pro_active() ) {
54 - $_permalink_structure = $this->remove_knowledge_base_placeholder( $_permalink_structure );
55 - }
61 + if ( $translated === $default ) {
62 + return $termlink;
63 + }
56 64
57 - if ( $_permalink_structure !== $_old_settings['permalink_structure'] ) {
58 - $this->settings->save( 'permalink_structure', $this->permalink_structure( $_permalink_structure ) );
59 - }
65 + return preg_replace( '#/' . preg_quote( $default, '#' ) . '/#', '/' . $translated . '/', $termlink, 1 );
66 + }
60 67
61 - /**
62 - * This block of code decides whether it needs to be flushed or not.
63 - * Flush happens after register the post type.
64 - */
65 - switch ( true ) {
66 - case $_permalink_structure !== $_old_settings['permalink_structure']:
67 - case $_settings['docs_slug'] !== $_old_settings['docs_slug']:
68 - case $_settings['builtin_doc_page'] !== $_old_settings['builtin_doc_page']:
69 - case $_settings['docs_page'] !== $_old_settings['docs_page']:
70 - case $_settings['tag_slug'] !== $_old_settings['tag_slug']:
71 - case $_settings['category_slug'] !== $_old_settings['category_slug']:
72 - $this->database->set_transient( 'betterdocs_flush_rewrite_rules', true );
73 - break;
74 - }
75 - }
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 76
77 - public function permalink_structure( $structure = '', $output = 'string' ) {
78 - if ( empty( $structure ) ) {
79 - $structure = $this->settings->get( 'permalink_structure', 'docs' );
80 - }
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 + }
81 85
82 - $docs_slug = $this->get_base_slug();
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 + );
83 94
84 - $_structure_array = explode( '%', $structure );
85 - if ( $_structure_array[0] == '/' ) {
86 - $structure = $docs_slug . $structure;
87 - } else if ( $_structure_array[0] == '' ) {
88 - $structure = $docs_slug . '/' . $structure;
89 - }
95 + return trailingslashit( implode( '/', $permalink_array ) );
96 + }
90 97
91 - $structure = trim( $structure, '/' );
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 + }
92 113
93 - if ( $output === 'string' ) {
94 - return $structure;
95 - }
114 + $_permalink_structure = $_settings['permalink_structure'];
96 115
97 - if ( $output === 'array' ) {
98 - $structure = explode( '/', $structure );
99 - $structure = array_map( function ( $item ) {
100 - return trim( $item, "/%" );
101 - }, $structure );
102 - }
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 + }
103 122
104 - if ( $output === 'arraywithpercent' ) {
105 - $structure = explode( '/', $structure );
106 - $structure = array_map( function ( $item ) {
107 - return trim( $item, "/" );
108 - }, $structure );
109 - }
123 + if ( $_permalink_structure !== $_old_settings['permalink_structure'] ) {
124 + $this->settings->save( 'permalink_structure', $this->permalink_structure( $_permalink_structure ) );
125 + }
110 126
111 - return $structure;
112 - }
127 + $old_docs_page = isset( $_old_settings['docs_page'] ) ? $_old_settings['docs_page'] : '';
128 + $current_docs_page = isset( $_settings['docs_page'] ) ? $_settings['docs_page'] : '';
113 129
114 - public function get_base_slug() {
115 - $docs_slug = $this->settings->get( 'docs_slug', 'docs' );
116 - $docs_page = $this->settings->get( 'docs_page', 0 );
117 - $builtin_doc_page = $this->settings->get( 'builtin_doc_page', true );
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 + }
118 145
119 - if ( ! $builtin_doc_page && $docs_page !== 0 ) {
120 - $post_info = get_post( $docs_page );
121 - $docs_slug = $post_info instanceof WP_Post ? $post_info->post_name : $docs_slug;
122 - }
146 + public function permalink_structure( $structure = '', $output = 'string' ) {
147 + if ( empty( $structure ) ) {
148 + $structure = $this->settings->get( 'permalink_structure', 'docs' );
149 + }
123 150
124 - $docs_slug = trim( $docs_slug, '/' );
151 + $docs_slug = $this->get_base_slug();
125 152
126 - return $docs_slug;
127 - }
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 + }
128 159
129 - public function normalzie_doc_perma_structure( $structure ){
130 - $structure = $this->permalink_structure( $structure, 'arraywithpercent' );
131 - $structure = array_reduce( $structure, function( $carry, $item ){
132 - if( strpos( $item, '%' ) !== false ) {
133 - $carry[] = $item;
134 - } else {
135 - if ($carry && strpos(end($carry), '%') === false) {
136 - $carry[count($carry) - 1] .= "/$item";
137 - } else {
138 - $carry[] = $item;
139 - }
140 - }
160 + $structure = trim( $structure, '/' );
141 161
142 - return $carry;
143 - }, []);
162 + if ( $output === 'string' ) {
163 + return $structure;
164 + }
144 165
145 - $structure[] = '%docs%';
166 + if ( $output === 'array' ) {
167 + $structure = explode( '/', $structure );
168 + $structure = array_map(
169 + function ( $item ) {
170 + return trim( $item, '/%' );
171 + },
172 + $structure
173 + );
174 + }
146 175
147 - $group = array_filter( $structure, function( $item ){
148 - return strpos($item, '%') === 0;
149 - });
176 + if ( $output === 'arraywithpercent' ) {
177 + $structure = explode( '/', $structure );
178 + $structure = array_map(
179 + function ( $item ) {
180 + return trim( $item, '/' );
181 + },
182 + $structure
183 + );
184 + }
150 185
151 - return [
152 - 'raw' => $structure,
153 - 'group' => $group
154 - ];
155 - }
186 + return $structure;
187 + }
156 188
157 - public function make_regex( $segments ){
158 - return array_reduce( $segments, function( $carry, $item ){
159 - $carry .= ( strpos( $item, '%' ) !== false ? "([^/]+)" : $item ) . '/';
160 - return $carry;
161 - }, '') . '?$';
162 - }
189 + public function hierarchy_based_slug_switch() {
190 + $hierarchy_slug = $this->settings->get( 'enable_category_hierarchy_slugs', false );
191 + return $hierarchy_slug;
192 + }
163 193
164 - public function make_query( $segments ){
165 - $query = [];
166 - $matchId = 1;
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 );
167 198
168 - foreach( $segments as $segment ) {
169 - $query[] = trim($segment, '%') . '=$matches[' . $matchId . ']';
170 - $matchId++;
171 - }
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 + }
172 203
173 - return 'index.php?' . implode('&', $query) . '&post_type=docs';
174 - }
204 + $docs_slug = trim( $docs_slug, '/' );
175 205
176 - public function rules() {
177 - /**
178 - * docs can be dynamic / as its a root slug
179 - *
180 - * https://url/docs/kb-slug
181 - * https://url/docs/kb-slug/category-slug
182 - * https://url/docs-category/category-slug
183 - *
184 - * Docs Visit:
185 - * docs can be dynamic / as its a root slug
186 - * or docs can be change by settings.
187 - *
188 - * https://url/docs/(docs-slug)
189 - * https://url/docs/(cat-slug)/(docs-slug)
190 - * https://url/docs/(kb-slug)/(docs-slug)
191 - * https://url/docs/(kb-slug)/(cat-slug)/(docs-slug)
192 - * https://url/docs/(cat-slug)/(kb-slug)/(docs-slug)
193 - */
206 + return $docs_slug;
207 + }
194 208
195 - /**
196 - * This code of blocks used to determine single docs permalink.
197 - */
198 - $_docs_perma_struct = betterdocs()->settings->get( 'permalink_structure', 'docs' );
199 - $_normalized_structure = $this->normalzie_doc_perma_structure($_docs_perma_struct);
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 + }
200 221
201 - $_feed_regex = $_normalized_structure['raw'];
202 - $_feed_regex[] = '(feed|rdf|rss|rss2|atom)';
203 - $_feed_group = $_normalized_structure['group'];
204 - $_feed_group[] = '%feed%';
222 + return $carry;
223 + },
224 + []
225 + );
205 226
206 - $this->add_rewrite_rule( $this->make_regex( $_feed_regex ), $this->make_query( $_feed_group ) );
227 + $structure[] = '%docs%';
207 228
208 - $this->add_rewrite_rule(
209 - $this->make_regex( $_normalized_structure['raw'] ),
210 - $this->make_query( $_normalized_structure['group'] )
211 - );
229 + $group = array_filter(
230 + $structure,
231 + function ( $item ) {
232 + return strpos( $item, '%' ) === 0;
233 + }
234 + );
212 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 +
213 294 $base = $this->get_base_slug();
214 - $this->add_rewrite_rule( $base . '/(feed|rdf|rss|rss2|atom)/?$', 'index.php?post_type=docs&feed=$matches[1]' );
215 - }
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]' );
216 298
217 - public function add_rewrite_rule( $regex, $query, $after = 'top' ) {
218 - add_rewrite_rule( $regex, $query, $after );
219 - }
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 );
220 304
221 - public function docs_type_rewrite( $rewrite, $slug ) {
222 - $permalink = betterdocs()->settings->get( 'permalink_structure', 'docs/' );
223 - $permalink_structure = $this->permalink_structure( $permalink, 'array' );
224 - $permalink = apply_filters( 'betterdocs_docs_type_rewrite_permalink', $permalink, $slug, $permalink_structure );
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 + }
225 316
226 - /**
227 - * TODO: Let's re-check later for optimization.
228 - */
229 - if ( ! betterdocs()->is_pro_active() ) {
230 - $permalink = $this->remove_knowledge_base_placeholder( $permalink );
231 - }
317 + $_feed_regex = $_normalized_structure['raw'];
318 + $_feed_regex[] = '(feed|rdf|rss|rss2|atom)';
319 + $_feed_group = $_normalized_structure['group'];
320 + $_feed_group[] = '%feed%';
232 321
233 - return apply_filters( 'betterdocs_docs_rewrite', [
234 - 'slug' => trim( $permalink, '/' ),
235 - 'with_front' => false
236 - ], $slug );
237 - }
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 + }
238 357 }