PluginProbe
Polylang / 3.7
Polylang v3.7
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.7, at include/translated-object.php

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