PluginProbe
Polylang / 3.8.8
Polylang v3.8.8
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 / src / translated-object.php

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

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