PluginProbe
Polylang / 3.8.10
Polylang v3.8.10
3.8.10 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 All 234 releases
polylang / src / translated-object.php

translated-object.php in Polylang 3.8.10, at src/translated-object.php

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