| @@ -32,8 +32,101 @@ | ||
| 32 | 32 | |
| 33 | 33 | public function register() { |
| 34 | 34 | $this->get( 'term-translations', [ $this, 'get_term_translations' ] ); |
| 35 | 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; | |
| 36 | 129 | } |
| 37 | 130 | |
| 38 | 131 | /** |
| 39 | 132 | * Translation group + "this is a translation of" candidates for a term (or for |