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

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