PluginProbe
Polylang / trunk
Polylang vtrunk
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 / src / modules / sync / sync-metas.php

sync-metas.php in Polylang trunk, at src/modules/sync/sync-metas.php

384 lines 12.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 * Abstract class to manage the copy and synchronization of metas
8 *
9 * @since 2.3
10 */
11 abstract class PLL_Sync_Metas {
12 /**
13 * @var PLL_Model
14 */
15 public $model;
16
17 /**
18 * Meta type. Typically 'post' or 'term'.
19 *
20 * @var string
21 */
22 protected $meta_type;
23
24 /**
25 * Stores the previous values when updating a meta.
26 *
27 * @var array
28 */
29 protected $prev_value = array();
30
31 /**
32 * Stores the metas to synchronize before deleting them.
33 *
34 * @var array
35 */
36 protected $to_copy = array();
37
38 /**
39 * Constructor.
40 *
41 * @since 2.3
42 *
43 * @param PLL_Base $polylang The Polylang object.
44 */
45 public function __construct( &$polylang ) {
46 $this->model = &$polylang->model;
47
48 add_filter( "add_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
49 add_filter( "update_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
50 add_filter( "delete_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
51
52 $this->add_all_meta_actions();
53
54 add_action( "pll_save_{$this->meta_type}", array( $this, 'save_object' ), 10, 3 );
55 }
56
57 /**
58 * Removes all meta synchronization actions and filters
59 *
60 * @since 2.3
61 *
62 * @return void
63 */
64 public function remove_all_meta_actions() {
65 remove_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ) );
66
67 remove_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999 );
68 remove_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ) );
69
70 remove_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ) );
71 remove_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ) );
72 }
73
74 /**
75 * Adds meta synchronization actions and filters
76 *
77 * @since 2.3
78 *
79 * @return void
80 */
81 public function add_all_meta_actions() {
82 add_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ), 10, 4 );
83
84 add_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999, 5 ); // Very late in case a filter prevents the meta to be updated
85 add_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ), 10, 4 );
86
87 add_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ), 10, 2 );
88 add_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ), 10, 4 );
89 }
90
91 /**
92 * Maybe modify ("translate") a meta value when it is copied or synchronized
93 *
94 * @since 2.3
95 *
96 * @param mixed $value Meta value
97 * @param string $key Meta key
98 * @param int $from Id of the source
99 * @param int $to Id of the target
100 * @param string $lang Language of target
101 * @return mixed
102 */
103 protected function maybe_translate_value( $value, $key, $from, $to, $lang ) {
104 /**
105 * Filter a meta value before is copied or synchronized
106 *
107 * @since 2.3
108 *
109 * @param mixed $value Meta value
110 * @param string $key Meta key
111 * @param string $lang Language of target
112 * @param int $from Id of the source
113 * @param int $to Id of the target
114 */
115 return apply_filters( "pll_translate_{$this->meta_type}_meta", $value, $key, $lang, $from, $to );
116 }
117
118 /**
119 * Get the custom fields to copy or synchronize.
120 *
121 * @since 2.3
122 *
123 * @param int $from Id of the post from which we copy information.
124 * @param int $to Id of the post to which we paste information.
125 * @param string $lang Language slug.
126 * @param bool $sync True if it is synchronization, false if it is a copy.
127 * @return string[] List of meta keys.
128 */
129 protected function get_metas_to_copy( $from, $to, $lang, $sync = false ) {
130 /**
131 * Filters the custom fields to copy or synchronize.
132 *
133 * @since 0.6
134 * @since 1.9.2 The `$from`, `$to`, `$lang` parameters were added.
135 *
136 * @param string[] $keys List of custom fields names.
137 * @param bool $sync True if it is synchronization, false if it is a copy.
138 * @param int $from Id of the post from which we copy information.
139 * @param int $to Id of the post to which we paste information.
140 * @param string $lang Language slug.
141 */
142 return array_unique( apply_filters( "pll_copy_{$this->meta_type}_metas", array(), $sync, $from, $to, $lang ) );
143 }
144
145 /**
146 * Disallow modifying synchronized meta if the current user can not modify translations
147 *
148 * @since 2.6
149 *
150 * @param null|bool $check Whether to allow adding/updating/deleting metadata.
151 * @param int $id Object ID.
152 * @param string $meta_key Meta key.
153 * @return null|bool
154 */
155 public function can_synchronize_metadata( $check, $id, $meta_key ) {
156 if ( ! $this->model->{$this->meta_type}->current_user_can_synchronize( $id ) ) {
157 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
158
159 foreach ( $tr_ids as $lang => $tr_id ) {
160 if ( $tr_id != $id ) {
161 $to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
162 if ( in_array( $meta_key, $to_copy ) ) {
163 return false;
164 }
165 }
166 }
167 }
168 return $check;
169 }
170
171 /**
172 * Synchronize added metas across translations
173 *
174 * @since 2.3
175 *
176 * @param int $mid Meta id.
177 * @param int $id Object ID.
178 * @param string $meta_key Meta key.
179 * @param mixed $meta_value Meta value. Must be serializable if non-scalar.
180 * @return void
181 */
182 public function add_meta( $mid, $id, $meta_key, $meta_value ) {
183 static $avoid_recursion = false;
184
185 if ( ! $avoid_recursion ) {
186 $avoid_recursion = true;
187 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
188
189 foreach ( $tr_ids as $lang => $tr_id ) {
190 if ( $tr_id != $id ) {
191 $to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
192 if ( in_array( $meta_key, $to_copy ) ) {
193 $meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
194 add_metadata( $this->meta_type, $tr_id, wp_slash( $meta_key ), is_object( $meta_value ) ? $meta_value : wp_slash( $meta_value ) );
195 }
196 }
197 }
198
199 $avoid_recursion = false;
200 }
201 }
202
203 /**
204 * Stores the previous value when updating metas
205 *
206 * @since 2.3
207 *
208 * @param null|bool $r Not used
209 * @param int $id Object ID.
210 * @param string $meta_key Meta key.
211 * @param mixed $meta_value Meta value. Must be serializable if non-scalar.
212 * @param mixed $prev_value If specified, only update existing metadata entries with the specified value.
213 * @return null|bool Unchanged
214 */
215 public function update_metadata( $r, $id, $meta_key, $meta_value, $prev_value ) {
216 if ( null === $r ) {
217 $hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
218 $this->prev_value[ $hash ] = $prev_value;
219 }
220 return $r;
221 }
222
223 /**
224 * Synchronize updated metas across translations
225 *
226 * @since 2.3
227 *
228 * @param int $mid Meta id.
229 * @param int $id Object ID.
230 * @param string $meta_key Meta key.
231 * @param mixed $meta_value Meta value. Must be serializable if non-scalar.
232 * @return void
233 */
234 public function update_meta( $mid, $id, $meta_key, $meta_value ) {
235 static $avoid_recursion = false;
236 $id = (int) $id;
237
238 if ( ! $avoid_recursion ) {
239 $avoid_recursion = true;
240 $hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
241
242 $prev_meta = get_metadata_by_mid( $this->meta_type, $mid );
243
244 if ( $prev_meta ) {
245 $this->remove_all_meta_actions(); // We don't want to sync back the new metas
246 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
247
248 foreach ( $tr_ids as $lang => $tr_id ) {
249 if ( $tr_id != $id && in_array( $meta_key, $this->get_metas_to_copy( $id, $tr_id, $lang, true ) ) ) {
250 if ( empty( $this->prev_value[ $hash ] ) || $this->prev_value[ $hash ] === $prev_meta->meta_value ) {
251 $prev_value = $this->maybe_translate_value( $prev_meta->meta_value, $meta_key, $id, $tr_id, $lang );
252 $meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
253 update_metadata( $this->meta_type, $tr_id, wp_slash( $meta_key ), is_object( $meta_value ) ? $meta_value : wp_slash( $meta_value ), $prev_value );
254 }
255 }
256 }
257 $this->add_all_meta_actions();
258 }
259
260 $avoid_recursion = false;
261 }
262 }
263
264 /**
265 * Store metas to synchronize before deleting them.
266 *
267 * @since 2.3
268 *
269 * @param int[] $mids Not used.
270 * @param int $id Object ID.
271 * @return void
272 */
273 public function store_metas_to_sync( $mids, $id ) {
274 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
275
276 foreach ( $tr_ids as $lang => $tr_id ) {
277 $this->to_copy[ $id ][ $tr_id ] = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
278 }
279 }
280
281 /**
282 * Synchronizes deleted meta across translations.
283 *
284 * @since 2.3
285 *
286 * @param int[] $mids Not used.
287 * @param int $id Object ID.
288 * @param string $key Meta key.
289 * @param mixed $value Meta value.
290 * @return void
291 */
292 public function delete_meta( $mids, $id, $key, $value ) {
293 static $avoid_recursion = false;
294
295 if ( ! $avoid_recursion ) {
296 $avoid_recursion = true;
297
298 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
299
300 foreach ( $tr_ids as $lang => $tr_id ) {
301 if ( $tr_id != $id ) {
302 if ( in_array( $key, $this->to_copy[ $id ][ $tr_id ] ) ) {
303 if ( '' !== $value && null !== $value && false !== $value ) { // Same test as WP
304 $value = $this->maybe_translate_value( $value, $key, $id, $tr_id, $lang );
305 }
306 delete_metadata( $this->meta_type, $tr_id, wp_slash( $key ), is_object( $value ) ? $value : wp_slash( $value ) );
307 }
308 }
309 }
310 }
311
312 $avoid_recursion = false;
313 }
314
315 /**
316 * Copy or synchronize metas
317 *
318 * @since 2.3
319 *
320 * @param int $from Id of the source object
321 * @param int $to Id of the target object
322 * @param string $lang Language code of the target object
323 * @param bool $sync Optional, defaults to true. True if it is synchronization, false if it is a copy
324 * @return void
325 */
326 public function copy( $from, $to, $lang, $sync = false ) {
327 $this->remove_all_meta_actions();
328
329 $to_copy = $this->get_metas_to_copy( $from, $to, $lang, $sync );
330 $metas = get_metadata( $this->meta_type, $from );
331 $metas = is_array( $metas ) ? $metas : array();
332 $tr_metas = get_metadata( $this->meta_type, $to );
333 $tr_metas = is_array( $tr_metas ) ? $tr_metas : array();
334
335 foreach ( $to_copy as $key ) {
336 if ( empty( $metas[ $key ] ) ) {
337 if ( ! empty( $tr_metas[ $key ] ) ) {
338 // If the meta key is not present in the source object, delete all values
339 delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
340 }
341 } elseif ( ! empty( $tr_metas[ $key ] ) && 1 === count( $metas[ $key ] ) && 1 === count( $tr_metas[ $key ] ) ) {
342 // One custom field to update
343 $value = reset( $metas[ $key ] );
344 $value = maybe_unserialize( $value );
345 $to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
346 update_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
347 } else {
348 // Multiple custom fields, either in the source or the target
349 if ( ! empty( $tr_metas[ $key ] ) ) {
350 // The synchronization of multiple values custom fields is easier if we delete all metas first
351 delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
352 }
353
354 foreach ( $metas[ $key ] as $value ) {
355 $value = maybe_unserialize( $value );
356 $to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
357 add_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
358 }
359 }
360 }
361
362 $this->add_all_meta_actions();
363 }
364
365 /**
366 * If synchronized custom fields were previously not synchronized, it is expected
367 * that saving a post (or term) will synchronize them.
368 *
369 * @since 2.3
370 *
371 * @param int $object_id Id of the object being saved.
372 * @param object $obj Not used.
373 * @param int[] $translations The list of translations object ids.
374 * @return void
375 */
376 public function save_object( $object_id, $obj, $translations ) {
377 foreach ( $translations as $tr_lang => $tr_id ) {
378 if ( $tr_id != $object_id ) {
379 $this->copy( $object_id, $tr_id, $tr_lang, true );
380 }
381 }
382 }
383 }
384