PluginProbe
Polylang / 3.7.1
Polylang v3.7.1
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 +441 -251 3.0.33.7.1 View file →
@@ -2,392 +2,379 @@
2 2 /**
3 3 * @package Polylang
4 4 */
5 5
6 +use WP_Syntex\Polylang\Options\Options;
7 +
8 +defined( 'ABSPATH' ) || exit;
9 +
6 10 /**
7 - * Setups the objects languages and translations model.
11 + * Abstract class to use for object types that support translations.
8 12 *
9 13 * @since 1.8
10 14 */
11 -abstract class PLL_Translated_Object {
12 - /**
13 - * @var PLL_Model
14 - */
15 - public $model;
15 +abstract class PLL_Translated_Object extends PLL_Translatable_Object {
16 16
17 17 /**
18 - * Object type to use when registering the taxonomies.
19 - * Left empty for posts.
18 + * Taxonomy name for the translation groups.
20 19 *
21 - * @var string|null
22 - */
23 - protected $object_type;
24 -
25 - /**
26 - * Object type to use when checking capabilities.
27 - *
28 20 * @var string
29 - */
30 - protected $type;
31 -
32 - /**
33 - * Taxonomy name for the languages.
34 21 *
35 - * @var string
22 + * @phpstan-var non-empty-string
36 23 */
37 - protected $tax_language;
38 -
39 - /**
40 - * Taxonomy name for the translation groups.
41 - *
42 - * @var string
43 - */
44 24 protected $tax_translations;
45 25
46 26 /**
47 - * PLL_Language property name for the term_taxonomy id.
48 - *
49 - * @var string
50 - */
51 - protected $tax_tt;
52 -
53 - /**
54 27 * Constructor.
55 28 *
56 29 * @since 1.8
57 30 *
58 - * @param PLL_Model $model Instance of PLL_Model.
31 + * @param PLL_Model $model Instance of `PLL_Model`.
59 32 */
60 - public function __construct( &$model ) {
61 - $this->model = &$model;
33 + public function __construct( PLL_Model $model ) {
34 + parent::__construct( $model );
62 35
36 + $this->tax_to_cache[] = $this->tax_translations;
37 +
63 38 /*
64 - * Register our taxonomies as soon as possible.
65 - * This is early registration, not ready for rewrite rules as $wp_rewrite will be setup later.
39 + * Register our taxonomy as soon as possible.
66 40 */
67 - $args = array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true );
68 - register_taxonomy( $this->tax_language, $this->object_type, $args );
69 - $args['update_count_callback'] = '_update_generic_term_count'; // Count *all* objects to avoid deleting in clean_translations_terms.
70 - register_taxonomy( $this->tax_translations, $this->object_type, $args );
41 + $this->register_translations_taxonomy();
71 42 }
72 43
73 44 /**
74 - * Stores the language in the database.
45 + * Registers the translations taxonomy.
75 46 *
76 - * @since 0.6
47 + * @since 3.7
77 48 *
78 - * @param int $id Object id.
79 - * @param int|string|PLL_Language $lang Language (term_id or slug or object).
80 49 * @return void
81 50 */
82 - abstract public function set_language( $id, $lang );
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 + }
83 65
84 66 /**
85 - * Returns the language of an object.
67 + * Returns the translations group taxonomy name.
86 68 *
87 - * @since 0.1
69 + * @since 3.4
88 70 *
89 - * @param int $id Object id.
90 - * @return PLL_Language|false PLL_Language object, false if no language is associated to that object.
71 + * @return string
72 + *
73 + * @phpstan-return non-empty-string
91 74 */
92 - abstract public function get_language( $id );
75 + public function get_tax_translations() {
76 + return $this->tax_translations;
77 + }
93 78
94 79 /**
95 - * Wrap wp_get_object_terms() to cache it and return only one object.
96 - * inspired by the WordPress function get_the_terms().
80 + * Assigns a language to an object, taking care of the translations group.
97 81 *
98 - * @since 1.2
82 + * @since 3.4
99 83 *
100 - * @param int $object_id Object id ( typically a post_id or term_id ).
101 - * @param string $taxonomy Polylang taxonomy depending if we are looking for a post ( or term ) language ( or translation ).
102 - * @return WP_Term|false The term associated to the object in the requested taxonomy if it exists, false otherwise.
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).
103 88 */
104 - public function get_object_term( $object_id, $taxonomy ) {
105 - if ( empty( $object_id ) || is_wp_error( $object_id ) ) {
89 + public function set_language( $id, $lang ) {
90 + if ( ! parent::set_language( $id, $lang ) ) {
106 91 return false;
107 92 }
108 93
109 - $object_id = (int) $object_id;
94 + $id = $this->sanitize_int_id( $id );
110 95
111 - if ( $object_id < 0 ) {
112 - return false;
113 - }
96 + $translations = $this->get_translations( $id );
114 97
115 - $term = get_object_term_cache( $object_id, $taxonomy );
116 -
117 - if ( false === $term ) {
118 - // Query language and translations at the same time.
119 - $taxonomies = array( $this->tax_language, $this->tax_translations );
120 -
121 - // Query terms.
122 - $terms = array();
123 - $object_terms = wp_get_object_terms( $object_id, $taxonomies, array( 'update_term_meta_cache' => false ) );
124 - if ( is_array( $object_terms ) ) {
125 - foreach ( $object_terms as $t ) {
126 - $terms[ $t->taxonomy ] = $t;
127 - if ( $t->taxonomy == $taxonomy ) {
128 - $term = $t;
129 - }
130 - }
131 - }
132 -
133 - // Stores it the way WP expects it. Set an empty cache if no term was found in the taxonomy.
134 - foreach ( $taxonomies as $tax ) {
135 - wp_cache_add( $object_id, empty( $terms[ $tax ] ) ? array() : array( $terms[ $tax ] ), $tax . '_relationships' );
136 - }
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 );
137 103 }
138 - else {
139 - $term = reset( $term );
140 - }
141 104
142 - return empty( $term ) ? false : $term;
105 + return true;
143 106 }
144 107
145 108 /**
146 - * Tells whether a translation term must be updated.
109 + * Returns a list of object translations, given a `tax_translations` term ID.
147 110 *
148 - * @since 2.3
111 + * @since 3.2
149 112 *
150 - * @param int $id Object id ( typically a post_id or term_id ).
151 - * @param int[] $translations An associative array of translations with language code as key and translation id as value.
152 - * @return bool
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>
153 117 */
154 - protected function should_update_translation_group( $id, $translations ) {
155 - // Don't do anything if no translations have been added to the group.
156 - $old_translations = $this->get_translations( $id ); // Includes at least $id itself.
157 - 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' );
158 136 }
159 137
160 138 /**
161 - * Saves translations for posts or terms.
139 + * Saves the object's translations.
162 140 *
163 141 * @since 0.5
164 142 *
165 - * @param int $id Object id ( typically a post_id or term_id ).
166 - * @param int[] $translations An associative array of translations with language code as key and translation id as value.
167 - * @return void
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>
168 148 */
169 - public function save_translations( $id, $translations ) {
170 - $id = (int) $id;
149 + public function save_translations( $id, array $translations = array() ) {
150 + $id = $this->sanitize_int_id( $id );
171 151
172 - if ( ( $lang = $this->get_language( $id ) ) && is_array( $translations ) ) {
173 - // Sanitize the translations array.
174 - $translations = array_map( 'intval', $translations );
175 - $translations = array_merge( array( $lang->slug => $id ), $translations ); // Make sure this object is in translations.
176 - $translations = array_diff( $translations, array( 0 ) ); // Don't keep non translated languages.
177 - $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 + }
178 155
179 - // Unlink removed translations.
180 - $old_translations = $this->get_translations( $id );
181 - foreach ( array_diff_assoc( $old_translations, $translations ) as $object_id ) {
182 - $this->delete_translation( $object_id );
183 - }
156 + $lang = $this->get_language( $id );
184 157
185 - // Check id we need to create or update the translation group.
186 - if ( $this->should_update_translation_group( $id, $translations ) ) {
187 - $terms = wp_get_object_terms( $translations, $this->tax_translations );
188 - $term = reset( $terms );
158 + if ( empty( $lang ) ) {
159 + return array();
160 + }
189 161
190 - // Create a new term if necessary.
191 - if ( empty( $term ) ) {
192 - wp_insert_term( $group = uniqid( 'pll_' ), $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
193 - } else {
194 - // Take care not to overwrite extra data stored in the description field, if any.
195 - $d = maybe_unserialize( $term->description );
196 - $d = is_array( $d ) ? array_diff_key( $d, $old_translations ) : array(); // Remove old translations.
197 - $d = array_merge( $d, $translations ); // Add new one.
198 - wp_update_term( $group = (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
199 - }
162 + // Sanitize and validate the translations array.
163 + $translations = $this->validate_translations( $translations, $id );
200 164
201 - // Link all translations to the new term.
202 - foreach ( $translations as $p ) {
203 - wp_set_object_terms( $p, $group, $this->tax_translations );
204 - }
165 + // Unlink removed translations.
166 + $old_translations = $this->get_translations( $id );
205 167
206 - // Clean now unused translation groups.
207 - foreach ( wp_list_pluck( $terms, 'term_id' ) as $term_id ) {
208 - $term = get_term( $term_id, $this->tax_translations );
209 - if ( empty( $term->count ) ) {
210 - wp_delete_term( $term_id, $this->tax_translations );
211 - }
212 - }
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 );
213 209 }
214 210 }
211 +
212 + return $translations;
215 213 }
216 214
217 215 /**
218 - * Deletes a translation of a post or term.
216 + * Deletes a translation of an object.
219 217 *
220 218 * @since 0.5
221 219 *
222 - * @param int $id Object id ( typically a post_id or term_id ).
220 + * @param int $id Object ID.
223 221 * @return void
224 222 */
225 223 public function delete_translation( $id ) {
226 - $id = (int) $id;
224 + $id = $this->sanitize_int_id( $id );
225 +
226 + if ( empty( $id ) ) {
227 + return;
228 + }
229 +
227 230 $term = $this->get_object_term( $id, $this->tax_translations );
228 231
229 - if ( ! empty( $term ) ) {
230 - $d = maybe_unserialize( $term->description );
231 - if ( is_array( $d ) ) {
232 - $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
233 - unset( $d[ $slug ] );
232 + if ( empty( $term ) ) {
233 + return;
234 + }
235 +
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 ] );
234 243 }
244 + }
235 245
236 - if ( empty( $d ) ) {
237 - wp_delete_term( (int) $term->term_id, $this->tax_translations );
238 - } else {
239 - wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
240 - }
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 ) ) );
241 250 }
242 251 }
243 252
244 253 /**
245 - * Returns an array of translations of a post or term.
254 + * Returns an array of valid translations of an object.
246 255 *
247 256 * @since 0.5
248 257 *
249 - * @param int $id Object id ( typically a post_id or term_id ).
250 - * @return int[] 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>
251 262 */
252 263 public function get_translations( $id ) {
253 - $term = $this->get_object_term( $id, $this->tax_translations );
254 - $translations = empty( $term ) ? array() : maybe_unserialize( $term->description );
264 + $id = $this->sanitize_int_id( $id );
255 265
256 - // Make sure we return only translations ( thus we allow plugins to store other information in the array ).
257 - if ( is_array( $translations ) ) {
258 - $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) );
266 + if ( empty( $id ) ) {
267 + return array();
259 268 }
260 269
261 - // Make sure to return at least the passed object in its translation array.
262 - if ( empty( $translations ) && $lang = $this->get_language( $id ) ) {
263 - $translations = array( $lang->slug => $id );
264 - }
270 + $translations = $this->get_raw_translations( $id );
265 271
266 - return $translations;
272 + return $this->validate_translations( $translations, $id, 'display' );
267 273 }
268 274
269 275 /**
270 - * Returns the id of the translation of a post or term.
276 + * Returns an unvalidated array of translations of an object.
277 + * It is generally preferable to use `get_translations()`.
271 278 *
272 - * @since 0.5
279 + * @since 3.4
273 280 *
274 - * @param int $id Object id ( typically a post_id or term_id ).
275 - * @param PLL_Language|string $lang Language ( slug or object ).
276 - * @return int|false Object id of the translation, false if there is none.
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>
277 285 */
278 - public function get_translation( $id, $lang ) {
279 - if ( ! $lang = $this->model->get_language( $lang ) ) {
280 - return false;
286 + public function get_raw_translations( $id ) {
287 + $id = $this->sanitize_int_id( $id );
288 +
289 + if ( empty( $id ) ) {
290 + return array();
281 291 }
282 292
283 - $translations = $this->get_translations( $id );
293 + $term = $this->get_object_term( $id, $this->tax_translations );
284 294
285 - return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
286 - }
295 + if ( empty( $term->description ) ) {
296 + return array();
297 + }
287 298
288 - /**
289 - * Among the object and its translations, returns the id of the object which is in $lang
290 - *
291 - * @since 0.1
292 - *
293 - * @param int $id Object id ( typically a post_id or term_id ).
294 - * @param int|string|PLL_Language $lang Language ( term_id or slug or object ).
295 - * @return int|false The translation object id if exists, otherwise the passed id, false if the passed object has no language.
296 - */
297 - public function get( $id, $lang ) {
298 - $id = (int) $id;
299 - $lang = $this->model->get_language( $lang );
300 - $obj_lang = $this->get_language( $id );
301 - if ( empty( $lang ) || empty( $obj_lang ) ) {
302 - return false;
303 - }
299 + $translations = maybe_unserialize( $term->description );
300 + $translations = is_array( $translations ) ? $translations : array();
304 301
305 - return $obj_lang->term_id == $lang->term_id ? $id : $this->get_translation( $id, $lang );
302 + return $translations;
306 303 }
307 304
308 305 /**
309 - * A join clause to add to sql queries when filtering by language is needed directly in query.
306 + * Returns the ID of the translation of an object.
310 307 *
311 - * @since 1.2
308 + * @since 0.5
312 309 *
313 - * @param string $alias Optional alias for object table.
314 - * @return string Join clause.
315 - */
316 - abstract public function join_clause( $alias = '' );
317 -
318 - /**
319 - * A where clause to add to sql queries when filtering by language is needed directly in query.
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.
320 313 *
321 - * @since 1.2
322 - *
323 - * @param PLL_Language|string|string[] $lang PLL_Language object or a comma separated list of language slug or an array of language slugs.
324 - * @return string Where clause.
314 + * @phpstan-return int<0, max>
325 315 */
326 - public function where_clause( $lang ) {
327 - $tt_id = $this->tax_tt;
316 + public function get_translation( $id, $lang ) {
317 + $lang = $this->languages->get( $lang );
328 318
329 - /*
330 - * $lang is an object.
331 - * This is generally the case if the query is coming from Polylang.
332 - */
333 - if ( is_object( $lang ) ) {
334 - return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
319 + if ( empty( $lang ) ) {
320 + return 0;
335 321 }
336 322
337 - /*
338 - * $lang is a comma separated list of slugs ( or an array of slugs ).
339 - * This is generally the case is the query is coming from outside with a 'lang' parameter.
340 - */
341 - $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
342 - $languages = array();
343 - foreach ( $slugs as $slug ) {
344 - $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
345 - }
323 + $translations = $this->get_translations( $id );
346 324
347 - return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
325 + return $translations[ $lang->slug ] ?? 0;
348 326 }
349 327
350 328 /**
351 - * Returns ids of objects in a language similarly to get_objects_in_term() for a taxonomy.
352 - * It is faster than get_objects_in_term() as it avoids a JOIN.
329 + * Among the object and its translations, returns the ID of the object which is in `$lang`.
353 330 *
354 - * @since 1.4
331 + * @since 0.1
332 + * @since 3.4 Returns `0` instead of `false`.
355 333 *
356 - * @param PLL_Language $lang PLL_Language object.
357 - * @return int[] Object ids.
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>
358 339 */
359 - public function get_objects_in_language( $lang ) {
360 - global $wpdb;
361 - $tt_id = $this->tax_tt;
340 + public function get( $id, $lang ) {
341 + $id = $this->sanitize_int_id( $id );
362 342
363 - $last_changed = wp_cache_get_last_changed( 'terms' );
364 - $cache_key = "polylang:get_objects_in_language:{$lang->$tt_id}:{$last_changed}";
365 - $cache = wp_cache_get( $cache_key, 'terms' );
343 + if ( empty( $id ) ) {
344 + return 0;
345 + }
366 346
367 - if ( false === $cache ) {
368 - $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $lang->$tt_id ) );
369 - wp_cache_set( $cache_key, $object_ids, 'terms' );
370 - } else {
371 - $object_ids = (array) $cache;
347 + $lang = $this->languages->get( $lang );
348 +
349 + if ( empty( $lang ) ) {
350 + return 0;
372 351 }
373 352
374 - if ( ! $object_ids ) {
375 - return array();
353 + $obj_lang = $this->get_language( $id );
354 +
355 + if ( empty( $obj_lang ) ) {
356 + return 0;
376 357 }
377 358
378 - return $object_ids;
359 + return $obj_lang->term_id === $lang->term_id ? $id : $this->get_translation( $id, $lang );
379 360 }
380 361
381 362 /**
382 - * Check if a user can synchronize translations.
363 + * Checks if a user can synchronize translations.
383 364 *
384 365 * @since 2.6
385 366 *
386 - * @param int $id Object id.
367 + * @param int $id Object ID.
387 368 * @return bool
388 369 */
389 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 +
390 377 /**
391 378 * Filters whether a synchronization capability check should take place.
392 379 *
393 380 * @since 2.6
@@ -395,13 +382,14 @@
395 382 * @param bool|null $check Null to enable the capability check,
396 383 * true to always allow the synchronization,
397 384 * false to always disallow the synchronization.
398 385 * Defaults to true.
399 - * @param int $id The synchronization source object id.
386 + * @param int $id The synchronization source object ID.
400 387 */
401 388 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
389 +
402 390 if ( null !== $check ) {
403 - return $check;
391 + return (bool) $check;
404 392 }
405 393
406 394 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
407 395 return false;
@@ -413,6 +401,208 @@
413 401 }
414 402 }
415 403
416 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 );
417 607 }
418 608 }