PluginProbe
Polylang / 3.1.3
Polylang v3.1.3
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 / translated-object.php

translated-object.php in Polylang 3.1.3, at include/translated-object.php

469 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 /**
7 * Setups the objects languages and translations model.
8 *
9 * @since 1.8
10 */
11 abstract class PLL_Translated_Object {
12 /**
13 * @var PLL_Model
14 */
15 public $model;
16
17 /**
18 * Object type to use when registering the taxonomies.
19 * Left empty for posts.
20 *
21 * @var string|null
22 */
23 protected $object_type;
24
25 /**
26 * Object type to use when checking capabilities.
27 *
28 * @var string
29 */
30 protected $type;
31
32 /**
33 * Taxonomy name for the languages.
34 *
35 * @var string
36 */
37 protected $tax_language;
38
39 /**
40 * Taxonomy name for the translation groups.
41 *
42 * @var string
43 */
44 protected $tax_translations;
45
46 /**
47 * PLL_Language property name for the term_taxonomy id.
48 *
49 * @var string
50 */
51 protected $tax_tt;
52
53 /**
54 * Constructor.
55 *
56 * @since 1.8
57 *
58 * @param PLL_Model $model Instance of PLL_Model.
59 */
60 public function __construct( &$model ) {
61 $this->model = &$model;
62
63 /*
64 * Register our taxonomies as soon as possible.
65 * This is early registration, not ready for rewrite rules as $wp_rewrite will be setup later.
66 */
67 $args = array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true );
68 register_taxonomy( $this->tax_language, $this->object_type, $args );
69 $args['update_count_callback'] = '_update_generic_term_count'; // Count *all* objects to avoid deleting in clean_translations_terms.
70 register_taxonomy( $this->tax_translations, $this->object_type, $args );
71 }
72
73 /**
74 * Stores the language in the database.
75 *
76 * @since 0.6
77 *
78 * @param int $id Object id.
79 * @param int|string|PLL_Language $lang Language (term_id or slug or object).
80 * @return void
81 */
82 abstract public function set_language( $id, $lang );
83
84 /**
85 * Returns the language of an object.
86 *
87 * @since 0.1
88 *
89 * @param int $id Object id.
90 * @return PLL_Language|false PLL_Language object, false if no language is associated to that object.
91 */
92 abstract public function get_language( $id );
93
94 /**
95 * Assigns a new language to an object, taking care of the translations group.
96 *
97 * @since 3.1
98 *
99 * @param int $id Object id.
100 * @param PLL_Language $lang New language to assign to the object.
101 * @return void
102 */
103 public function update_language( $id, $lang ) {
104 if ( $this->get_language( $id ) === $lang ) {
105 return;
106 }
107
108 $this->set_language( $id, $lang );
109
110 $translations = $this->get_translations( $id );
111
112 if ( $translations ) {
113 // Remove the post's former language from the new translations group.
114 $translations = array_diff( $translations, array( $id ) );
115 $this->save_translations( $id, $translations );
116 }
117 }
118
119 /**
120 * Wrap wp_get_object_terms() to cache it and return only one object.
121 * inspired by the WordPress function get_the_terms().
122 *
123 * @since 1.2
124 *
125 * @param int $object_id Object id ( typically a post_id or term_id ).
126 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post ( or term ) language ( or translation ).
127 * @return WP_Term|false The term associated to the object in the requested taxonomy if it exists, false otherwise.
128 */
129 public function get_object_term( $object_id, $taxonomy ) {
130 if ( empty( $object_id ) || is_wp_error( $object_id ) ) {
131 return false;
132 }
133
134 $object_id = (int) $object_id;
135
136 if ( $object_id < 0 ) {
137 return false;
138 }
139
140 $term = get_object_term_cache( $object_id, $taxonomy );
141
142 if ( false === $term ) {
143 // Query language and translations at the same time.
144 $taxonomies = array( $this->tax_language, $this->tax_translations );
145
146 // Query terms.
147 $terms = array();
148 $object_terms = wp_get_object_terms( $object_id, $taxonomies, array( 'update_term_meta_cache' => false ) );
149 if ( is_array( $object_terms ) ) {
150 foreach ( $object_terms as $t ) {
151 $terms[ $t->taxonomy ] = $t;
152 if ( $t->taxonomy == $taxonomy ) {
153 $term = $t;
154 }
155 }
156 }
157
158 // Stores it the way WP expects it. Set an empty cache if no term was found in the taxonomy.
159 foreach ( $taxonomies as $tax ) {
160 wp_cache_add( $object_id, empty( $terms[ $tax ] ) ? array() : array( $terms[ $tax ] ), $tax . '_relationships' );
161 }
162 }
163 else {
164 $term = reset( $term );
165 }
166
167 return empty( $term ) ? false : $term;
168 }
169
170 /**
171 * Tells whether a translation term must be updated.
172 *
173 * @since 2.3
174 *
175 * @param int $id Object id ( typically a post_id or term_id ).
176 * @param int[] $translations An associative array of translations with language code as key and translation id as value.
177 * @return bool
178 */
179 protected function should_update_translation_group( $id, $translations ) {
180 // Don't do anything if no translations have been added to the group.
181 $old_translations = $this->get_translations( $id ); // Includes at least $id itself.
182 return count( array_diff_assoc( $translations, $old_translations ) ) > 0;
183 }
184
185 /**
186 * Saves translations for posts or terms.
187 *
188 * @since 0.5
189 *
190 * @param int $id Object id ( typically a post_id or term_id ).
191 * @param int[] $translations An associative array of translations with language code as key and translation id as value.
192 *
193 * @return int[] An associative array with language codes as key and post ids as values.
194 */
195 public function save_translations( $id, $translations ) {
196 $translations = $this->validate_translations( $translations );
197
198 $id = (int) $id;
199
200 if ( $lang = $this->get_language( $id ) ) {
201 // Sanitize the translations array.
202 $translations = array_map( 'intval', $translations );
203 $translations = array_merge( array( $lang->slug => $id ), $translations ); // Make sure this object is in translations.
204 $translations = array_diff( $translations, array( 0 ) ); // Don't keep non translated languages.
205 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); // Keep only valid languages slugs as keys.
206
207 // Unlink removed translations.
208 $old_translations = $this->get_translations( $id );
209 foreach ( array_diff_assoc( $old_translations, $translations ) as $object_id ) {
210 $this->delete_translation( $object_id );
211 }
212
213 // Check id we need to create or update the translation group.
214 if ( $this->should_update_translation_group( $id, $translations ) ) {
215 $terms = wp_get_object_terms( $translations, $this->tax_translations );
216 $term = reset( $terms );
217
218 // Create a new term if necessary.
219 if ( empty( $term ) ) {
220 wp_insert_term( $group = uniqid( 'pll_' ), $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
221 } else {
222 // Take care not to overwrite extra data stored in the description field, if any.
223 $d = maybe_unserialize( $term->description );
224 $d = is_array( $d ) ? array_diff_key( $d, $old_translations ) : array(); // Remove old translations.
225 $d = array_merge( $d, $translations ); // Add new one.
226 wp_update_term( $group = (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
227 }
228
229 // Link all translations to the new term.
230 foreach ( $translations as $p ) {
231 wp_set_object_terms( $p, $group, $this->tax_translations );
232 }
233
234 // Clean now unused translation groups.
235 foreach ( wp_list_pluck( $terms, 'term_id' ) as $term_id ) {
236 $term = get_term( $term_id, $this->tax_translations );
237 if ( empty( $term->count ) ) {
238 wp_delete_term( $term_id, $this->tax_translations );
239 }
240 }
241 }
242 return $translations;
243 } else {
244 return array();
245 }
246 }
247
248 /**
249 * Returns translations after checking the translated post is in the right language
250 *
251 * @since 3.1
252 *
253 * @param int[] $translations An associative array of translations with language code as key and translation id as value.
254 *
255 * @return int[]
256 */
257 public function validate_translations( $translations ) {
258 $valid_translations = array();
259
260 foreach ( $translations as $lang => $tr_id ) {
261 $valid_translations[ $lang ] = ( $tr_id && $this->get_language( (int) $tr_id )->slug == $lang ) ? (int) $tr_id : 0;
262 }
263
264 return $valid_translations;
265 }
266
267 /**
268 * Deletes a translation of a post or term.
269 *
270 * @since 0.5
271 *
272 * @param int $id Object id ( typically a post_id or term_id ).
273 * @return void
274 */
275 public function delete_translation( $id ) {
276 $id = (int) $id;
277 $term = $this->get_object_term( $id, $this->tax_translations );
278
279 if ( ! empty( $term ) ) {
280 $d = maybe_unserialize( $term->description );
281 if ( is_array( $d ) ) {
282 $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
283 unset( $d[ $slug ] );
284 }
285
286 if ( empty( $d ) ) {
287 wp_delete_term( (int) $term->term_id, $this->tax_translations );
288 } else {
289 wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
290 }
291 }
292 }
293
294 /**
295 * Returns an array of translations of a post or term.
296 *
297 * @since 0.5
298 *
299 * @param int $id Object id ( typically a post_id or term_id ).
300 * @return int[] An associative array of translations with language code as key and translation id as value.
301 */
302 public function get_translations( $id ) {
303 $term = $this->get_object_term( $id, $this->tax_translations );
304 $translations = empty( $term ) ? array() : maybe_unserialize( $term->description );
305
306 // Make sure we return only translations ( thus we allow plugins to store other information in the array ).
307 if ( is_array( $translations ) ) {
308 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) );
309 }
310
311 // Make sure to return at least the passed object in its translation array.
312 if ( empty( $translations ) && $lang = $this->get_language( $id ) ) {
313 $translations = array( $lang->slug => $id );
314 }
315
316 return $translations;
317 }
318
319 /**
320 * Returns the id of the translation of a post or term.
321 *
322 * @since 0.5
323 *
324 * @param int $id Object id ( typically a post_id or term_id ).
325 * @param PLL_Language|string $lang Language ( slug or object ).
326 * @return int|false Object id of the translation, false if there is none.
327 */
328 public function get_translation( $id, $lang ) {
329 if ( ! $lang = $this->model->get_language( $lang ) ) {
330 return false;
331 }
332
333 $translations = $this->get_translations( $id );
334
335 return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
336 }
337
338 /**
339 * Among the object and its translations, returns the id of the object which is in $lang
340 *
341 * @since 0.1
342 *
343 * @param int $id Object id ( typically a post_id or term_id ).
344 * @param int|string|PLL_Language $lang Language ( term_id or slug or object ).
345 * @return int|false The translation object id if exists, otherwise the passed id, false if the passed object has no language.
346 */
347 public function get( $id, $lang ) {
348 $id = (int) $id;
349 $lang = $this->model->get_language( $lang );
350 $obj_lang = $this->get_language( $id );
351 if ( empty( $lang ) || empty( $obj_lang ) ) {
352 return false;
353 }
354
355 return $obj_lang->term_id == $lang->term_id ? $id : $this->get_translation( $id, $lang );
356 }
357
358 /**
359 * A join clause to add to sql queries when filtering by language is needed directly in query.
360 *
361 * @since 1.2
362 *
363 * @param string $alias Optional alias for object table.
364 * @return string Join clause.
365 */
366 abstract public function join_clause( $alias = '' );
367
368 /**
369 * A where clause to add to sql queries when filtering by language is needed directly in query.
370 *
371 * @since 1.2
372 *
373 * @param PLL_Language|string|string[] $lang PLL_Language object or a comma separated list of language slug or an array of language slugs.
374 * @return string Where clause.
375 */
376 public function where_clause( $lang ) {
377 $tt_id = $this->tax_tt;
378
379 /*
380 * $lang is an object.
381 * This is generally the case if the query is coming from Polylang.
382 */
383 if ( is_object( $lang ) ) {
384 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
385 }
386
387 /*
388 * $lang is a comma separated list of slugs ( or an array of slugs ).
389 * This is generally the case is the query is coming from outside with a 'lang' parameter.
390 */
391 $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
392 $languages = array();
393 foreach ( $slugs as $slug ) {
394 $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
395 }
396
397 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
398 }
399
400 /**
401 * Returns ids of objects in a language similarly to get_objects_in_term() for a taxonomy.
402 * It is faster than get_objects_in_term() as it avoids a JOIN.
403 *
404 * @since 1.4
405 *
406 * @param PLL_Language $lang PLL_Language object.
407 * @return int[] Object ids.
408 */
409 public function get_objects_in_language( $lang ) {
410 global $wpdb;
411 $tt_id = $this->tax_tt;
412
413 $last_changed = wp_cache_get_last_changed( 'terms' );
414 $cache_key = "polylang:get_objects_in_language:{$lang->$tt_id}:{$last_changed}";
415 $cache = wp_cache_get( $cache_key, 'terms' );
416
417 if ( false === $cache ) {
418 $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $lang->$tt_id ) );
419 wp_cache_set( $cache_key, $object_ids, 'terms' );
420 } else {
421 $object_ids = (array) $cache;
422 }
423
424 if ( ! $object_ids ) {
425 return array();
426 }
427
428 return $object_ids;
429 }
430
431 /**
432 * Check if a user can synchronize translations.
433 *
434 * @since 2.6
435 *
436 * @param int $id Object id.
437 * @return bool
438 */
439 public function current_user_can_synchronize( $id ) {
440 /**
441 * Filters whether a synchronization capability check should take place.
442 *
443 * @since 2.6
444 *
445 * @param bool|null $check Null to enable the capability check,
446 * true to always allow the synchronization,
447 * false to always disallow the synchronization.
448 * Defaults to true.
449 * @param int $id The synchronization source object id.
450 */
451 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
452 if ( null !== $check ) {
453 return $check;
454 }
455
456 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
457 return false;
458 }
459
460 foreach ( $this->get_translations( $id ) as $tr_id ) {
461 if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) {
462 return false;
463 }
464 }
465
466 return true;
467 }
468 }
469