PluginProbe
Polylang / 3.2.2
Polylang v3.2.2
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
polylang / include / translated-object.php

translated-object.php in Polylang 3.2.2, at include/translated-object.php

590 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 /**
7 * Setups the objects languages and translations model.
8 *
9 * @since 1.8
10 */
11 abstract class PLL_Translated_Object {
12 /**
13 * @var PLL_Model
14 */
15 public $model;
16
17 /**
18 * Object type to use when registering the taxonomies.
19 * Left empty for posts.
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 *
56 * @since 1.8
57 *
58 * @param PLL_Model $model Instance of PLL_Model.
59 */
60 public function __construct( &$model ) {
61 $this->model = &$model;
62
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 */
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 );
71 }
72
73 /**
74 * Stores the language in the database.
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 *
125 * @since 1.2
126 *
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.
130 */
131 public function get_object_term( $object_id, $taxonomy ) {
132 $object_id = $this->sanitize_int_id( $object_id );
133
134 if ( empty( $object_id ) ) {
135 return false;
136 }
137
138 $term = get_object_term_cache( $object_id, $taxonomy );
139
140 if ( is_array( $term ) ) {
141 return ! empty( $term ) ? reset( $term ) : false;
142 }
143
144 // Query language and translations at the same time.
145 $taxonomies = array( $this->tax_language, $this->tax_translations );
146
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 ) );
151
152 if ( is_array( $object_terms ) ) {
153 foreach ( $object_terms as $t ) {
154 $terms[ $t->taxonomy ] = $t;
155 if ( $t->taxonomy === $taxonomy ) {
156 $term = $t;
157 }
158 }
159 }
160
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' );
164 }
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 array<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();
182 }
183
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' );
194 }
195
196 /**
197 * Tells whether a translation term must be updated.
198 *
199 * @since 2.3
200 *
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
205 */
206 protected function should_update_translation_group( $id, $translations ) {
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 ) );
210 }
211
212 /**
213 * Saves translations for posts or terms.
214 *
215 * @since 0.5
216 *
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.
220 */
221 public function save_translations( $id, $translations ) {
222 $id = $this->sanitize_int_id( $id );
223
224 if ( empty( $id ) ) {
225 return array();
226 }
227
228 $lang = $this->get_language( $id );
229
230 if ( empty( $lang ) ) {
231 return array();
232 }
233
234 // Sanitize and validate the translations array.
235 $translations = $this->validate_translations( $translations, $id );
236
237 // Unlink removed translations.
238 $old_translations = $this->get_translations( $id );
239
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 );
281 }
282 }
283
284 return $translations;
285 }
286
287 /**
288 * Deletes a translation of a post or term.
289 *
290 * @since 0.5
291 *
292 * @param int $id Object id ( typically a post_id or term_id ).
293 * @return void
294 */
295 public function delete_translation( $id ) {
296 $id = $this->sanitize_int_id( $id );
297
298 if ( empty( $id ) ) {
299 return;
300 }
301
302 $term = $this->get_object_term( $id, $this->tax_translations );
303
304 if ( empty( $term ) ) {
305 return;
306 }
307
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 ] );
315 }
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 }
323 }
324
325 /**
326 * Returns an array of translations of a post or term.
327 *
328 * @since 0.5
329 *
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.
332 */
333 public function get_translations( $id ) {
334 $id = $this->sanitize_int_id( $id );
335
336 if ( empty( $id ) ) {
337 return array();
338 }
339
340 $term = $this->get_object_term( $id, $this->tax_translations );
341 $translations = empty( $term->description ) ? array() : maybe_unserialize( $term->description );
342
343 return $this->validate_translations( $translations, $id, 'display' );
344 }
345
346 /**
347 * Returns the id of the translation of a post or term.
348 *
349 * @since 0.5
350 *
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.
354 */
355 public function get_translation( $id, $lang ) {
356 $lang = $this->model->get_language( $lang );
357
358 if ( empty( $lang ) ) {
359 return false;
360 }
361
362 $translations = $this->get_translations( $id );
363
364 return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
365 }
366
367 /**
368 * Among the object and its translations, returns the id of the object which is in $lang
369 *
370 * @since 0.1
371 *
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.
375 */
376 public function get( $id, $lang ) {
377 $id = $this->sanitize_int_id( $id );
378
379 if ( empty( $id ) ) {
380 return false;
381 }
382
383 $lang = $this->model->get_language( $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 );
396 }
397
398 /**
399 * A join clause to add to sql queries when filtering by language is needed directly in query.
400 *
401 * @since 1.2
402 *
403 * @param string $alias Optional alias for object table.
404 * @return string Join clause.
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|string|string[] $lang PLL_Language object or a comma separated list of language slug or an array of language slugs.
414 * @return string Where clause.
415 */
416 public function where_clause( $lang ) {
417 $tt_id = $this->tax_tt;
418
419 /*
420 * $lang is an object.
421 * This is generally the case if the query is coming from Polylang.
422 */
423 if ( is_object( $lang ) ) {
424 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
425 }
426
427 /*
428 * $lang is a comma separated list of slugs ( or an array of slugs ).
429 * This is generally the case is the query is coming from outside with a 'lang' parameter.
430 */
431 $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
432 $languages = array();
433 foreach ( $slugs as $slug ) {
434 $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
435 }
436
437 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
438 }
439
440 /**
441 * Checks if a user can synchronize translations.
442 *
443 * @since 2.6
444 *
445 * @param int $id Object id.
446 * @return bool
447 */
448 public function current_user_can_synchronize( $id ) {
449 $id = $this->sanitize_int_id( $id );
450
451 if ( empty( $id ) ) {
452 return false;
453 }
454
455 /**
456 * Filters whether a synchronization capability check should take place.
457 *
458 * @since 2.6
459 *
460 * @param bool|null $check Null to enable the capability check,
461 * true to always allow the synchronization,
462 * false to always disallow the synchronization.
463 * Defaults to true.
464 * @param int $id The synchronization source object id.
465 */
466 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
467
468 if ( null !== $check ) {
469 return (bool) $check;
470 }
471
472 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
473 return false;
474 }
475
476 foreach ( $this->get_translations( $id ) as $tr_id ) {
477 if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) {
478 return false;
479 }
480 }
481
482 return true;
483 }
484
485 /**
486 * Sanitizes an ID as positive integer.
487 * Kind of similar to `absint()`, but rejects negetive integers instead of making them positive.
488 *
489 * @since 3.2
490 *
491 * @param mixed $id A supposedly numeric ID.
492 * @return int A positive integer. `0` for non numeric values and negative integers.
493 */
494 public function sanitize_int_id( $id ) {
495 return is_numeric( $id ) && $id >= 1 ? (int) $id : 0;
496 }
497
498 /**
499 * Sanitizes an array of IDs as positive integers.
500 * `0` values are removed.
501 *
502 * @since 3.2
503 *
504 * @param mixed $ids An associative array of translations with language code as key and translation ID as value.
505 * @return array<int> An associative array of translations with language code as key and translation ID as value.
506 */
507 public function sanitize_int_ids_list( $ids ) {
508 if ( empty( $ids ) || ! is_array( $ids ) ) {
509 return array();
510 }
511
512 $ids = array_map( array( $this, 'sanitize_int_id' ), $ids );
513
514 return array_filter( $ids );
515 }
516
517 /**
518 * Validates and sanitizes translations.
519 * This will:
520 * - Make sure to return only translations in existing languages (and only translations).
521 * - Sanitize the values.
522 * - Make sure the provided translation (`$id`) is in the list.
523 * - Check that the translated objects are in the right language, if `$context` is set to 'save'.
524 *
525 * @since 3.1
526 * @since 3.2 Doesn't return `0` ID values.
527 * @since 3.2 Added parameters `$id` and `$context`.
528 *
529 * @param int[] $translations An associative array of translations with language code as key and translation ID as
530 * value.
531 * @param int $id Optional. The object ID for which the translations are validated. When provided, the
532 * process makes sure it is added to the list. Default 0.
533 * @param string $context Optional. The operation for which the translations are validated. When set to
534 * 'save', a check is done to verify that the IDs and langs correspond.
535 * 'display' should be used otherwise. Default 'save'.
536 * @return int[]
537 */
538 protected function validate_translations( $translations, $id = 0, $context = 'save' ) {
539 if ( ! is_array( $translations ) ) {
540 $translations = array();
541 }
542
543 /**
544 * Remove translations in non-existing languages, and non-translation data (we allow plugins to store other
545 * information in the array).
546 */
547 $translations = array_intersect_key(
548 $translations,
549 array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) )
550 );
551
552 // Make sure values are clean before working with them.
553 $translations = $this->sanitize_int_ids_list( $translations );
554
555 if ( 'save' === $context ) {
556 /**
557 * Check that the translated objects are in the right language.
558 * For better performance, this should be done only when saving the data into the database, not when
559 * retrieving data from it.
560 */
561 $valid_translations = array();
562
563 foreach ( $translations as $lang_slug => $tr_id ) {
564 $tr_lang = $this->get_language( $tr_id );
565
566 if ( ! empty( $tr_lang ) && $tr_lang->slug === $lang_slug ) {
567 $valid_translations[ $lang_slug ] = $tr_id;
568 }
569 }
570
571 $translations = $valid_translations;
572 }
573
574 $id = $this->sanitize_int_id( $id );
575
576 if ( empty( $id ) ) {
577 return $translations;
578 }
579
580 // Make sure to return at least the passed object in its translation array.
581 $lang = $this->get_language( $id );
582
583 if ( empty( $lang ) ) {
584 return $translations;
585 }
586
587 return array_merge( array( $lang->slug => $id ), $translations );
588 }
589 }
590