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

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