PluginProbe
Polylang / 3.6
Polylang v3.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.6, at include/translatable-object.php

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