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