PluginProbe
Polylang / 3.0.2
Polylang v3.0.2
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.0.2, at include/translated-object.php

419 lines 12.8 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 * Wrap wp_get_object_terms() to cache it and return only one object.
96 * inspired by the WordPress function get_the_terms().
97 *
98 * @since 1.2
99 *
100 * @param int $object_id Object id ( typically a post_id or term_id ).
101 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post ( or term ) language ( or translation ).
102 * @return WP_Term|false The term associated to the object in the requested taxonomy if it exists, false otherwise.
103 */
104 public function get_object_term( $object_id, $taxonomy ) {
105 if ( empty( $object_id ) || is_wp_error( $object_id ) ) {
106 return false;
107 }
108
109 $object_id = (int) $object_id;
110
111 if ( $object_id < 0 ) {
112 return false;
113 }
114
115 $term = get_object_term_cache( $object_id, $taxonomy );
116
117 if ( false === $term ) {
118 // Query language and translations at the same time.
119 $taxonomies = array( $this->tax_language, $this->tax_translations );
120
121 // Query terms.
122 $terms = array();
123 $object_terms = wp_get_object_terms( $object_id, $taxonomies, array( 'update_term_meta_cache' => false ) );
124 if ( is_array( $object_terms ) ) {
125 foreach ( $object_terms as $t ) {
126 $terms[ $t->taxonomy ] = $t;
127 if ( $t->taxonomy == $taxonomy ) {
128 $term = $t;
129 }
130 }
131 }
132
133 // Stores it the way WP expects it. Set an empty cache if no term was found in the taxonomy.
134 foreach ( $taxonomies as $tax ) {
135 wp_cache_add( $object_id, empty( $terms[ $tax ] ) ? array() : array( $terms[ $tax ] ), $tax . '_relationships' );
136 }
137 }
138 else {
139 $term = reset( $term );
140 }
141
142 return empty( $term ) ? false : $term;
143 }
144
145 /**
146 * Tells whether a translation term must be updated.
147 *
148 * @since 2.3
149 *
150 * @param int $id Object id ( typically a post_id or term_id ).
151 * @param int[] $translations An associative array of translations with language code as key and translation id as value.
152 * @return bool
153 */
154 protected function should_update_translation_group( $id, $translations ) {
155 // Don't do anything if no translations have been added to the group.
156 $old_translations = $this->get_translations( $id ); // Includes at least $id itself.
157 return count( array_diff_assoc( $translations, $old_translations ) ) > 0;
158 }
159
160 /**
161 * Saves translations for posts or terms.
162 *
163 * @since 0.5
164 *
165 * @param int $id Object id ( typically a post_id or term_id ).
166 * @param int[] $translations An associative array of translations with language code as key and translation id as value.
167 * @return void
168 */
169 public function save_translations( $id, $translations ) {
170 $id = (int) $id;
171
172 if ( ( $lang = $this->get_language( $id ) ) && is_array( $translations ) ) {
173 // Sanitize the translations array.
174 $translations = array_map( 'intval', $translations );
175 $translations = array_merge( array( $lang->slug => $id ), $translations ); // Make sure this object is in translations.
176 $translations = array_diff( $translations, array( 0 ) ); // Don't keep non translated languages.
177 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); // Keep only valid languages slugs as keys.
178
179 // Unlink removed translations.
180 $old_translations = $this->get_translations( $id );
181 foreach ( array_diff_assoc( $old_translations, $translations ) as $object_id ) {
182 $this->delete_translation( $object_id );
183 }
184
185 // Check id we need to create or update the translation group.
186 if ( $this->should_update_translation_group( $id, $translations ) ) {
187 $terms = wp_get_object_terms( $translations, $this->tax_translations );
188 $term = reset( $terms );
189
190 // Create a new term if necessary.
191 if ( empty( $term ) ) {
192 wp_insert_term( $group = uniqid( 'pll_' ), $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
193 } else {
194 // Take care not to overwrite extra data stored in the description field, if any.
195 $d = maybe_unserialize( $term->description );
196 $d = is_array( $d ) ? array_diff_key( $d, $old_translations ) : array(); // Remove old translations.
197 $d = array_merge( $d, $translations ); // Add new one.
198 wp_update_term( $group = (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
199 }
200
201 // Link all translations to the new term.
202 foreach ( $translations as $p ) {
203 wp_set_object_terms( $p, $group, $this->tax_translations );
204 }
205
206 // Clean now unused translation groups.
207 foreach ( wp_list_pluck( $terms, 'term_id' ) as $term_id ) {
208 $term = get_term( $term_id, $this->tax_translations );
209 if ( empty( $term->count ) ) {
210 wp_delete_term( $term_id, $this->tax_translations );
211 }
212 }
213 }
214 }
215 }
216
217 /**
218 * Deletes a translation of a post or term.
219 *
220 * @since 0.5
221 *
222 * @param int $id Object id ( typically a post_id or term_id ).
223 * @return void
224 */
225 public function delete_translation( $id ) {
226 $id = (int) $id;
227 $term = $this->get_object_term( $id, $this->tax_translations );
228
229 if ( ! empty( $term ) ) {
230 $d = maybe_unserialize( $term->description );
231 if ( is_array( $d ) ) {
232 $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
233 unset( $d[ $slug ] );
234 }
235
236 if ( empty( $d ) ) {
237 wp_delete_term( (int) $term->term_id, $this->tax_translations );
238 } else {
239 wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
240 }
241 }
242 }
243
244 /**
245 * Returns an array of translations of a post or term.
246 *
247 * @since 0.5
248 *
249 * @param int $id Object id ( typically a post_id or term_id ).
250 * @return int[] An associative array of translations with language code as key and translation id as value.
251 */
252 public function get_translations( $id ) {
253 $term = $this->get_object_term( $id, $this->tax_translations );
254 $translations = empty( $term ) ? array() : maybe_unserialize( $term->description );
255
256 // Make sure we return only translations ( thus we allow plugins to store other information in the array ).
257 if ( is_array( $translations ) ) {
258 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) );
259 }
260
261 // Make sure to return at least the passed object in its translation array.
262 if ( empty( $translations ) && $lang = $this->get_language( $id ) ) {
263 $translations = array( $lang->slug => $id );
264 }
265
266 return $translations;
267 }
268
269 /**
270 * Returns the id of the translation of a post or term.
271 *
272 * @since 0.5
273 *
274 * @param int $id Object id ( typically a post_id or term_id ).
275 * @param PLL_Language|string $lang Language ( slug or object ).
276 * @return int|false Object id of the translation, false if there is none.
277 */
278 public function get_translation( $id, $lang ) {
279 if ( ! $lang = $this->model->get_language( $lang ) ) {
280 return false;
281 }
282
283 $translations = $this->get_translations( $id );
284
285 return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
286 }
287
288 /**
289 * Among the object and its translations, returns the id of the object which is in $lang
290 *
291 * @since 0.1
292 *
293 * @param int $id Object id ( typically a post_id or term_id ).
294 * @param int|string|PLL_Language $lang Language ( term_id or slug or object ).
295 * @return int|false The translation object id if exists, otherwise the passed id, false if the passed object has no language.
296 */
297 public function get( $id, $lang ) {
298 $id = (int) $id;
299 $lang = $this->model->get_language( $lang );
300 $obj_lang = $this->get_language( $id );
301 if ( empty( $lang ) || empty( $obj_lang ) ) {
302 return false;
303 }
304
305 return $obj_lang->term_id == $lang->term_id ? $id : $this->get_translation( $id, $lang );
306 }
307
308 /**
309 * A join clause to add to sql queries when filtering by language is needed directly in query.
310 *
311 * @since 1.2
312 *
313 * @param string $alias Optional alias for object table.
314 * @return string Join clause.
315 */
316 abstract public function join_clause( $alias = '' );
317
318 /**
319 * A where clause to add to sql queries when filtering by language is needed directly in query.
320 *
321 * @since 1.2
322 *
323 * @param PLL_Language|string|string[] $lang PLL_Language object or a comma separated list of language slug or an array of language slugs.
324 * @return string Where clause.
325 */
326 public function where_clause( $lang ) {
327 $tt_id = $this->tax_tt;
328
329 /*
330 * $lang is an object.
331 * This is generally the case if the query is coming from Polylang.
332 */
333 if ( is_object( $lang ) ) {
334 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
335 }
336
337 /*
338 * $lang is a comma separated list of slugs ( or an array of slugs ).
339 * This is generally the case is the query is coming from outside with a 'lang' parameter.
340 */
341 $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
342 $languages = array();
343 foreach ( $slugs as $slug ) {
344 $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
345 }
346
347 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
348 }
349
350 /**
351 * Returns ids of objects in a language similarly to get_objects_in_term() for a taxonomy.
352 * It is faster than get_objects_in_term() as it avoids a JOIN.
353 *
354 * @since 1.4
355 *
356 * @param PLL_Language $lang PLL_Language object.
357 * @return int[] Object ids.
358 */
359 public function get_objects_in_language( $lang ) {
360 global $wpdb;
361 $tt_id = $this->tax_tt;
362
363 $last_changed = wp_cache_get_last_changed( 'terms' );
364 $cache_key = "polylang:get_objects_in_language:{$lang->$tt_id}:{$last_changed}";
365 $cache = wp_cache_get( $cache_key, 'terms' );
366
367 if ( false === $cache ) {
368 $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $lang->$tt_id ) );
369 wp_cache_set( $cache_key, $object_ids, 'terms' );
370 } else {
371 $object_ids = (array) $cache;
372 }
373
374 if ( ! $object_ids ) {
375 return array();
376 }
377
378 return $object_ids;
379 }
380
381 /**
382 * Check if a user can synchronize translations.
383 *
384 * @since 2.6
385 *
386 * @param int $id Object id.
387 * @return bool
388 */
389 public function current_user_can_synchronize( $id ) {
390 /**
391 * Filters whether a synchronization capability check should take place.
392 *
393 * @since 2.6
394 *
395 * @param bool|null $check Null to enable the capability check,
396 * true to always allow the synchronization,
397 * false to always disallow the synchronization.
398 * Defaults to true.
399 * @param int $id The synchronization source object id.
400 */
401 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
402 if ( null !== $check ) {
403 return $check;
404 }
405
406 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
407 return false;
408 }
409
410 foreach ( $this->get_translations( $id ) as $tr_id ) {
411 if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) {
412 return false;
413 }
414 }
415
416 return true;
417 }
418 }
419