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

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

407 lines 12.3 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 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' ) );
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 );
79 remove_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ) );
80
81 remove_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ) );
82 remove_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ) );
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 $id = (int) $id;
259
260 if ( ! $avoid_recursion ) {
261 $avoid_recursion = true;
262 $hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
263
264 $prev_meta = get_metadata_by_mid( $this->meta_type, $mid );
265
266 if ( $prev_meta ) {
267 $this->remove_add_meta_action(); // We don't want to sync back the new metas
268 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
269
270 foreach ( $tr_ids as $lang => $tr_id ) {
271 if ( $tr_id != $id && in_array( $meta_key, $this->get_metas_to_copy( $id, $tr_id, $lang, true ) ) ) {
272 if ( empty( $this->prev_value[ $hash ] ) || $this->prev_value[ $hash ] === $prev_meta->meta_value ) {
273 $prev_value = $this->maybe_translate_value( $prev_meta->meta_value, $meta_key, $id, $tr_id, $lang );
274 $meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
275 update_metadata( $this->meta_type, $tr_id, wp_slash( $meta_key ), is_object( $meta_value ) ? $meta_value : wp_slash( $meta_value ), $prev_value );
276 }
277 }
278 }
279 $this->restore_add_meta_action();
280 }
281
282 unset( $this->prev_value[ $hash ] );
283 $avoid_recursion = false;
284 }
285 }
286
287 /**
288 * Store metas to synchronize before deleting them.
289 *
290 * @since 2.3
291 *
292 * @param int[] $mids Not used.
293 * @param int $id Object ID.
294 * @return void
295 */
296 public function store_metas_to_sync( $mids, $id ) {
297 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
298
299 foreach ( $tr_ids as $lang => $tr_id ) {
300 $this->to_copy[ $id ][ $tr_id ] = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
301 }
302 }
303
304 /**
305 * Synchronizes deleted meta across translations.
306 *
307 * @since 2.3
308 *
309 * @param int[] $mids Not used.
310 * @param int $id Object ID.
311 * @param string $key Meta key.
312 * @param mixed $value Meta value.
313 * @return void
314 */
315 public function delete_meta( $mids, $id, $key, $value ) {
316 static $avoid_recursion = false;
317
318 if ( ! $avoid_recursion ) {
319 $avoid_recursion = true;
320
321 $tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
322
323 foreach ( $tr_ids as $lang => $tr_id ) {
324 if ( $tr_id != $id ) {
325 if ( in_array( $key, $this->to_copy[ $id ][ $tr_id ] ) ) {
326 if ( '' !== $value && null !== $value && false !== $value ) { // Same test as WP
327 $value = $this->maybe_translate_value( $value, $key, $id, $tr_id, $lang );
328 }
329 delete_metadata( $this->meta_type, $tr_id, wp_slash( $key ), is_object( $value ) ? $value : wp_slash( $value ) );
330 }
331 }
332 }
333 }
334
335 $avoid_recursion = false;
336 }
337
338 /**
339 * Copy or synchronize metas
340 *
341 * @since 2.3
342 *
343 * @param int $from Id of the source object
344 * @param int $to Id of the target object
345 * @param string $lang Language code of the target object
346 * @param bool $sync Optional, defaults to true. True if it is synchronization, false if it is a copy
347 * @return void
348 */
349 public function copy( $from, $to, $lang, $sync = false ) {
350 $this->remove_all_meta_actions();
351
352 $to_copy = $this->get_metas_to_copy( $from, $to, $lang, $sync );
353 $metas = get_metadata( $this->meta_type, $from );
354 $tr_metas = get_metadata( $this->meta_type, $to );
355
356 foreach ( $to_copy as $key ) {
357 if ( empty( $metas[ $key ] ) ) {
358 if ( ! empty( $tr_metas[ $key ] ) ) {
359 // If the meta key is not present in the source object, delete all values
360 delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
361 }
362 } else {
363 if ( ! empty( $tr_metas[ $key ] ) && 1 === count( $metas[ $key ] ) && 1 === count( $tr_metas[ $key ] ) ) {
364 // One custom field to update
365 $value = reset( $metas[ $key ] );
366 $value = maybe_unserialize( $value );
367 $to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
368 update_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
369 } else {
370 // Multiple custom fields, either in the source or the target
371 if ( ! empty( $tr_metas[ $key ] ) ) {
372 // The synchronization of multiple values custom fields is easier if we delete all metas first
373 delete_metadata( $this->meta_type, $to, wp_slash( $key ) );
374 }
375
376 foreach ( $metas[ $key ] as $value ) {
377 $value = maybe_unserialize( $value );
378 $to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
379 add_metadata( $this->meta_type, $to, wp_slash( $key ), is_object( $to_value ) ? $to_value : wp_slash( $to_value ) );
380 }
381 }
382 }
383 }
384
385 $this->add_all_meta_actions();
386 }
387
388 /**
389 * If synchronized custom fields were previously not synchronized, it is expected
390 * that saving a post (or term) will synchronize them.
391 *
392 * @since 2.3
393 *
394 * @param int $object_id Id of the object being saved.
395 * @param object $obj Not used.
396 * @param int[] $translations The list of translations object ids.
397 * @return void
398 */
399 public function save_object( $object_id, $obj, $translations ) {
400 foreach ( $translations as $tr_lang => $tr_id ) {
401 if ( $tr_id != $object_id ) {
402 $this->copy( $object_id, $tr_id, $tr_lang, true );
403 }
404 }
405 }
406 }
407