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 / translatable-object.php

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

690 lines 17.6 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> 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 $all_term_ids = array_values( $cached_values );
265 _prime_term_caches( $all_term_ids, false );
266
267 $terms = array();
268 foreach ( $cached_values as $object_id => $term_id ) {
269 /** @var WP_Term $term */
270 $term = get_term( $term_id );
271 $terms[ $object_id ] = $term;
272 }
273
274 return $terms;
275 }
276
277 /**
278 * Caches all object-relationship terms.
279 *
280 * @since 3.8.1
281 *
282 * @param int[] $object_ids Array of object IDs.
283 *
284 * @return int[][][]
285 */
286 protected function update_object_term_cache( array $object_ids ) {
287 $non_cached_ids = array();
288 foreach ( $this->tax_to_cache as $tax ) {
289 $non_cached_ids = array_merge( $non_cached_ids, _get_non_cached_ids( $object_ids, "{$tax}_relationships" ) );
290 }
291
292 if ( empty( $non_cached_ids ) ) {
293 return array();
294 }
295
296 $terms = wp_get_object_terms(
297 array_unique( $non_cached_ids ),
298 $this->tax_to_cache,
299 array(
300 'fields' => 'all_with_object_id',
301 'update_term_meta_cache' => false,
302 )
303 );
304
305 if ( ! is_array( $terms ) ) {
306 return array();
307 }
308
309 $object_terms = array();
310 foreach ( $terms as $term ) {
311 $object_terms[ $term->taxonomy ][ $term->object_id ][] = $term->term_id;
312 }
313
314 foreach ( $non_cached_ids as $id ) {
315 foreach ( $this->tax_to_cache as $tax ) {
316 if ( ! isset( $object_terms[ $tax ][ $id ] ) ) {
317 $object_terms[ $tax ][ $id ] = array();
318 }
319 }
320 }
321
322 foreach ( $object_terms as $tax => $data ) {
323 wp_cache_add_multiple( $data, "{$tax}_relationships" );
324 }
325
326 return $object_terms;
327 }
328
329 /**
330 * Caches all object-relationship terms and returns them for the specified taxonomy.
331 *
332 * @since 3.8
333 *
334 * @param int[] $object_ids Array of object IDs to retrieve terms for.
335 * @param string $taxonomy Taxonomy name.
336 *
337 * @return int[] Array of term IDs with object ID as key.
338 */
339 protected function get_from_object_term_cache( array $object_ids, string $taxonomy ): array {
340 $values = wp_cache_get_multiple( $object_ids, "{$taxonomy}_relationships" );
341
342 // If values are missing, then update the cache and replace missed values by freshly cached ones.
343 $object_terms = $this->update_object_term_cache( $object_ids );
344 if ( isset( $object_terms[ $taxonomy ] ) ) {
345 $values = array_replace( $values, $object_terms[ $taxonomy ] );
346 }
347
348 $sanitized_values = array();
349 foreach ( $values as $object_id => $term_ids ) {
350 if ( ! is_array( $term_ids ) ) {
351 continue;
352 }
353
354 $id = reset( $term_ids );
355 if ( ! is_numeric( $id ) || empty( $id ) ) {
356 continue;
357 }
358
359 $sanitized_values[ $object_id ] = (int) $id;
360 }
361
362 return $sanitized_values;
363 }
364
365 /**
366 * Returns terms associated to the given object in the given taxonomy.
367 *
368 * @since 1.2
369 * @since 3.8 Returns null if the associated term doesn't exist.
370 *
371 * @param int $object_id Object ID.
372 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post (or term, or else) language.
373 * @return WP_Term|null The term associated to the object in the requested taxonomy if it exists, `null` otherwise.
374 */
375 public function get_object_term( $object_id, $taxonomy ) {
376 $terms = $this->get_object_terms( array( $object_id ), $taxonomy );
377 return $terms[ $object_id ] ?? null;
378 }
379
380 /**
381 * A JOIN clause to add to sql queries when filtering by language is needed directly in query.
382 *
383 * @since 3.4
384 *
385 * @param string $alias Optional alias for object table.
386 * @return string The JOIN clause.
387 *
388 * @phpstan-return non-empty-string
389 */
390 public function join_clause( $alias = '' ) {
391 global $wpdb;
392
393 $db = $this->get_db_infos();
394
395 if ( empty( $alias ) ) {
396 $alias = $db['default_alias'];
397 }
398
399 return " INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = {$alias}.{$db['id_column']}";
400 }
401
402 /**
403 * A WHERE clause to add to sql queries when filtering by language is needed directly in query.
404 *
405 * @since 1.2
406 *
407 * @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.
408 * @return string The WHERE clause.
409 */
410 public function where_clause( $lang ) {
411 /*
412 * $lang is an object.
413 * This is generally the case if the query is coming from Polylang.
414 */
415 if ( $lang instanceof PLL_Language ) {
416 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
417 }
418
419 /*
420 * $lang is an array of objects, an array of slugs, or a comma separated list of slugs.
421 * The comma separated list of slugs can happen if the query is coming from outside with a 'lang' parameter.
422 */
423 $languages = is_array( $lang ) ? $lang : explode( ',', $lang );
424 $languages_tt_ids = array();
425
426 foreach ( $languages as $language ) {
427 $language = $this->languages->get( $language );
428
429 if ( ! empty( $language ) ) {
430 $languages_tt_ids[] = absint( $language->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
431 }
432 }
433
434 if ( empty( $languages_tt_ids ) ) {
435 return '';
436 }
437
438 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages_tt_ids ) . ' )';
439 }
440
441 /**
442 * Returns the IDs of the objects without language.
443 *
444 * @since 3.4
445 *
446 * @param int $limit Max number of objects to return. `-1` to return all of them.
447 * @param array $args The object args.
448 * @return int[] Array of object IDs.
449 *
450 * @phpstan-param -1|positive-int $limit
451 * @phpstan-return list<positive-int>
452 */
453 public function get_objects_with_no_lang( $limit, array $args = array() ) {
454 $language_ids = array();
455
456 foreach ( $this->languages->get_list() as $language ) {
457 $language_ids[] = $language->get_tax_prop( $this->get_tax_language(), 'term_taxonomy_id' );
458 }
459
460 $language_ids = array_filter( $language_ids );
461
462 if ( empty( $language_ids ) ) {
463 return array();
464 }
465
466 $object_ids = $this->query_objects_with_no_lang( $language_ids, $limit, $args );
467
468 return array_values( $this->sanitize_int_ids_list( $object_ids ) );
469 }
470
471 /**
472 * Returns object IDs without language.
473 * Can be overridden by child classes in case queried object doesn't use
474 * `wp_cache_set_last_changed()` or another cache system.
475 *
476 * @since 3.4
477 * @since 3.7 Changed all parameters.
478 *
479 * @param int[] $language_ids List of language `term_taxonomy_id`.
480 * @param int $limit Max number of objects to return. `-1` to return all of them.
481 * @param array $args The object args.
482 * @return string[] An array of numeric object IDs.
483 *
484 * @phpstan-param array<positive-int> $language_ids
485 * @phpstan-param -1|positive-int $limit
486 * @phpstan-param array<empty> $args
487 */
488 protected function query_objects_with_no_lang( array $language_ids, $limit, array $args = array() ) {
489 $key = "{$this->cache_type}_no_lang:" . md5( maybe_serialize( $language_ids ) . maybe_serialize( $args ) . $limit );
490 $object_ids = $this->get_from_cache( $key );
491
492 if ( is_array( $object_ids ) ) {
493 return $object_ids;
494 }
495
496 $object_ids = $this->get_raw_objects_with_no_lang( $language_ids, $limit, $args );
497 $this->set_to_cache( $key, $object_ids );
498
499 return $object_ids;
500 }
501
502 /**
503 * Sanitizes an ID as positive integer.
504 * Kind of similar to `absint()`, but rejects negative integers instead of making them positive.
505 *
506 * @since 3.2
507 *
508 * @param mixed $id A supposedly numeric ID.
509 * @return int A positive integer. `0` for non numeric values and negative integers.
510 *
511 * @phpstan-return int<0,max>
512 */
513 public function sanitize_int_id( $id ) {
514 return is_numeric( $id ) && $id >= 1 ? abs( (int) $id ) : 0;
515 }
516
517 /**
518 * Sanitizes an array of IDs as positive integers.
519 * `0` values are removed.
520 *
521 * @since 3.2
522 *
523 * @param mixed $ids An array of numeric IDs.
524 * @return int[]
525 *
526 * @phpstan-return array<positive-int>
527 */
528 public function sanitize_int_ids_list( $ids ) {
529 if ( empty( $ids ) || ! is_array( $ids ) ) {
530 return array();
531 }
532
533 $ids = array_map( array( $this, 'sanitize_int_id' ), $ids );
534
535 return array_filter( $ids );
536 }
537
538 /**
539 * Fetches the IDs of the objects without language.
540 *
541 * @since 3.7
542 *
543 * @param int[] $language_ids List of language `term_taxonomy_id`.
544 * @param int $limit Max number of objects to return. `-1` to return all of them.
545 * @param array $args The object args.
546 * @return string[]
547 *
548 * @phpstan-param array<positive-int> $language_ids
549 * @phpstan-param -1|positive-int $limit
550 * @phpstan-param array<empty> $args
551 */
552 protected function get_raw_objects_with_no_lang( array $language_ids, $limit, array $args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
553 global $wpdb;
554
555 $db = $this->get_db_infos();
556
557 return $wpdb->get_col(
558 $wpdb->prepare(
559 sprintf(
560 "SELECT %%i FROM %%i
561 WHERE %%i NOT IN (
562 SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN (%s)
563 )
564 LIMIT %%d",
565 implode( ',', array_fill( 0, count( $language_ids ), '%d' ) )
566 ),
567 array_merge(
568 array( $db['id_column'], $db['table'], $db['id_column'] ),
569 $language_ids,
570 array( $limit >= 1 ? $limit : 4294967295 )
571 )
572 )
573 );
574 }
575
576 /**
577 * Assigns a language to object in mass.
578 *
579 * @since 1.2
580 * @since 3.4 Moved from PLL_Admin_Model class.
581 *
582 * @param int[] $ids Array of post ids or term ids.
583 * @param PLL_Language $lang Language to assign to the posts or terms.
584 * @return void
585 */
586 public function set_language_in_mass( $ids, $lang ) {
587 global $wpdb;
588
589 $tt_id = $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' );
590
591 if ( empty( $tt_id ) ) {
592 return;
593 }
594 $ids = array_map( 'intval', $ids );
595 $ids = array_filter( $ids );
596
597 if ( empty( $ids ) ) {
598 return;
599 }
600
601 $values = array();
602
603 foreach ( $ids as $id ) {
604 $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
605 }
606
607 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
608 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
609
610 // Updating term count is mandatory (thanks to AndyDeGroo).
611 $lang->update_count();
612 clean_term_cache( $ids, $this->tax_language );
613
614 // Invalidate our cache.
615 wp_cache_set_last_changed( $this->cache_type );
616 }
617
618 /**
619 * Returns the description to use for the "language properties" in the REST API.
620 *
621 * @since 3.7
622 * @see WP_Syntex\Polylang\REST\V2\Languages::get_item_schema()
623 *
624 * @return string
625 */
626 public function get_rest_description(): string {
627 /* translators: %s is the name of a database table. */
628 return sprintf( __( 'Language taxonomy properties for table %s.', 'polylang' ), $this->get_db_infos()['table'] );
629 }
630
631 /**
632 * Fetches the value from the cache. Handles backward compatibility with WordPress < 6.9.
633 *
634 * @since 3.8
635 *
636 * @param string $key The cache key.
637 * @return mixed|false The cached value, false if not found.
638 */
639 private function get_from_cache( string $key ) {
640 $last_changed = wp_cache_get_last_changed( $this->cache_type );
641
642 if ( ! function_exists( 'wp_cache_get_salted' ) ) {
643 // Backward compatibility with WordPress < 6.9.
644 $cache_key = "{$key}:{$last_changed}";
645 return wp_cache_get( $cache_key, $this->cache_type );
646 }
647
648 return wp_cache_get_salted( $key, $this->cache_type, $last_changed );
649 }
650
651 /**
652 * Stores the value in the cache. Handles backward compatibility with WordPress < 6.9.
653 *
654 * @since 3.8
655 *
656 * @param string $key The cache key.
657 * @param mixed $value The value to store in the cache.
658 * @return bool True if the value has been stored, false otherwise.
659 */
660 private function set_to_cache( string $key, $value ): bool {
661 $last_changed = wp_cache_get_last_changed( $this->cache_type );
662
663 if ( ! function_exists( 'wp_cache_set_salted' ) ) {
664 // Backward compatibility with WordPress < 6.9.
665 $cache_key = "{$key}:{$last_changed}";
666 return wp_cache_set( $cache_key, $value, $this->cache_type );
667 }
668
669 return wp_cache_set_salted( $key, $value, $this->cache_type, $last_changed );
670 }
671
672 /**
673 * Returns database-related information that can be used in some of this class methods.
674 * These are specific to the table containing the objects.
675 *
676 * @see PLL_Translatable_Object::join_clause()
677 * @see PLL_Translatable_Object::get_raw_objects_with_no_lang()
678 *
679 * @since 3.4.3
680 *
681 * @return string[] {
682 * @type string $table Name of the table.
683 * @type string $id_column Name of the column containing the object's ID.
684 * @type string $default_alias Default alias corresponding to the object's table.
685 * }
686 * @phpstan-return DBInfo
687 */
688 abstract protected function get_db_infos();
689 }
690