PluginProbe
Polylang / trunk
Polylang vtrunk
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 trunk, at src/translated-object.php

653 lines 18.7 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 = pll_sanitize_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 = pll_sanitize_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 = pll_sanitize_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' => (string) 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' => (string) 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 = pll_sanitize_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' => (string) 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 = pll_sanitize_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 = pll_sanitize_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 = pll_sanitize_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 = pll_sanitize_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 return count( $translations ) > 1;
409 }
410
411 /**
412 * Returns an array of valid translations for multiple objects.
413 *
414 * @since 3.8
415 *
416 * @param int[] $object_ids Array of object IDs.
417 * @return int[] An associative array of translations with language code as key and translation ID as value.
418 *
419 * @phpstan-return array<non-empty-string, positive-int>
420 */
421 protected function get_objects_translations( array $object_ids ) {
422 $translations_arrays = $this->get_raw_objects_translations( $object_ids );
423
424 $validated = array();
425 foreach ( $translations_arrays as $id => $translations ) {
426 $validated = array_merge( $validated, $this->validate_translations( $translations, $id, 'display' ) );
427 }
428 return $validated;
429 }
430
431 /**
432 * Returns an unvalidated array of translations for multiple objects.
433 * It is generally preferable to use `get_objects_translations()`.
434 *
435 * @since 3.8
436 *
437 * @param int[] $object_ids Array of object IDs.
438 * @return int[][] An array of an associative array of translations with language code as key and translation ID as value.
439 * First level key is the id of the object that translations are related to.
440 *
441 * @phpstan-return array<int,array<non-empty-string, positive-int>>
442 */
443 protected function get_raw_objects_translations( array $object_ids ) {
444 $terms = $this->get_object_terms( $object_ids, $this->tax_translations );
445
446 $translations = array();
447 foreach ( $object_ids as $id ) {
448 if ( empty( $terms[ $id ] ) || empty( $terms[ $id ]->description ) ) {
449 $translations[ $id ] = array();
450 continue;
451 }
452
453 $trans = maybe_unserialize( $terms[ $id ]->description );
454 $translations[ $id ] = is_array( $trans ) ? $trans : array();
455 }
456
457 return $translations;
458 }
459
460 /**
461 * Validates and sanitizes translations.
462 * This will:
463 * - Make sure to return only translations in existing languages (and only translations).
464 * - Sanitize the values.
465 * - Make sure the provided translation (`$id`) is in the list.
466 * - Check that the translated objects are in the right language, if `$context` is set to 'save'.
467 *
468 * @since 3.1
469 * @since 3.2 Doesn't return `0` ID values.
470 * @since 3.2 Added parameters `$id` and `$context`.
471 *
472 * @param int[] $translations An associative array of translations with language code as key and translation ID as
473 * value.
474 * @param int $id Optional. The object ID for which the translations are validated. When provided, the
475 * process makes sure it is added to the list. Default 0.
476 * @param string $context Optional. The operation for which the translations are validated. When set to
477 * 'save', a check is done to verify that the IDs and langs correspond.
478 * 'display' should be used otherwise. Default 'save'.
479 * @return int[]
480 *
481 * @phpstan-param non-empty-string $context
482 * @phpstan-return array<non-empty-string, positive-int>
483 */
484 protected function validate_translations( $translations, $id = 0, $context = 'save' ) {
485 if ( ! is_array( $translations ) ) {
486 $translations = array();
487 }
488
489 /**
490 * Remove translations in non-existing languages, and non-translation data (we allow plugins to store other
491 * information in the array).
492 */
493 $translations = array_intersect_key(
494 $translations,
495 array_flip( $this->languages->get_list( array( 'fields' => 'slug' ) ) )
496 );
497
498 // Make sure values are clean before working with them.
499 /** @phpstan-var array<non-empty-string, positive-int> $translations */
500 $translations = pll_sanitize_ids( $translations );
501
502 if ( 'save' === $context ) {
503 /**
504 * Check that the translated objects are in the right language.
505 * For better performance, this should be done only when saving the data into the database, not when
506 * retrieving data from it.
507 */
508 $valid_translations = array();
509
510 foreach ( $translations as $lang_slug => $tr_id ) {
511 $tr_lang = $this->get_language( $tr_id );
512
513 if ( ! empty( $tr_lang ) && $tr_lang->slug === $lang_slug ) {
514 $valid_translations[ $lang_slug ] = $tr_id;
515 }
516 }
517
518 $translations = $valid_translations;
519 }
520
521 $id = pll_sanitize_id( $id );
522
523 if ( empty( $id ) ) {
524 return $translations;
525 }
526
527 // Make sure to return at least the passed object in its translation array.
528 $lang = $this->get_language( $id );
529
530 if ( empty( $lang ) ) {
531 return $translations;
532 }
533
534 /** @phpstan-var array<non-empty-string, positive-int> $translations */
535 return array_merge( array( $lang->slug => $id ), $translations );
536 }
537
538 /**
539 * Creates translations groups in mass.
540 *
541 * @since 1.6.3
542 * @since 3.4 Moved from PLL_Admin_Model class. The `$type` parameter is removed.
543 * @since 3.8 The name of the translation terms can be customized.
544 *
545 * @param int[][] $translations Array of translations arrays. The keys of the first level array can be used to
546 * customize the name of the translation terms. Example:
547 * array(
548 * 'pll_term_name_1' => array(
549 * 'lang_slug_1' => {object ID},
550 * 'lang_slug_2' => {object ID},
551 * )
552 * )
553 * @return void
554 *
555 * @phpstan-param array<array<string,int>> $translations
556 */
557 public function set_translation_in_mass( $translations ) {
558 global $wpdb;
559
560 $terms = array();
561 $slugs = array();
562 $description = array();
563 $count = array();
564
565 foreach ( $translations as $k => $t ) {
566 $term = is_string( $k ) ? $k : uniqid( 'pll_' ); // The term name.
567 $terms[] = array( $term, $term );
568 $slugs[] = $term;
569 $description[ $term ] = maybe_serialize( $t );
570 $count[ $term ] = count( $t );
571 }
572
573 // Insert terms.
574 if ( ! empty( $terms ) ) {
575 $wpdb->query(
576 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
577 sprintf(
578 "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES %s",
579 implode( ',', array_fill( 0, count( $terms ), '( %s, %s )' ) )
580 ),
581 array_merge( ...$terms )
582 )
583 );
584 }
585
586 // Get all terms with their term_id.
587 $terms = $wpdb->get_results(
588 $wpdb->prepare(
589 sprintf(
590 "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN (%s)",
591 implode( ',', array_fill( 0, count( $slugs ), '%s' ) )
592 ),
593 $slugs
594 )
595 );
596
597 $term_ids = array();
598 $tts = array();
599
600 // Prepare terms taxonomy relationship.
601 foreach ( $terms as $term ) {
602 $term_ids[] = $term->term_id;
603 $tts[] = array( $term->term_id, $this->tax_translations, $description[ $term->slug ], $count[ $term->slug ] );
604 }
605
606 // Insert term_taxonomy.
607 if ( ! empty( $tts ) ) {
608 $wpdb->query(
609 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
610 sprintf(
611 "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES %s",
612 implode( ',', array_fill( 0, count( $tts ), '( %d, %s, %s, %d )' ) )
613 ),
614 array_merge( ...$tts )
615 )
616 );
617 }
618
619 // Get all terms with term_taxonomy_id.
620 $terms = get_terms( array( 'taxonomy' => $this->tax_translations, 'hide_empty' => false ) );
621 $trs = array();
622
623 // Prepare objects relationships.
624 if ( is_array( $terms ) ) {
625 foreach ( $terms as $term ) {
626 $t = maybe_unserialize( $term->description );
627 if ( is_array( $t ) && in_array( $t, $translations ) ) {
628 foreach ( $t as $object_id ) {
629 if ( ! empty( $object_id ) ) {
630 $trs[] = array( $object_id, $term->term_taxonomy_id );
631 }
632 }
633 }
634 }
635 }
636
637 // Insert term_relationships.
638 if ( ! empty( $trs ) ) {
639 $wpdb->query(
640 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
641 sprintf(
642 "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES %s",
643 implode( ',', array_fill( 0, count( $trs ), '( %d, %d )' ) )
644 ),
645 array_merge( ...$trs )
646 )
647 );
648 }
649
650 clean_term_cache( $term_ids, $this->tax_translations );
651 }
652 }
653