PluginProbe
Polylang / 3.7.7
Polylang v3.7.7
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
← All changes | include/translated-object.php +476 -182 2.73.7.7 View file →
@@ -1,303 +1,395 @@
1 1 <?php
2 +/**
3 + * @package Polylang
4 + */
2 5
6 +use WP_Syntex\Polylang\Options\Options;
7 +
8 +defined( 'ABSPATH' ) || exit;
9 +
3 10 /**
4 - * Setups the objects languages and translations model
11 + * Abstract class to use for object types that support translations.
5 12 *
6 13 * @since 1.8
7 14 */
8 -abstract class PLL_Translated_Object {
9 - public $model;
10 - protected $object_type, $type, $tax_language, $tax_translations, $tax_tt;
15 +abstract class PLL_Translated_Object extends PLL_Translatable_Object {
11 16
12 17 /**
13 - * Constructor
18 + * Taxonomy name for the translation groups.
14 19 *
20 + * @var string
21 + *
22 + * @phpstan-var non-empty-string
23 + */
24 + protected $tax_translations;
25 +
26 + /**
27 + * Constructor.
28 + *
15 29 * @since 1.8
16 30 *
17 - * @param object $model
31 + * @param PLL_Model $model Instance of `PLL_Model`.
18 32 */
19 - public function __construct( &$model ) {
20 - $this->model = &$model;
33 + public function __construct( PLL_Model $model ) {
34 + parent::__construct( $model );
21 35
22 - // register our taxonomies as soon as possible
23 - // this is early registration, not ready for rewrite rules as wp_rewrite will be setup later
24 - $args = array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true );
25 - register_taxonomy( $this->tax_language, $this->object_type, $args );
26 - $args['update_count_callback'] = '_update_generic_term_count'; // count *all* posts to avoid deleting in clean_translations_terms
27 - register_taxonomy( $this->tax_translations, $this->object_type, $args );
36 + $this->tax_to_cache[] = $this->tax_translations;
37 +
38 + /*
39 + * Register our taxonomy as soon as possible.
40 + */
41 + $this->register_translations_taxonomy();
28 42 }
29 43
30 44 /**
31 - * Wrap wp_get_object_terms to cache it and return only one object
32 - * inspired by the function get_the_terms
45 + * Registers the translations taxonomy.
33 46 *
34 - * @since 1.2
47 + * @since 3.7
35 48 *
36 - * @param int $object_id post_id or term_id
37 - * @param string $taxonomy Polylang taxonomy depending if we are looking for a post ( or term ) language ( or translation )
38 - * @return bool|object the term associated to the object in the requested taxonomy if exists, false otherwise
49 + * @return void
39 50 */
40 - public function get_object_term( $object_id, $taxonomy ) {
41 - if ( empty( $object_id ) || is_wp_error( $object_id ) ) {
51 + protected function register_translations_taxonomy(): void {
52 + register_taxonomy(
53 + $this->tax_translations,
54 + (array) $this->object_type,
55 + array(
56 + 'label' => false,
57 + 'public' => false,
58 + 'query_var' => false,
59 + 'rewrite' => false,
60 + '_pll' => true,
61 + 'update_count_callback' => '_update_generic_term_count', // Count *all* objects to correctly detect unused terms.
62 + )
63 + );
64 + }
65 +
66 + /**
67 + * Returns the translations group taxonomy name.
68 + *
69 + * @since 3.4
70 + *
71 + * @return string
72 + *
73 + * @phpstan-return non-empty-string
74 + */
75 + public function get_tax_translations() {
76 + return $this->tax_translations;
77 + }
78 +
79 + /**
80 + * Assigns a language to an object, taking care of the translations group.
81 + *
82 + * @since 3.4
83 + *
84 + * @param int $id Object ID.
85 + * @param PLL_Language|string|int $lang Language to assign to the object.
86 + * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
87 + * the object).
88 + */
89 + public function set_language( $id, $lang ) {
90 + if ( ! parent::set_language( $id, $lang ) ) {
42 91 return false;
43 92 }
44 93
45 - $object_id = (int) $object_id;
46 - $term = get_object_term_cache( $object_id, $taxonomy );
94 + $id = $this->sanitize_int_id( $id );
47 95
48 - if ( false === $term ) {
49 - // query language and translations at the same time
50 - $taxonomies = array( $this->tax_language, $this->tax_translations );
96 + $translations = $this->get_translations( $id );
51 97
52 - // query terms
53 - $terms = array();
54 - foreach ( wp_get_object_terms( $object_id, $taxonomies, array( 'update_term_meta_cache' => false ) ) as $t ) {
55 - $terms[ $t->taxonomy ] = $t;
56 - if ( $t->taxonomy == $taxonomy ) {
57 - $term = $t;
58 - }
59 - }
60 -
61 - // store it the way WP wants it
62 - // set an empty cache if no term found in the taxonomy
63 - foreach ( $taxonomies as $tax ) {
64 - wp_cache_add( $object_id, empty( $terms[ $tax ] ) ? array() : array( $terms[ $tax ] ), $tax . '_relationships' );
65 - }
98 + // Don't create translation groups with only 1 value.
99 + if ( ! empty( $translations ) ) {
100 + // Remove the object's former language from the new translations group before adding the new value.
101 + $translations = array_diff( $translations, array( $id ) );
102 + $this->save_translations( $id, $translations );
66 103 }
67 - else {
68 - $term = reset( $term );
69 - }
70 104
71 - return empty( $term ) ? false : $term;
105 + return true;
72 106 }
73 107
74 108 /**
75 - * Tells whether a translation term must be updated
109 + * Returns a list of object translations, given a `tax_translations` term ID.
76 110 *
77 - * @since 2.3
111 + * @since 3.2
78 112 *
79 - * @param array $id Post id or term id
80 - * @param array $translations An associative array of translations with language code as key and translation id as value
113 + * @param int $term_id A `tax_translations` term ID.
114 + * @return int[] An associative array of translations with language code as key and translation ID as value.
115 + *
116 + * @phpstan-return array<non-empty-string, positive-int>
81 117 */
82 - protected function should_update_translation_group( $id, $translations ) {
83 - // Don't do anything if no translations have been added to the group
84 - $old_translations = $this->get_translations( $id ); // Includes at least $id itself
85 - return count( array_diff_assoc( $translations, $old_translations ) ) > 0;
118 + public function get_translations_from_term_id( $term_id ) {
119 + $term_id = $this->sanitize_int_id( $term_id );
120 +
121 + if ( empty( $term_id ) ) {
122 + return array();
123 + }
124 +
125 + $translations_term = get_term( $term_id, $this->tax_translations );
126 +
127 + if ( ! $translations_term instanceof WP_Term || empty( $translations_term->description ) ) {
128 + return array();
129 + }
130 +
131 + // Lang slugs as array keys, translation IDs as array values.
132 + $translations = maybe_unserialize( $translations_term->description );
133 + $translations = is_array( $translations ) ? $translations : array();
134 +
135 + return $this->validate_translations( $translations, 0, 'display' );
86 136 }
87 137
88 138 /**
89 - * Saves translations for posts or terms
139 + * Saves the object's translations.
90 140 *
91 141 * @since 0.5
92 142 *
93 - * @param int $id Post id or term id
94 - * @param array $translations An associative array of translations with language code as key and translation id as value
143 + * @param int $id Object ID.
144 + * @param int[] $translations An associative array of translations with language code as key and translation ID as value.
145 + * @return int[] An associative array with language codes as key and object IDs as values.
146 + *
147 + * @phpstan-return array<non-empty-string, positive-int>
95 148 */
96 - public function save_translations( $id, $translations ) {
97 - $id = (int) $id;
149 + public function save_translations( $id, array $translations = array() ) {
150 + $id = $this->sanitize_int_id( $id );
98 151
99 - if ( ( $lang = $this->get_language( $id ) ) && isset( $translations ) && is_array( $translations ) ) {
100 - // sanitize the translations array
101 - $translations = array_map( 'intval', $translations );
102 - $translations = array_merge( array( $lang->slug => $id ), $translations ); // make sure this object is in translations
103 - $translations = array_diff( $translations, array( 0 ) ); // don't keep non translated languages
104 - $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); // keep only valid languages slugs as keys
152 + if ( empty( $id ) ) {
153 + return array();
154 + }
105 155
106 - // unlink removed translations
107 - $old_translations = $this->get_translations( $id );
108 - foreach ( array_diff_assoc( $old_translations, $translations ) as $object_id ) {
109 - $this->delete_translation( $object_id );
110 - }
156 + $lang = $this->get_language( $id );
111 157
112 - // Check id we need to create or update the translation group
113 - if ( $this->should_update_translation_group( $id, $translations ) ) {
114 - $terms = wp_get_object_terms( $translations, $this->tax_translations );
115 - $term = reset( $terms );
158 + if ( empty( $lang ) ) {
159 + return array();
160 + }
116 161
117 - // create a new term if necessary
118 - if ( empty( $term ) ) {
119 - wp_insert_term( $group = uniqid( 'pll_' ), $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
120 - }
121 - else {
122 - // take care not to overwrite extra data stored in description field, if any
123 - $d = maybe_unserialize( $term->description );
124 - $d = is_array( $d ) ? array_diff_key( $d, $old_translations ) : array(); // remove old translations
125 - $d = array_merge( $d, $translations ); // add new one
126 - wp_update_term( $group = (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
127 - }
162 + // Sanitize and validate the translations array.
163 + $translations = $this->validate_translations( $translations, $id );
128 164
129 - // link all translations to the new term
130 - foreach ( $translations as $p ) {
131 - wp_set_object_terms( $p, $group, $this->tax_translations );
132 - }
165 + // Unlink removed translations.
166 + $old_translations = $this->get_translations( $id );
133 167
134 - // clean now unused translation groups
135 - foreach ( wp_list_pluck( $terms, 'term_id' ) as $term_id ) {
136 - $term = get_term( $term_id, $this->tax_translations );
137 - if ( empty( $term->count ) ) {
138 - wp_delete_term( $term_id, $this->tax_translations );
139 - }
140 - }
168 + foreach ( array_diff_assoc( $old_translations, $translations ) as $id ) {
169 + $this->delete_translation( $id );
170 + }
171 +
172 + // Check ID we need to create or update the translation group.
173 + if ( ! $this->should_update_translation_group( $id, $translations ) ) {
174 + return $translations;
175 + }
176 +
177 + $terms = wp_get_object_terms( $translations, $this->tax_translations );
178 + $term = is_array( $terms ) && ! empty( $terms ) ? reset( $terms ) : false;
179 +
180 + if ( empty( $term ) ) {
181 + // Create a new term if necessary.
182 + $group = uniqid( 'pll_' );
183 + wp_insert_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
184 + } else {
185 + // Take care not to overwrite extra data stored in the description field, if any.
186 + $group = (int) $term->term_id;
187 + $descr = maybe_unserialize( $term->description );
188 + $descr = is_array( $descr ) ? array_diff_key( $descr, $old_translations ) : array(); // Remove old translations.
189 + $descr = array_merge( $descr, $translations ); // Add new one.
190 + wp_update_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) );
191 + }
192 +
193 + // Link all translations to the new term.
194 + foreach ( $translations as $p ) {
195 + wp_set_object_terms( $p, $group, $this->tax_translations );
196 + }
197 +
198 + if ( ! is_array( $terms ) ) {
199 + return $translations;
200 + }
201 +
202 + // Clean now unused translation groups.
203 + foreach ( $terms as $term ) {
204 + // Get fresh count value.
205 + $term = get_term( $term->term_id, $this->tax_translations );
206 +
207 + if ( $term instanceof WP_Term && empty( $term->count ) ) {
208 + wp_delete_term( $term->term_id, $this->tax_translations );
141 209 }
142 210 }
211 +
212 + return $translations;
143 213 }
144 214
145 215 /**
146 - * Deletes a translation of a post or term
216 + * Deletes a translation of an object.
147 217 *
148 218 * @since 0.5
149 219 *
150 - * @param int $id post id or term id
220 + * @param int $id Object ID.
221 + * @return void
151 222 */
152 223 public function delete_translation( $id ) {
153 - $id = (int) $id;
224 + $id = $this->sanitize_int_id( $id );
225 +
226 + if ( empty( $id ) ) {
227 + return;
228 + }
229 +
154 230 $term = $this->get_object_term( $id, $this->tax_translations );
155 231
156 - if ( ! empty( $term ) ) {
157 - $d = maybe_unserialize( $term->description );
158 - $slug = array_search( $id, $this->get_translations( $id ) ); // in case some plugin stores the same value with different key
159 - unset( $d[ $slug ] );
232 + if ( empty( $term ) ) {
233 + return;
234 + }
160 235
161 - if ( empty( $d ) ) {
162 - wp_delete_term( (int) $term->term_id, $this->tax_translations );
236 + $descr = maybe_unserialize( $term->description );
237 +
238 + if ( ! empty( $descr ) && is_array( $descr ) ) {
239 + $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
240 +
241 + if ( false !== $slug ) {
242 + unset( $descr[ $slug ] );
163 243 }
164 - else {
165 - wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
166 - }
167 244 }
245 +
246 + if ( empty( $descr ) || ! is_array( $descr ) ) {
247 + wp_delete_term( (int) $term->term_id, $this->tax_translations );
248 + } else {
249 + wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) );
250 + }
168 251 }
169 252
170 253 /**
171 - * Returns an array of translations of a post or term
254 + * Returns an array of valid translations of an object.
172 255 *
173 256 * @since 0.5
174 257 *
175 - * @param int $id post id or term id
176 - * @return array an associative array of translations with language code as key and translation id as value
258 + * @param int $id Object ID.
259 + * @return int[] An associative array of translations with language code as key and translation ID as value.
260 + *
261 + * @phpstan-return array<non-empty-string, positive-int>
177 262 */
178 263 public function get_translations( $id ) {
264 + $id = $this->sanitize_int_id( $id );
265 +
266 + if ( empty( $id ) ) {
267 + return array();
268 + }
269 +
270 + $translations = $this->get_raw_translations( $id );
271 +
272 + return $this->validate_translations( $translations, $id, 'display' );
273 + }
274 +
275 + /**
276 + * Returns an unvalidated array of translations of an object.
277 + * It is generally preferable to use `get_translations()`.
278 + *
279 + * @since 3.4
280 + *
281 + * @param int $id Object ID.
282 + * @return int[] An associative array of translations with language code as key and translation ID as value.
283 + *
284 + * @phpstan-return array<non-empty-string, positive-int>
285 + */
286 + public function get_raw_translations( $id ) {
287 + $id = $this->sanitize_int_id( $id );
288 +
289 + if ( empty( $id ) ) {
290 + return array();
291 + }
292 +
179 293 $term = $this->get_object_term( $id, $this->tax_translations );
180 - $translations = empty( $term ) ? array() : maybe_unserialize( $term->description );
181 294
182 - // make sure we return only translations ( thus we allow plugins to store other information in the array )
183 - if ( is_array( $translations ) ) {
184 - $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) );
295 + if ( empty( $term->description ) ) {
296 + return array();
185 297 }
186 298
187 - // make sure to return at least the passed post or term in its translation array
188 - if ( empty( $translations ) && $lang = $this->get_language( $id ) ) {
189 - $translations = array( $lang->slug => $id );
190 - }
299 + $translations = maybe_unserialize( $term->description );
300 + $translations = is_array( $translations ) ? $translations : array();
191 301
192 302 return $translations;
193 303 }
194 304
195 305 /**
196 - * Returns the id of the translation of a post or term
306 + * Returns the ID of the translation of an object.
197 307 *
198 308 * @since 0.5
199 309 *
200 - * @param int $id post id or term id
201 - * @param object|string $lang object or slug
202 - * @return bool|int post id or term id of the translation, false if there is none
310 + * @param int $id Object ID.
311 + * @param PLL_Language|string $lang Language (slug or object).
312 + * @return int Object ID of the translation, `0` if there is none.
313 + *
314 + * @phpstan-return int<0, max>
203 315 */
204 316 public function get_translation( $id, $lang ) {
205 - if ( ! $lang = $this->model->get_language( $lang ) ) {
206 - return false;
317 + $lang = $this->languages->get( $lang );
318 +
319 + if ( empty( $lang ) ) {
320 + return 0;
207 321 }
208 322
209 323 $translations = $this->get_translations( $id );
210 324
211 - return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
325 + return $translations[ $lang->slug ] ?? 0;
212 326 }
213 327
214 328 /**
215 - * Among the object and its translations, returns the id of the object which is in $lang
329 + * Among the object and its translations, returns the ID of the object which is in `$lang`.
216 330 *
217 331 * @since 0.1
332 + * @since 3.4 Returns `0` instead of `false`.
218 333 *
219 - * @param int $id post id or term id
220 - * @param int|string|object $lang language ( term_id or slug or object )
221 - * @return bool|int the translation post id or term id if exists, otherwise the post id or term id, false if the post has no language
334 + * @param int $id Object ID.
335 + * @param PLL_Language|string|int $lang Language (object, slug, or term ID).
336 + * @return int The translation object ID if exists. `0` if the passed object has no language or if not translated.
337 + *
338 + * @phpstan-return int<0, max>
222 339 */
223 340 public function get( $id, $lang ) {
224 - $id = (int) $id;
225 - $obj_lang = $this->get_language( $id ); // FIXME is this necessary?
226 - if ( ! $lang || ! $obj_lang ) {
227 - return false;
341 + $id = $this->sanitize_int_id( $id );
342 +
343 + if ( empty( $id ) ) {
344 + return 0;
228 345 }
229 346
230 - $lang = $this->model->get_language( $lang );
231 - return $obj_lang->term_id == $lang->term_id ? $id : $this->get_translation( $id, $lang );
232 - }
347 + $lang = $this->languages->get( $lang );
233 348
234 - /**
235 - * A where clause to add to sql queries when filtering by language is needed directly in query
236 - *
237 - * @since 1.2
238 - *
239 - * @param object|array|string $lang a PLL_Language object or a comma separated list of language slug or an array of language slugs
240 - * @return string where clause
241 - */
242 - public function where_clause( $lang ) {
243 - $tt_id = $this->tax_tt;
349 + if ( empty( $lang ) ) {
350 + return 0;
351 + }
244 352
245 - // $lang is an object
246 - // generally the case if the query is coming from Polylang
247 - if ( is_object( $lang ) ) {
248 - return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
249 - }
353 + $obj_lang = $this->get_language( $id );
250 354
251 - // $lang is a comma separated list of slugs ( or an array of slugs )
252 - // generally the case is the query is coming from outside with 'lang' parameter
253 - $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
254 - $languages = array();
255 - foreach ( $slugs as $slug ) {
256 - $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
355 + if ( empty( $obj_lang ) ) {
356 + return 0;
257 357 }
258 358
259 - return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
359 + return $obj_lang->term_id === $lang->term_id ? $id : $this->get_translation( $id, $lang );
260 360 }
261 361
262 362 /**
263 - * Returns ids of objects in a language similarly to get_objects_in_term for a taxonomy
264 - * faster than get_objects_in_term as it avoids a JOIN
363 + * Checks if a user can synchronize translations.
265 364 *
266 - * @since 1.4
267 - *
268 - * @param object $lang a PLL_Language object
269 - * @return array
270 - */
271 - public function get_objects_in_language( $lang ) {
272 - global $wpdb;
273 - $tt_id = $this->tax_tt;
274 - return $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $lang->$tt_id ) );
275 - }
276 -
277 - /**
278 - * Check if a user can synchronize translations
279 - *
280 365 * @since 2.6
281 366 *
282 - * @param int $id Object id
367 + * @param int $id Object ID.
283 368 * @return bool
284 369 */
285 370 public function current_user_can_synchronize( $id ) {
371 + $id = $this->sanitize_int_id( $id );
372 +
373 + if ( empty( $id ) ) {
374 + return false;
375 + }
376 +
286 377 /**
287 - * Filters whether a synchronization capability check should take place
378 + * Filters whether a synchronization capability check should take place.
288 379 *
289 380 * @since 2.6
290 381 *
291 - * @param $check null to enable the capability check,
292 - * true to always allow the synchronization,
293 - * false to always disallow the synchronization.
294 - * Defaults to true.
295 - * @param $id The synchronization source object id
382 + * @param bool|null $check Null to enable the capability check,
383 + * true to always allow the synchronization,
384 + * false to always disallow the synchronization.
385 + * Defaults to true.
386 + * @param int $id The synchronization source object ID.
296 387 */
297 388 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
389 +
298 390 if ( null !== $check ) {
299 - return $check;
391 + return (bool) $check;
300 392 }
301 393
302 394 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
303 395 return false;
@@ -309,6 +401,208 @@
309 401 }
310 402 }
311 403
312 404 return true;
405 + }
406 +
407 + /**
408 + * Tells whether a translation term must be updated.
409 + *
410 + * @since 2.3
411 + *
412 + * @param int $id Object ID.
413 + * @param int[] $translations An associative array of translations with language code as key and translation ID as
414 + * value. Make sure to sanitize this.
415 + * @return bool
416 + *
417 + * @phpstan-param array<non-empty-string, positive-int> $translations
418 + */
419 + protected function should_update_translation_group( $id, $translations ) {
420 + // Don't do anything if no translations have been added to the group.
421 + $old_translations = $this->get_translations( $id ); // Includes at least $id itself.
422 + return ! empty( array_diff_assoc( $translations, $old_translations ) );
423 + }
424 +
425 + /**
426 + * Validates and sanitizes translations.
427 + * This will:
428 + * - Make sure to return only translations in existing languages (and only translations).
429 + * - Sanitize the values.
430 + * - Make sure the provided translation (`$id`) is in the list.
431 + * - Check that the translated objects are in the right language, if `$context` is set to 'save'.
432 + *
433 + * @since 3.1
434 + * @since 3.2 Doesn't return `0` ID values.
435 + * @since 3.2 Added parameters `$id` and `$context`.
436 + *
437 + * @param int[] $translations An associative array of translations with language code as key and translation ID as
438 + * value.
439 + * @param int $id Optional. The object ID for which the translations are validated. When provided, the
440 + * process makes sure it is added to the list. Default 0.
441 + * @param string $context Optional. The operation for which the translations are validated. When set to
442 + * 'save', a check is done to verify that the IDs and langs correspond.
443 + * 'display' should be used otherwise. Default 'save'.
444 + * @return int[]
445 + *
446 + * @phpstan-param non-empty-string $context
447 + * @phpstan-return array<non-empty-string, positive-int>
448 + */
449 + protected function validate_translations( $translations, $id = 0, $context = 'save' ) {
450 + if ( ! is_array( $translations ) ) {
451 + $translations = array();
452 + }
453 +
454 + /**
455 + * Remove translations in non-existing languages, and non-translation data (we allow plugins to store other
456 + * information in the array).
457 + */
458 + $translations = array_intersect_key(
459 + $translations,
460 + array_flip( $this->languages->get_list( array( 'fields' => 'slug' ) ) )
461 + );
462 +
463 + // Make sure values are clean before working with them.
464 + /** @phpstan-var array<non-empty-string, positive-int> $translations */
465 + $translations = $this->sanitize_int_ids_list( $translations );
466 +
467 + if ( 'save' === $context ) {
468 + /**
469 + * Check that the translated objects are in the right language.
470 + * For better performance, this should be done only when saving the data into the database, not when
471 + * retrieving data from it.
472 + */
473 + $valid_translations = array();
474 +
475 + foreach ( $translations as $lang_slug => $tr_id ) {
476 + $tr_lang = $this->get_language( $tr_id );
477 +
478 + if ( ! empty( $tr_lang ) && $tr_lang->slug === $lang_slug ) {
479 + $valid_translations[ $lang_slug ] = $tr_id;
480 + }
481 + }
482 +
483 + $translations = $valid_translations;
484 + }
485 +
486 + $id = $this->sanitize_int_id( $id );
487 +
488 + if ( empty( $id ) ) {
489 + return $translations;
490 + }
491 +
492 + // Make sure to return at least the passed object in its translation array.
493 + $lang = $this->get_language( $id );
494 +
495 + if ( empty( $lang ) ) {
496 + return $translations;
497 + }
498 +
499 + /** @phpstan-var array<non-empty-string, positive-int> $translations */
500 + return array_merge( array( $lang->slug => $id ), $translations );
501 + }
502 + /**
503 + * Creates translations groups in mass.
504 + *
505 + * @since 1.6.3
506 + * @since 3.4 Moved from PLL_Admin_Model class.
507 + *
508 + * @param int[][] $translations Array of translations arrays.
509 + * @return void
510 + *
511 + * @phpstan-param array<array<string,int>> $translations
512 + */
513 + public function set_translation_in_mass( $translations ) {
514 + global $wpdb;
515 +
516 + $terms = array();
517 + $slugs = array();
518 + $description = array();
519 + $count = array();
520 +
521 + foreach ( $translations as $t ) {
522 + $term = uniqid( 'pll_' ); // The term name.
523 + $terms[] = array( $term, $term );
524 + $slugs[] = $term;
525 + $description[ $term ] = maybe_serialize( $t );
526 + $count[ $term ] = count( $t );
527 + }
528 +
529 + // Insert terms.
530 + if ( ! empty( $terms ) ) {
531 + $wpdb->query(
532 + $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
533 + sprintf(
534 + "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES %s",
535 + implode( ',', array_fill( 0, count( $terms ), '( %s, %s )' ) )
536 + ),
537 + array_merge( ...$terms )
538 + )
539 + );
540 + }
541 +
542 + // Get all terms with their term_id.
543 + $terms = $wpdb->get_results(
544 + $wpdb->prepare(
545 + sprintf(
546 + "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN (%s)",
547 + implode( ',', array_fill( 0, count( $slugs ), '%s' ) )
548 + ),
549 + $slugs
550 + )
551 + );
552 +
553 + $term_ids = array();
554 + $tts = array();
555 +
556 + // Prepare terms taxonomy relationship.
557 + foreach ( $terms as $term ) {
558 + $term_ids[] = $term->term_id;
559 + $tts[] = array( $term->term_id, $this->tax_translations, $description[ $term->slug ], $count[ $term->slug ] );
560 + }
561 +
562 + // Insert term_taxonomy.
563 + if ( ! empty( $tts ) ) {
564 + $wpdb->query(
565 + $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
566 + sprintf(
567 + "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES %s",
568 + implode( ',', array_fill( 0, count( $tts ), '( %d, %s, %s, %d )' ) )
569 + ),
570 + array_merge( ...$tts )
571 + )
572 + );
573 + }
574 +
575 + // Get all terms with term_taxonomy_id.
576 + $terms = get_terms( array( 'taxonomy' => $this->tax_translations, 'hide_empty' => false ) );
577 + $trs = array();
578 +
579 + // Prepare objects relationships.
580 + if ( is_array( $terms ) ) {
581 + foreach ( $terms as $term ) {
582 + $t = maybe_unserialize( $term->description );
583 + if ( is_array( $t ) && in_array( $t, $translations ) ) {
584 + foreach ( $t as $object_id ) {
585 + if ( ! empty( $object_id ) ) {
586 + $trs[] = array( $object_id, $term->term_taxonomy_id );
587 + }
588 + }
589 + }
590 + }
591 + }
592 +
593 + // Insert term_relationships.
594 + if ( ! empty( $trs ) ) {
595 + $wpdb->query(
596 + $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
597 + sprintf(
598 + "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES %s",
599 + implode( ',', array_fill( 0, count( $trs ), '( %d, %d )' ) )
600 + ),
601 + array_merge( ...$trs )
602 + )
603 + );
604 + }
605 +
606 + clean_term_cache( $term_ids, $this->tax_translations );
313 607 }
314 608 }