PluginProbe
Polylang / 3.8
Polylang v3.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 / translatable-object.php

translatable-object.php in Polylang 3.8, at src/translatable-object.php

676 lines 17.3 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\Model\Languages;
7
8 defined( 'ABSPATH' ) || exit;
9
10 /**
11 * Abstract class to use for object types that support at least one language.
12 *
13 * @since 3.4
14 *
15 * @phpstan-type DBInfo array{
16 * table: non-empty-string,
17 * id_column: non-empty-string,
18 * default_alias: non-empty-string
19 * }
20 */
21 abstract class PLL_Translatable_Object {
22 /**
23 * Model for the languages.
24 *
25 * @var Languages
26 */
27 protected $languages;
28
29 /**
30 * Polylang's options.
31 *
32 * @var \WP_Syntex\Polylang\Options\Options
33 */
34 protected $options;
35
36 /**
37 * Internal non persistent cache object.
38 *
39 * @var PLL_Cache<mixed>
40 */
41 protected $cache;
42
43 /**
44 * List of taxonomies to cache.
45 *
46 * @var string[]
47 * @see PLL_Translatable_Object::get_object_term()
48 *
49 * @phpstan-var list<non-empty-string>
50 */
51 protected $tax_to_cache = array();
52
53 /**
54 * Taxonomy name for the languages.
55 *
56 * @var string
57 *
58 * @phpstan-var non-empty-string
59 */
60 protected $tax_language;
61
62 /**
63 * Identifier that must be unique for each type of content.
64 * Also used when checking capabilities.
65 *
66 * @var string
67 *
68 * @phpstan-var non-empty-string
69 */
70 protected $type;
71
72 /**
73 * Identifier for each type of content to used for cache type.
74 *
75 * @var string
76 *
77 * @phpstan-var non-empty-string
78 */
79 protected $cache_type;
80
81 /**
82 * Object type to use when registering the taxonomy.
83 * Left empty for posts.
84 *
85 * @var string|null
86 *
87 * @phpstan-var non-empty-string|null
88 */
89 protected $object_type = null;
90
91 /**
92 * Constructor.
93 *
94 * @since 3.4
95 *
96 * @param PLL_Model $model Instance of `PLL_Model`.
97 */
98 public function __construct( PLL_Model $model ) {
99 $this->languages = $model->languages;
100 $this->options = $model->options;
101 $this->cache = $model->cache;
102 $this->tax_to_cache[] = $this->tax_language;
103
104 /*
105 * Register our taxonomy as soon as possible.
106 */
107 $this->register_language_taxonomy();
108 }
109
110 /**
111 * Registers the language taxonomy.
112 *
113 * @since 3.7
114 *
115 * @return void
116 */
117 protected function register_language_taxonomy(): void {
118 register_taxonomy(
119 $this->tax_language,
120 (array) $this->object_type,
121 array(
122 'label' => false,
123 'public' => false,
124 'query_var' => false,
125 'rewrite' => false,
126 '_pll' => true,
127 )
128 );
129 }
130
131 /**
132 * Returns the language taxonomy name.
133 *
134 * @since 3.4
135 *
136 * @return string
137 *
138 * @phpstan-return non-empty-string
139 */
140 public function get_tax_language() {
141 return $this->tax_language;
142 }
143
144 /**
145 * Returns the type of object.
146 *
147 * @since 3.4
148 *
149 * @return string
150 *
151 * @phpstan-return non-empty-string
152 */
153 public function get_type() {
154 return $this->type;
155 }
156
157 /**
158 * Adds hooks.
159 *
160 * @since 3.4
161 *
162 * @return static
163 */
164 public function init() {
165 return $this;
166 }
167
168 /**
169 * Stores the object's language into the database.
170 *
171 * @since 3.4
172 *
173 * @param int $id Object ID.
174 * @param PLL_Language|string|int $lang Language (object, slug, or term ID).
175 * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
176 * the object).
177 */
178 public function set_language( $id, $lang ) {
179 $id = $this->sanitize_int_id( $id );
180
181 if ( empty( $id ) ) {
182 return false;
183 }
184
185 $old_lang = $this->get_language( $id );
186 $old_lang = $old_lang ? $old_lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0;
187
188 $lang = $this->languages->get( $lang );
189 $lang = $lang ? $lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0;
190
191 if ( $old_lang === $lang ) {
192 return false;
193 }
194
195 $term_taxonomy_ids = wp_set_object_terms( $id, $lang, $this->tax_language );
196
197 wp_cache_set_last_changed( $this->cache_type );
198
199 return is_array( $term_taxonomy_ids );
200 }
201
202 /**
203 * Returns the language of an object.
204 *
205 * @since 0.1
206 * @since 3.4 Renamed the parameter $post_id into $id.
207 *
208 * @param int $id Object ID.
209 * @return PLL_Language|false A `PLL_Language` object. `false` if no language is associated to that object or if the
210 * ID is invalid.
211 */
212 public function get_language( $id ) {
213 $id = $this->sanitize_int_id( $id );
214
215 if ( empty( $id ) ) {
216 return false;
217 }
218
219 // Get the language and make sure it is a PLL_Language object.
220 $lang = $this->get_object_term( $id, $this->tax_language );
221
222 if ( empty( $lang ) ) {
223 return false;
224 }
225
226 return $this->languages->get( $lang->term_id );
227 }
228
229 /**
230 * Removes the term language from the database.
231 *
232 * @since 3.4
233 *
234 * @param int $id Term ID.
235 * @return void
236 */
237 public function delete_language( $id ) {
238 $id = $this->sanitize_int_id( $id );
239
240 if ( empty( $id ) ) {
241 return;
242 }
243
244 wp_delete_object_term_relationships( $id, $this->tax_language );
245 }
246
247 /**
248 * Wraps `wp_get_object_terms()` to cache it for multiple objects.
249 *
250 * @since 3.8
251 *
252 * @param int[] $object_ids Array of object IDs.
253 * @param string $taxonomy Taxonomy name.
254 * @return array<int,WP_Term|null> Array of terms with object ID as key.
255 */
256 protected function get_object_terms( array $object_ids, string $taxonomy ) {
257 $object_ids = $this->sanitize_int_ids_list( $object_ids );
258 if ( empty( $object_ids ) ) {
259 return array();
260 }
261
262 $cached_values = $this->get_from_object_term_cache( $object_ids, $taxonomy );
263
264 // Flatten the array to prime the terms cache.
265 $all_term_ids = array();
266 foreach ( $cached_values as $term_ids ) {
267 $all_term_ids = array_merge( $all_term_ids, $term_ids );
268 }
269 _prime_term_caches( $all_term_ids, false );
270
271 $terms = array();
272 foreach ( $cached_values as $object_id => $term_ids ) {
273 if ( ! empty( $term_ids ) ) {
274 $term_id = reset( $term_ids ); // There is only one term for language or translation groups.
275
276 /** @var WP_Term $term */
277 $term = get_term( $term_id );
278 $terms[ $object_id ] = $term;
279 }
280 }
281
282 return $terms;
283 }
284
285 /**
286 * Caches all object-relationship terms.
287 *
288 * @since 3.8
289 *
290 * @param int[] $object_ids Array of object IDs.
291 *
292 * @return void
293 */
294 protected function prime_object_term_cache( array $object_ids ) {
295 $non_cached_ids = array();
296 foreach ( $this->tax_to_cache as $tax ) {
297 $non_cached_ids = array_merge( $non_cached_ids, _get_non_cached_ids( $object_ids, "{$tax}_relationships" ) );
298 }
299
300 if ( empty( $non_cached_ids ) ) {
301 return;
302 }
303
304 $terms = wp_get_object_terms(
305 array_unique( $non_cached_ids ),
306 $this->tax_to_cache,
307 array(
308 'fields' => 'all_with_object_id',
309 'update_term_meta_cache' => false,
310 )
311 );
312
313 if ( ! is_array( $terms ) ) {
314 return;
315 }
316
317 $object_terms = array();
318 foreach ( $terms as $term ) {
319 $object_terms[ $term->taxonomy ][ $term->object_id ][] = $term->term_id;
320 }
321
322 foreach ( $non_cached_ids as $id ) {
323 foreach ( $this->tax_to_cache as $tax ) {
324 if ( ! isset( $object_terms[ $tax ][ $id ] ) ) {
325 $object_terms[ $tax ][ $id ] = array();
326 }
327 }
328 }
329
330 foreach ( $object_terms as $tax => $data ) {
331 wp_cache_add_multiple( $data, "{$tax}_relationships" );
332 }
333 }
334
335 /**
336 * Caches all object-relationship terms and returns them for the specified taxonomy.
337 *
338 * @since 3.8
339 *
340 * @param int[] $object_ids Array of object IDs to retrieve terms for.
341 * @param string $taxonomy Taxonomy name.
342 *
343 * @return int[][]
344 */
345 protected function get_from_object_term_cache( array $object_ids, string $taxonomy ) {
346 $this->prime_object_term_cache( $object_ids );
347 return wp_cache_get_multiple( $object_ids, "{$taxonomy}_relationships" );
348 }
349
350
351 /**
352 * Returns terms associated to the given object in the given taxonomy.
353 *
354 * @since 1.2
355 * @since 3.8 Returns null if the associated term doesn't exist.
356 *
357 * @param int $object_id Object ID.
358 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post (or term, or else) language.
359 * @return WP_Term|null The term associated to the object in the requested taxonomy if it exists, `null` otherwise.
360 */
361 public function get_object_term( $object_id, $taxonomy ) {
362 $terms = $this->get_object_terms( array( $object_id ), $taxonomy );
363 return $terms[ $object_id ] ?? null;
364 }
365
366 /**
367 * A JOIN clause to add to sql queries when filtering by language is needed directly in query.
368 *
369 * @since 3.4
370 *
371 * @param string $alias Optional alias for object table.
372 * @return string The JOIN clause.
373 *
374 * @phpstan-return non-empty-string
375 */
376 public function join_clause( $alias = '' ) {
377 global $wpdb;
378
379 $db = $this->get_db_infos();
380
381 if ( empty( $alias ) ) {
382 $alias = $db['default_alias'];
383 }
384
385 return " INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = {$alias}.{$db['id_column']}";
386 }
387
388 /**
389 * A WHERE clause to add to sql queries when filtering by language is needed directly in query.
390 *
391 * @since 1.2
392 *
393 * @param PLL_Language|PLL_Language[]|string|string[] $lang A `PLL_Language` object, or a comma separated list of language slugs, or an array of language slugs or objects.
394 * @return string The WHERE clause.
395 */
396 public function where_clause( $lang ) {
397 /*
398 * $lang is an object.
399 * This is generally the case if the query is coming from Polylang.
400 */
401 if ( $lang instanceof PLL_Language ) {
402 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
403 }
404
405 /*
406 * $lang is an array of objects, an array of slugs, or a comma separated list of slugs.
407 * The comma separated list of slugs can happen if the query is coming from outside with a 'lang' parameter.
408 */
409 $languages = is_array( $lang ) ? $lang : explode( ',', $lang );
410 $languages_tt_ids = array();
411
412 foreach ( $languages as $language ) {
413 $language = $this->languages->get( $language );
414
415 if ( ! empty( $language ) ) {
416 $languages_tt_ids[] = absint( $language->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
417 }
418 }
419
420 if ( empty( $languages_tt_ids ) ) {
421 return '';
422 }
423
424 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages_tt_ids ) . ' )';
425 }
426
427 /**
428 * Returns the IDs of the objects without language.
429 *
430 * @since 3.4
431 *
432 * @param int $limit Max number of objects to return. `-1` to return all of them.
433 * @param array $args The object args.
434 * @return int[] Array of object IDs.
435 *
436 * @phpstan-param -1|positive-int $limit
437 * @phpstan-return list<positive-int>
438 */
439 public function get_objects_with_no_lang( $limit, array $args = array() ) {
440 $language_ids = array();
441
442 foreach ( $this->languages->get_list() as $language ) {
443 $language_ids[] = $language->get_tax_prop( $this->get_tax_language(), 'term_taxonomy_id' );
444 }
445
446 $language_ids = array_filter( $language_ids );
447
448 if ( empty( $language_ids ) ) {
449 return array();
450 }
451
452 $object_ids = $this->query_objects_with_no_lang( $language_ids, $limit, $args );
453
454 return array_values( $this->sanitize_int_ids_list( $object_ids ) );
455 }
456
457 /**
458 * Returns object IDs without language.
459 * Can be overridden by child classes in case queried object doesn't use
460 * `wp_cache_set_last_changed()` or another cache system.
461 *
462 * @since 3.4
463 * @since 3.7 Changed all parameters.
464 *
465 * @param int[] $language_ids List of language `term_taxonomy_id`.
466 * @param int $limit Max number of objects to return. `-1` to return all of them.
467 * @param array $args The object args.
468 * @return string[] An array of numeric object IDs.
469 *
470 * @phpstan-param array<positive-int> $language_ids
471 * @phpstan-param -1|positive-int $limit
472 * @phpstan-param array<empty> $args
473 */
474 protected function query_objects_with_no_lang( array $language_ids, $limit, array $args = array() ) {
475 $key = "{$this->cache_type}_no_lang:" . md5( maybe_serialize( $language_ids ) . maybe_serialize( $args ) . $limit );
476 $object_ids = $this->get_from_cache( $key );
477
478 if ( is_array( $object_ids ) ) {
479 return $object_ids;
480 }
481
482 $object_ids = $this->get_raw_objects_with_no_lang( $language_ids, $limit, $args );
483 $this->set_to_cache( $key, $object_ids );
484
485 return $object_ids;
486 }
487
488 /**
489 * Sanitizes an ID as positive integer.
490 * Kind of similar to `absint()`, but rejects negative integers instead of making them positive.
491 *
492 * @since 3.2
493 *
494 * @param mixed $id A supposedly numeric ID.
495 * @return int A positive integer. `0` for non numeric values and negative integers.
496 *
497 * @phpstan-return int<0,max>
498 */
499 public function sanitize_int_id( $id ) {
500 return is_numeric( $id ) && $id >= 1 ? abs( (int) $id ) : 0;
501 }
502
503 /**
504 * Sanitizes an array of IDs as positive integers.
505 * `0` values are removed.
506 *
507 * @since 3.2
508 *
509 * @param mixed $ids An array of numeric IDs.
510 * @return int[]
511 *
512 * @phpstan-return array<positive-int>
513 */
514 public function sanitize_int_ids_list( $ids ) {
515 if ( empty( $ids ) || ! is_array( $ids ) ) {
516 return array();
517 }
518
519 $ids = array_map( array( $this, 'sanitize_int_id' ), $ids );
520
521 return array_filter( $ids );
522 }
523
524 /**
525 * Fetches the IDs of the objects without language.
526 *
527 * @since 3.7
528 *
529 * @param int[] $language_ids List of language `term_taxonomy_id`.
530 * @param int $limit Max number of objects to return. `-1` to return all of them.
531 * @param array $args The object args.
532 * @return string[]
533 *
534 * @phpstan-param array<positive-int> $language_ids
535 * @phpstan-param -1|positive-int $limit
536 * @phpstan-param array<empty> $args
537 */
538 protected function get_raw_objects_with_no_lang( array $language_ids, $limit, array $args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
539 global $wpdb;
540
541 $db = $this->get_db_infos();
542
543 return $wpdb->get_col(
544 $wpdb->prepare(
545 sprintf(
546 "SELECT %%i FROM %%i
547 WHERE %%i NOT IN (
548 SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN (%s)
549 )
550 LIMIT %%d",
551 implode( ',', array_fill( 0, count( $language_ids ), '%d' ) )
552 ),
553 array_merge(
554 array( $db['id_column'], $db['table'], $db['id_column'] ),
555 $language_ids,
556 array( $limit >= 1 ? $limit : 4294967295 )
557 )
558 )
559 );
560 }
561
562 /**
563 * Assigns a language to object in mass.
564 *
565 * @since 1.2
566 * @since 3.4 Moved from PLL_Admin_Model class.
567 *
568 * @param int[] $ids Array of post ids or term ids.
569 * @param PLL_Language $lang Language to assign to the posts or terms.
570 * @return void
571 */
572 public function set_language_in_mass( $ids, $lang ) {
573 global $wpdb;
574
575 $tt_id = $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' );
576
577 if ( empty( $tt_id ) ) {
578 return;
579 }
580 $ids = array_map( 'intval', $ids );
581 $ids = array_filter( $ids );
582
583 if ( empty( $ids ) ) {
584 return;
585 }
586
587 $values = array();
588
589 foreach ( $ids as $id ) {
590 $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
591 }
592
593 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
594 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
595
596 // Updating term count is mandatory (thanks to AndyDeGroo).
597 $lang->update_count();
598 clean_term_cache( $ids, $this->tax_language );
599
600 // Invalidate our cache.
601 wp_cache_set_last_changed( $this->cache_type );
602 }
603
604 /**
605 * Returns the description to use for the "language properties" in the REST API.
606 *
607 * @since 3.7
608 * @see WP_Syntex\Polylang\REST\V2\Languages::get_item_schema()
609 *
610 * @return string
611 */
612 public function get_rest_description(): string {
613 /* translators: %s is the name of a database table. */
614 return sprintf( __( 'Language taxonomy properties for table %s.', 'polylang' ), $this->get_db_infos()['table'] );
615 }
616
617 /**
618 * Fetches the value from the cache. Handles backward compatibility with WordPress < 6.9.
619 *
620 * @since 3.8
621 *
622 * @param string $key The cache key.
623 * @return mixed|false The cached value, false if not found.
624 */
625 private function get_from_cache( string $key ) {
626 $last_changed = wp_cache_get_last_changed( $this->cache_type );
627
628 if ( ! function_exists( 'wp_cache_get_salted' ) ) {
629 // Backward compatibility with WordPress < 6.9.
630 $cache_key = "{$key}:{$last_changed}";
631 return wp_cache_get( $cache_key, $this->cache_type );
632 }
633
634 return wp_cache_get_salted( $key, $this->cache_type, $last_changed );
635 }
636
637 /**
638 * Stores the value in the cache. Handles backward compatibility with WordPress < 6.9.
639 *
640 * @since 3.8
641 *
642 * @param string $key The cache key.
643 * @param mixed $value The value to store in the cache.
644 * @return bool True if the value has been stored, false otherwise.
645 */
646 private function set_to_cache( string $key, $value ): bool {
647 $last_changed = wp_cache_get_last_changed( $this->cache_type );
648
649 if ( ! function_exists( 'wp_cache_set_salted' ) ) {
650 // Backward compatibility with WordPress < 6.9.
651 $cache_key = "{$key}:{$last_changed}";
652 return wp_cache_set( $cache_key, $value, $this->cache_type );
653 }
654
655 return wp_cache_set_salted( $key, $value, $this->cache_type, $last_changed );
656 }
657
658 /**
659 * Returns database-related information that can be used in some of this class methods.
660 * These are specific to the table containing the objects.
661 *
662 * @see PLL_Translatable_Object::join_clause()
663 * @see PLL_Translatable_Object::get_raw_objects_with_no_lang()
664 *
665 * @since 3.4.3
666 *
667 * @return string[] {
668 * @type string $table Name of the table.
669 * @type string $id_column Name of the column containing the object's ID.
670 * @type string $default_alias Default alias corresponding to the object's table.
671 * }
672 * @phpstan-return DBInfo
673 */
674 abstract protected function get_db_infos();
675 }
676