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