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
betterdocs / includes / REST / Translations.php

Translations.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.2, at includes/REST/Translations.php

203 lines 7.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\REST;
4
5 use WP_REST_Request;
6 use WPDeveloper\BetterDocs\Core\BaseAPI;
7 use WPDeveloper\BetterDocs\Utils\Helper;
8
9 /**
10 * Term translation linking for the React admin (WPML / Polylang).
11 *
12 * Powers the slide-over "Language / This is a translation of / Translate" panel
13 * for the doc_category, doc_tag and knowledge_base taxonomies.
14 */
15 class Translations extends BaseAPI {
16
17 /** Taxonomies the React admins manage. */
18 private function allowed_taxonomies() {
19 return array_filter(
20 [ 'doc_category', 'doc_tag', 'knowledge_base' ],
21 'taxonomy_exists'
22 );
23 }
24
25 public function permission_check(): bool {
26 // QA-013: these routes read/write term language meta, so require the
27 // doc-term management cap (Editor+) — `edit_docs` is Author-level and
28 // let any author rewrite another category's language assignment. Mirrors
29 // the `manage_doc_terms` guard used by PostType term operations.
30 return current_user_can( 'manage_doc_terms' );
31 }
32
33 public function register() {
34 $this->get( 'term-translations', [ $this, 'get_term_translations' ] );
35 $this->post( 'term-language', [ $this, 'set_term_language' ] );
36
37 add_filter( 'rest_request_before_callbacks', [ $this, 'switch_language_for_term_update' ], 10, 3 );
38 }
39
40 /**
41 * WPML lets translated terms share one slug across languages, but it
42 * deliberately skips its language filtering whenever get_term_by() is in the
43 * call stack (WPML_Term_Clauses::filter). Core wp_update_term() uses exactly
44 * that lookup for its duplicate-slug check, so updating a term whose slug has
45 * an older other-language twin via the core REST controllers always fails
46 * with duplicate_term_slug — even when the payload never touches the slug
47 * (e.g. changing only the KB order from the React admin). While such a
48 * request is being dispatched, restrict slug lookups on the edited taxonomy
49 * to the edited term's own language: the term matches itself, and genuine
50 * same-language collisions are still rejected.
51 *
52 * The term's language is read from its term_taxonomy_id directly because
53 * get_term() under WPML adjusts the ID to the current display language's
54 * translation, which would report the wrong language here.
55 */
56 public function switch_language_for_term_update( $response, $handler, $request ) {
57 global $wpdb;
58
59 if ( ! $request instanceof WP_REST_Request || ! Helper::is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
60 return $response;
61 }
62
63 if ( ! in_array( $request->get_method(), [ 'POST', 'PUT', 'PATCH' ], true ) ) {
64 return $response;
65 }
66
67 if ( ! preg_match( '#^/wp/v2/(doc_category|doc_tag|knowledge_base)/(\d+)$#', $request->get_route(), $matches ) ) {
68 return $response;
69 }
70
71 $taxonomy = $matches[1];
72 $ttid = $wpdb->get_var(
73 $wpdb->prepare(
74 "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s",
75 absint( $matches[2] ),
76 $taxonomy
77 )
78 );
79 if ( ! $ttid ) {
80 return $response;
81 }
82
83 $term_lang = apply_filters( 'wpml_element_language_code', null, [
84 'element_id' => (int) $ttid,
85 'element_type' => $taxonomy,
86 ] );
87 if ( ! $term_lang ) {
88 return $response;
89 }
90
91 $clauses_filter = function ( $clauses, $taxonomies, $args ) use ( $taxonomy, $term_lang ) {
92 global $wpdb;
93
94 // Only slug lookups on the edited taxonomy (the duplicate-slug check).
95 if ( empty( $args['slug'] ) || [ $taxonomy ] !== (array) $taxonomies ) {
96 return $clauses;
97 }
98
99 $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}icl_translations bd_icl_t ON bd_icl_t.element_id = tt.term_taxonomy_id AND bd_icl_t.element_type = CONCAT( 'tax_', tt.taxonomy )";
100 $clauses['where'] .= $wpdb->prepare( ' AND ( bd_icl_t.language_code = %s OR bd_icl_t.language_code IS NULL )', $term_lang );
101
102 return $clauses;
103 };
104 add_filter( 'terms_clauses', $clauses_filter, 10, 3 );
105
106 // WPML's save hooks stamp edited terms with the *current* language, so a
107 // request arriving in another language context would silently reassign
108 // the term's language. Align the context with the edited term.
109 $prev_lang = apply_filters( 'wpml_current_language', null );
110 if ( $term_lang !== $prev_lang ) {
111 do_action( 'wpml_switch_language', $term_lang );
112 }
113
114 // Scope the workaround to this callback only: undo both the clause
115 // filter and the language switch once the targeted update has run, so
116 // nothing leaks into batch subrequests or response-side hooks.
117 $cleanup = function ( $after_response ) use ( &$cleanup, $clauses_filter, $term_lang, $prev_lang ) {
118 remove_filter( 'terms_clauses', $clauses_filter, 10 );
119 if ( $term_lang !== $prev_lang ) {
120 do_action( 'wpml_switch_language', $prev_lang );
121 }
122 remove_filter( 'rest_request_after_callbacks', $cleanup, 10 );
123
124 return $after_response;
125 };
126 add_filter( 'rest_request_after_callbacks', $cleanup, 10 );
127
128 return $response;
129 }
130
131 /**
132 * Translation group + "this is a translation of" candidates for a term (or for
133 * a target language when creating a new translation).
134 */
135 public function get_term_translations( WP_REST_Request $request ) {
136 $taxonomy = sanitize_key( (string) $request->get_param( 'taxonomy' ) );
137 if ( ! in_array( $taxonomy, $this->allowed_taxonomies(), true ) || ! Helper::is_multilingual_active() ) {
138 return rest_ensure_response( [ 'enabled' => false ] );
139 }
140
141 $default_lang = Helper::get_default_language();
142 $target_lang = sanitize_text_field( (string) $request->get_param( 'lang' ) );
143 $source_lang = sanitize_text_field( (string) $request->get_param( 'source_lang' ) ) ?: $default_lang;
144 $term_id = absint( $request->get_param( 'term_id' ) );
145
146 $current_lang = '';
147 $translation_of = 0;
148 $translations = [];
149
150 if ( $term_id ) {
151 $term = get_term( $term_id, $taxonomy );
152 if ( $term && ! is_wp_error( $term ) ) {
153 $current_lang = Helper::get_term_language( $term );
154 $translations = Helper::get_term_translations( $term );
155 if ( ! $target_lang ) {
156 $target_lang = $current_lang;
157 }
158 // The source-language sibling (if this term is already a translation).
159 if ( isset( $translations[ $source_lang ]['term_id'] ) ) {
160 $translation_of = (int) $translations[ $source_lang ]['term_id'];
161 }
162 }
163 }
164
165 // Candidates only matter when assigning a non-default language.
166 $candidates = ( $target_lang && $target_lang !== $default_lang )
167 ? Helper::get_translation_candidates( $taxonomy, $target_lang, $source_lang )
168 : [];
169
170 return rest_ensure_response( [
171 'enabled' => true,
172 'default_lang' => $default_lang,
173 'current_lang' => $current_lang,
174 'translation_of' => $translation_of,
175 'translations' => $translations,
176 'candidates' => $candidates,
177 ] );
178 }
179
180 /**
181 * Set a term's language and link it into the chosen translation group.
182 */
183 public function set_term_language( WP_REST_Request $request ) {
184 $taxonomy = sanitize_key( (string) $request->get_param( 'taxonomy' ) );
185 $term_id = absint( $request->get_param( 'term_id' ) );
186 $lang = sanitize_text_field( (string) $request->get_param( 'lang' ) );
187 $source = absint( $request->get_param( 'translation_of' ) );
188
189 if ( ! in_array( $taxonomy, $this->allowed_taxonomies(), true ) || ! $term_id || ! Helper::is_multilingual_active() ) {
190 return rest_ensure_response( [ 'success' => false ] );
191 }
192
193 $term = get_term( $term_id, $taxonomy );
194 if ( ! $term || is_wp_error( $term ) ) {
195 return rest_ensure_response( [ 'success' => false ] );
196 }
197
198 Helper::link_term_translation( $term, $lang, $source );
199
200 return rest_ensure_response( [ 'success' => true, 'lang' => $lang ] );
201 }
202 }
203