PluginProbe
Polylang / 3.0
Polylang v3.0
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 / modules / sync / sync-metas.php

sync-metas.php in Polylang 3.0, at modules/sync/sync-metas.php

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