PluginProbe
Polylang / 2.9
Polylang v2.9
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 2.9, at include/translated-object.php

340 lines 11.0 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 public $model;
13 protected $object_type, $type, $tax_language, $tax_translations, $tax_tt;
14
15 /**
16 * Constructor
17 *
18 * @since 1.8
19 *
20 * @param object $model
21 */
22 public function __construct( &$model ) {
23 $this->model = &$model;
24
25 // register our taxonomies as soon as possible
26 // this is early registration, not ready for rewrite rules as wp_rewrite will be setup later
27 $args = array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true );
28 register_taxonomy( $this->tax_language, $this->object_type, $args );
29 $args['update_count_callback'] = '_update_generic_term_count'; // count *all* posts to avoid deleting in clean_translations_terms
30 register_taxonomy( $this->tax_translations, $this->object_type, $args );
31 }
32
33 /**
34 * Wrap wp_get_object_terms to cache it and return only one object
35 * inspired by the function get_the_terms
36 *
37 * @since 1.2
38 *
39 * @param int $object_id post_id or term_id
40 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post ( or term ) language ( or translation )
41 * @return bool|object the term associated to the object in the requested taxonomy if exists, false otherwise
42 */
43 public function get_object_term( $object_id, $taxonomy ) {
44 if ( empty( $object_id ) || is_wp_error( $object_id ) ) {
45 return false;
46 }
47
48 $object_id = (int) $object_id;
49
50 if ( $object_id < 0 ) {
51 return false;
52 }
53
54 $term = get_object_term_cache( $object_id, $taxonomy );
55
56 if ( false === $term ) {
57 // query language and translations at the same time
58 $taxonomies = array( $this->tax_language, $this->tax_translations );
59
60 // query terms
61 $terms = array();
62 foreach ( wp_get_object_terms( $object_id, $taxonomies, array( 'update_term_meta_cache' => false ) ) as $t ) {
63 $terms[ $t->taxonomy ] = $t;
64 if ( $t->taxonomy == $taxonomy ) {
65 $term = $t;
66 }
67 }
68
69 // store it the way WP wants it
70 // set an empty cache if no term found in the taxonomy
71 foreach ( $taxonomies as $tax ) {
72 wp_cache_add( $object_id, empty( $terms[ $tax ] ) ? array() : array( $terms[ $tax ] ), $tax . '_relationships' );
73 }
74 }
75 else {
76 $term = reset( $term );
77 }
78
79 return empty( $term ) ? false : $term;
80 }
81
82 /**
83 * Tells whether a translation term must be updated
84 *
85 * @since 2.3
86 *
87 * @param array $id Post id or term id
88 * @param array $translations An associative array of translations with language code as key and translation id as value
89 */
90 protected function should_update_translation_group( $id, $translations ) {
91 // Don't do anything if no translations have been added to the group
92 $old_translations = $this->get_translations( $id ); // Includes at least $id itself
93 return count( array_diff_assoc( $translations, $old_translations ) ) > 0;
94 }
95
96 /**
97 * Saves translations for posts or terms
98 *
99 * @since 0.5
100 *
101 * @param int $id Post id or term id
102 * @param array $translations An associative array of translations with language code as key and translation id as value
103 */
104 public function save_translations( $id, $translations ) {
105 $id = (int) $id;
106
107 if ( ( $lang = $this->get_language( $id ) ) && isset( $translations ) && is_array( $translations ) ) {
108 // sanitize the translations array
109 $translations = array_map( 'intval', $translations );
110 $translations = array_merge( array( $lang->slug => $id ), $translations ); // make sure this object is in translations
111 $translations = array_diff( $translations, array( 0 ) ); // don't keep non translated languages
112 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); // keep only valid languages slugs as keys
113
114 // unlink removed translations
115 $old_translations = $this->get_translations( $id );
116 foreach ( array_diff_assoc( $old_translations, $translations ) as $object_id ) {
117 $this->delete_translation( $object_id );
118 }
119
120 // Check id we need to create or update the translation group
121 if ( $this->should_update_translation_group( $id, $translations ) ) {
122 $terms = wp_get_object_terms( $translations, $this->tax_translations );
123 $term = reset( $terms );
124
125 // create a new term if necessary
126 if ( empty( $term ) ) {
127 wp_insert_term( $group = uniqid( 'pll_' ), $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
128 }
129 else {
130 // take care not to overwrite extra data stored in description field, if any
131 $d = maybe_unserialize( $term->description );
132 $d = is_array( $d ) ? array_diff_key( $d, $old_translations ) : array(); // remove old translations
133 $d = array_merge( $d, $translations ); // add new one
134 wp_update_term( $group = (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
135 }
136
137 // link all translations to the new term
138 foreach ( $translations as $p ) {
139 wp_set_object_terms( $p, $group, $this->tax_translations );
140 }
141
142 // clean now unused translation groups
143 foreach ( wp_list_pluck( $terms, 'term_id' ) as $term_id ) {
144 $term = get_term( $term_id, $this->tax_translations );
145 if ( empty( $term->count ) ) {
146 wp_delete_term( $term_id, $this->tax_translations );
147 }
148 }
149 }
150 }
151 }
152
153 /**
154 * Deletes a translation of a post or term
155 *
156 * @since 0.5
157 *
158 * @param int $id post id or term id
159 */
160 public function delete_translation( $id ) {
161 $id = (int) $id;
162 $term = $this->get_object_term( $id, $this->tax_translations );
163
164 if ( ! empty( $term ) ) {
165 $d = maybe_unserialize( $term->description );
166 if ( is_array( $d ) ) {
167 $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
168 unset( $d[ $slug ] );
169 }
170
171 if ( empty( $d ) ) {
172 wp_delete_term( (int) $term->term_id, $this->tax_translations );
173 } else {
174 wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $d ) ) );
175 }
176 }
177 }
178
179 /**
180 * Returns an array of translations of a post or term
181 *
182 * @since 0.5
183 *
184 * @param int $id post id or term id
185 * @return array an associative array of translations with language code as key and translation id as value
186 */
187 public function get_translations( $id ) {
188 $term = $this->get_object_term( $id, $this->tax_translations );
189 $translations = empty( $term ) ? array() : maybe_unserialize( $term->description );
190
191 // make sure we return only translations ( thus we allow plugins to store other information in the array )
192 if ( is_array( $translations ) ) {
193 $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) );
194 }
195
196 // make sure to return at least the passed post or term in its translation array
197 if ( empty( $translations ) && $lang = $this->get_language( $id ) ) {
198 $translations = array( $lang->slug => $id );
199 }
200
201 return $translations;
202 }
203
204 /**
205 * Returns the id of the translation of a post or term
206 *
207 * @since 0.5
208 *
209 * @param int $id post id or term id
210 * @param object|string $lang object or slug
211 * @return bool|int post id or term id of the translation, false if there is none
212 */
213 public function get_translation( $id, $lang ) {
214 if ( ! $lang = $this->model->get_language( $lang ) ) {
215 return false;
216 }
217
218 $translations = $this->get_translations( $id );
219
220 return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
221 }
222
223 /**
224 * Among the object and its translations, returns the id of the object which is in $lang
225 *
226 * @since 0.1
227 *
228 * @param int $id post id or term id
229 * @param int|string|object $lang language ( term_id or slug or object )
230 * @return bool|int the translation post id or term id if exists, otherwise the post id or term id, false if the post has no language
231 */
232 public function get( $id, $lang ) {
233 $id = (int) $id;
234 $obj_lang = $this->get_language( $id ); // FIXME is this necessary?
235 if ( ! $lang || ! $obj_lang ) {
236 return false;
237 }
238
239 $lang = $this->model->get_language( $lang );
240 return $obj_lang->term_id == $lang->term_id ? $id : $this->get_translation( $id, $lang );
241 }
242
243 /**
244 * A where clause to add to sql queries when filtering by language is needed directly in query
245 *
246 * @since 1.2
247 *
248 * @param object|array|string $lang a PLL_Language object or a comma separated list of language slug or an array of language slugs
249 * @return string where clause
250 */
251 public function where_clause( $lang ) {
252 $tt_id = $this->tax_tt;
253
254 // $lang is an object
255 // generally the case if the query is coming from Polylang
256 if ( is_object( $lang ) ) {
257 return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->$tt_id );
258 }
259
260 // $lang is a comma separated list of slugs ( or an array of slugs )
261 // generally the case is the query is coming from outside with 'lang' parameter
262 $slugs = is_array( $lang ) ? $lang : explode( ',', $lang );
263 $languages = array();
264 foreach ( $slugs as $slug ) {
265 $languages[] = absint( $this->model->get_language( $slug )->$tt_id );
266 }
267
268 return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages ) . ' )';
269 }
270
271 /**
272 * Returns ids of objects in a language similarly to get_objects_in_term for a taxonomy
273 * faster than get_objects_in_term as it avoids a JOIN
274 *
275 * @since 1.4
276 *
277 * @param object $lang a PLL_Language object
278 * @return array
279 */
280 public function get_objects_in_language( $lang ) {
281 global $wpdb;
282 $tt_id = $this->tax_tt;
283
284 $last_changed = wp_cache_get_last_changed( 'terms' );
285 $cache_key = "polylang:get_objects_in_language:{$lang->$tt_id}:{$last_changed}";
286 $cache = wp_cache_get( $cache_key, 'terms' );
287
288 if ( false === $cache ) {
289 $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $lang->$tt_id ) );
290 wp_cache_set( $cache_key, $object_ids, 'terms' );
291 } else {
292 $object_ids = (array) $cache;
293 }
294
295 if ( ! $object_ids ) {
296 return array();
297 }
298
299 return $object_ids;
300 }
301
302 /**
303 * Check if a user can synchronize translations
304 *
305 * @since 2.6
306 *
307 * @param int $id Object id
308 * @return bool
309 */
310 public function current_user_can_synchronize( $id ) {
311 /**
312 * Filters whether a synchronization capability check should take place
313 *
314 * @since 2.6
315 *
316 * @param $check null to enable the capability check,
317 * true to always allow the synchronization,
318 * false to always disallow the synchronization.
319 * Defaults to true.
320 * @param $id The synchronization source object id
321 */
322 $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
323 if ( null !== $check ) {
324 return $check;
325 }
326
327 if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
328 return false;
329 }
330
331 foreach ( $this->get_translations( $id ) as $tr_id ) {
332 if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) {
333 return false;
334 }
335 }
336
337 return true;
338 }
339 }
340