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

translatable-object.php in Polylang 3.7.6, at include/translatable-object.php

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