PluginProbe
Advanced Custom Fields (ACF®) / 6.8.1
Advanced Custom Fields (ACF®) v6.8.1
6.8.9 6.8.8 6.8.7 6.8.6 6.8.5 6.8.4 6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 All 230 releases
advanced-custom-fields / includes / revisions.php
revisions.php
484 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 ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 if ( ! class_exists( 'acf_revisions' ) ) :
17 class acf_revisions {
18
19 /**
20 * An array to cache post IDs for revisions.
21 * @var array
22 */
23 public $cache = array();
24
25 /**
26 * Constructs the acf_revisions class.
27 */
28 public function __construct() {
29 add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
30 add_filter( '_wp_post_revision_fields', array( $this, 'wp_preview_post_fields' ), 10, 2 );
31 add_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ), 10, 2 );
32 add_filter( 'acf/validate_post_id', array( $this, 'acf_validate_post_id' ), 10, 2 );
33
34 // WP 6.4+ handles things differently.
35 if ( version_compare( get_bloginfo( 'version' ), '6.4', '>=' ) ) {
36 add_action( '_wp_put_post_revision', array( $this, 'maybe_save_revision' ), 10, 2 );
37 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, 'check_acf_fields_have_changed' ), 9, 3 );
38 add_filter( 'wp_post_revision_meta_keys', array( $this, 'wp_post_revision_meta_keys' ) );
39
40 $this->register_meta();
41 } else {
42 add_filter( 'wp_save_post_revision_check_for_changes', array( $this, 'wp_save_post_revision_check_for_changes' ), 10, 3 );
43 }
44 }
45
46 /**
47 * Registers any ACF meta that should be sent the REST/Gutenberg request.
48 * For now, this is just our "_acf_changed" key that we use to detect if ACF fields have changed.
49 *
50 * @since 6.2.6
51 */
52 public function register_meta() {
53 register_meta(
54 'post',
55 '_acf_changed',
56 array(
57 'type' => 'boolean',
58 'single' => true,
59 'show_in_rest' => true,
60 'revisions_enabled' => true,
61 'auth_callback' => '__return_true',
62 )
63 );
64 }
65
66 /**
67 * Lets WordPress know which meta keys to include in revisions.
68 * For now, this is just our "_acf_changed" key, as we still handle revisions ourselves.
69 *
70 * @since 6.2.6
71 *
72 * @param array $keys The meta keys that should be revisioned.
73 * @return array
74 */
75 public function wp_post_revision_meta_keys( $keys ) {
76 $keys[] = '_acf_changed';
77 return $keys;
78 }
79
80 /**
81 * Helps WordPress determine if fields have changed, and if in a legacy
82 * metabox AJAX request, copies the metadata to the new revision.
83 *
84 * @since 6.2.6
85 *
86 * @param boolean $post_has_changed True if the post has changed, false if not.
87 * @param WP_Post $last_revision The WP_Post object for the latest revision.
88 * @param WP_Post $post The WP_Post object for the parent post.
89 * @return boolean
90 */
91 public function check_acf_fields_have_changed( $post_has_changed, $last_revision, $post ) {
92 if ( apply_filters( 'acf/revisions/skip_legacy_metabox_handling', false ) ) {
93 return $post_has_changed;
94 }
95
96 if ( acf_maybe_get_GET( 'meta-box-loader', false ) ) {
97 // We're in a legacy AJAX request, so we copy fields over to the latest revision.
98 $this->maybe_save_revision( $last_revision->ID, $post->ID );
99 } elseif ( acf_maybe_get_POST( '_acf_changed', false ) ) {
100 // We're in a classic editor save request, so notify WP that fields have changed.
101 $post_has_changed = true;
102 }
103
104 // Let WordPress decide for REST/block editor requests.
105 return $post_has_changed;
106 }
107
108 /**
109 * Copies ACF field data to the latest revision.
110 *
111 * @since 6.2.6
112 *
113 * @param integer $revision_id The ID of the revision that was just created.
114 * @param integer $post_id The ID of the post being updated.
115 * @return void
116 */
117 public function maybe_save_revision( $revision_id, $post_id ) {
118 if ( apply_filters( 'acf/revisions/skip_legacy_metabox_handling', false ) ) {
119 return;
120 }
121
122 // We don't have anything to copy over yet.
123 if ( ! did_action( 'acf/save_post' ) ) {
124 delete_metadata( 'post', $post_id, '_acf_changed' );
125 delete_metadata( 'post', $revision_id, '_acf_changed' );
126 return;
127 }
128
129 // Bail if this is an autosave in Classic Editor, it already has the field values.
130 if ( acf_maybe_get_POST( '_acf_changed' ) && wp_is_post_autosave( $revision_id ) ) {
131 return;
132 }
133
134 // Copy the saved meta from the main post to the latest revision.
135 acf_save_post_revision( $post_id );
136 }
137
138 /**
139 * This function is used to trick WP into thinking that one of the $post's fields has changed and
140 * will allow an autosave to be updated.
141 * Fixes an odd bug causing the preview page to render the non autosave post data on every odd attempt
142 *
143 * @type function
144 * @date 21/10/2014
145 * @since 5.1.0
146 *
147 * @param $fields (array)
148 * @return $fields
149 */
150 function wp_preview_post_fields( $fields ) {
151
152 // bail early if not previewing a post
153 if ( acf_maybe_get_POST( 'wp-preview' ) !== 'dopreview' ) {
154 return $fields;
155 }
156
157 // add to fields if ACF has changed
158 if ( acf_maybe_get_POST( '_acf_changed' ) ) {
159 $fields['_acf_changed'] = 'different than 1';
160 }
161
162 // return
163 return $fields;
164 }
165
166
167 /**
168 * This filter will return false and force WP to save a revision. This is required due to
169 * WP checking only post_title, post_excerpt and post_content values, not custom fields.
170 *
171 * @type filter
172 * @date 19/09/13
173 *
174 * @param boolean $return defaults to true
175 * @param object $last_revision the last revision that WP will compare against
176 * @param object $post the $post object that WP will compare against
177 * @return boolean $return
178 */
179 function wp_save_post_revision_check_for_changes( $return, $last_revision, $post ) {
180
181 // if acf has changed, return false and prevent WP from performing 'compare' logic
182 if ( acf_maybe_get_POST( '_acf_changed' ) ) {
183 return false;
184 }
185
186 // return
187 return $return;
188 }
189
190
191 /**
192 * This filter will add the ACF fields to the returned array
193 * Versions 3.5 and 3.6 of WP feature different uses of the revisions filters, so there are
194 * some hacks to allow both versions to work correctly
195 *
196 * @type filter
197 * @date 11/08/13
198 *
199 * @param $post_id (int)
200 * @return $post_id (int)
201 */
202 function wp_post_revision_fields( $fields, $post = null ) {
203
204 // validate page
205 if ( acf_is_screen( 'revision' ) || acf_is_ajax( 'get-revision-diffs' ) ) {
206
207 // bail early if is restoring
208 if ( acf_maybe_get_GET( 'action' ) === 'restore' ) {
209 return $fields;
210 }
211
212 // allow
213 } else {
214
215 // bail early (most likely saving a post)
216 return $fields;
217 }
218
219 // vars
220 $append = array();
221 $order = array();
222 $post_id = acf_maybe_get( $post, 'ID' );
223
224 // compatibility with WP < 4.5 (test)
225 if ( ! $post_id ) {
226 global $post;
227 $post_id = $post->ID;
228 }
229
230 // get all postmeta
231 $meta = get_post_meta( $post_id );
232
233 // bail early if no meta
234 if ( ! $meta ) {
235 return $fields;
236 }
237
238 // loop
239 foreach ( $meta as $name => $value ) {
240
241 // attempt to find key value
242 $key = acf_maybe_get( $meta, '_' . $name );
243
244 // bail early if no key
245 if ( ! $key ) {
246 continue;
247 }
248
249 // update vars
250 $value = $value[0];
251 $key = $key[0];
252
253 // Load field.
254 $field = acf_get_field( $key );
255 if ( ! $field ) {
256 continue;
257 }
258
259 // get field
260 $field_title = $field['label'] . ' (' . $name . ')';
261 $field_order = $field['menu_order'];
262 $ancestors = acf_get_field_ancestors( $field );
263
264 // ancestors
265 if ( ! empty( $ancestors ) ) {
266
267 // vars
268 $count = count( $ancestors );
269 $oldest = acf_get_field( $ancestors[ $count - 1 ] );
270
271 // update vars
272 $field_title = str_repeat( '- ', $count ) . $field_title;
273 $field_order = $oldest['menu_order'] . '.1';
274 }
275
276 // append
277 $append[ $name ] = $field_title;
278 $order[ $name ] = $field_order;
279
280 // hook into specific revision field filter and return local value
281 add_filter( "_wp_post_revision_field_{$name}", array( $this, 'wp_post_revision_field' ), 10, 4 );
282 }
283
284 // append
285 if ( ! empty( $append ) ) {
286
287 // vars
288 $prefix = '_';
289
290 // add prefix
291 $append = acf_add_array_key_prefix( $append, $prefix );
292 $order = acf_add_array_key_prefix( $order, $prefix );
293
294 // sort by name (orders sub field values correctly)
295 array_multisort( $order, $append );
296
297 // remove prefix
298 $append = acf_remove_array_key_prefix( $append, $prefix );
299
300 // append
301 $fields = $fields + $append;
302 }
303
304 // return
305 return $fields;
306 }
307
308 /**
309 * Load the value for the given field and return it for rendering.
310 *
311 * @param mixed $value Should be false as it has not yet been loaded.
312 * @param string $field_name The name of the field
313 * @param mixed $post Holds the $post object to load from - in WP 3.5, this is not passed!
314 * @param string $direction To / from - not used.
315 * @return string $value
316 */
317 public function wp_post_revision_field( $value, $field_name, $post = null, $direction = false ) {
318 // Bail early if is empty.
319 if ( empty( $value ) ) {
320 return '';
321 }
322
323 $value = acf_maybe_unserialize( $value );
324 $post_id = $post->ID;
325
326 // load field.
327 $field = acf_maybe_get_field( $field_name, $post_id );
328
329 // default formatting.
330 if ( is_array( $value ) ) {
331 $value = implode( ', ', $value );
332 } elseif ( is_object( $value ) ) {
333 $value = serialize( $value );
334 }
335
336 // image.
337 if ( is_array( $field ) && isset( $field['type'] ) && ( $field['type'] === 'image' || $field['type'] === 'file' ) ) {
338 $url = wp_get_attachment_url( $value );
339 $value = $value . ' (' . $url . ')';
340 }
341
342 return $value;
343 }
344
345 /**
346 * This action will copy and paste the metadata from a revision to the post
347 *
348 * @type action
349 * @date 11/08/13
350 *
351 * @param $parent_id (int) the destination post
352 * @return $revision_id (int) the source post
353 */
354 function wp_restore_post_revision( $post_id, $revision_id ) {
355
356 // copy postmeta from revision to post (restore from revision)
357 acf_copy_postmeta( $revision_id, $post_id );
358
359 // Make sure the latest revision is also updated to match the new $post data
360 // get latest revision
361 $revision = acf_get_post_latest_revision( $post_id );
362
363 // save
364 if ( $revision ) {
365
366 // copy postmeta from revision to latest revision (potentialy may be the same, but most likely are different)
367 acf_copy_postmeta( $revision_id, $revision->ID );
368 }
369 }
370
371
372 /**
373 * This function will modify the $post_id and allow loading values from a revision
374 *
375 * @type function
376 * @date 6/3/17
377 * @since 5.5.10
378 *
379 * @param $post_id (int)
380 * @param $_post_id (int)
381 * @return $post_id (int)
382 */
383 function acf_validate_post_id( $post_id, $_post_id ) {
384
385 // phpcs:disable WordPress.Security.NonceVerification.Recommended
386 // bail early if no preview in URL
387 if ( ! isset( $_GET['preview'] ) ) {
388 return $post_id;
389 }
390
391 // bail early if $post_id is not numeric
392 if ( ! is_numeric( $post_id ) ) {
393 return $post_id;
394 }
395
396 // vars
397 $k = $post_id;
398 $preview_id = 0;
399
400 // check cache
401 if ( isset( $this->cache[ $k ] ) ) {
402 return $this->cache[ $k ];
403 }
404
405 // validate
406 if ( isset( $_GET['preview_id'] ) ) {
407 $preview_id = (int) $_GET['preview_id'];
408 } elseif ( isset( $_GET['p'] ) ) {
409 $preview_id = (int) $_GET['p'];
410 } elseif ( isset( $_GET['page_id'] ) ) {
411 $preview_id = (int) $_GET['page_id'];
412 }
413 // phpcs:enable WordPress.Security.NonceVerification.Recommended
414
415 // bail early id $preview_id does not match $post_id
416 if ( $preview_id != $post_id ) {
417 return $post_id;
418 }
419
420 // attempt find revision
421 $revision = acf_get_post_latest_revision( $post_id );
422
423 // save
424 if ( $revision && $revision->post_parent == $post_id ) {
425 $post_id = (int) $revision->ID;
426 }
427
428 // set cache
429 $this->cache[ $k ] = $post_id;
430
431 // return
432 return $post_id;
433 }
434 }
435
436 // initialize
437 acf()->revisions = new acf_revisions();
438 endif; // class_exists check
439
440
441 /**
442 * This function will copy meta from a post to it's latest revision
443 *
444 * @type function
445 * @date 26/09/2016
446 * @since 5.4.0
447 *
448 * @param $post_id (int)
449 * @return n/a
450 */
451 function acf_save_post_revision( $post_id = 0 ) {
452
453 // get latest revision
454 $revision = acf_get_post_latest_revision( $post_id );
455
456 // save
457 if ( $revision ) {
458 acf_copy_postmeta( $post_id, $revision->ID );
459 }
460 }
461
462
463 /**
464 * This function will return the latest revision for a given post
465 *
466 * @type function
467 * @date 25/06/2016
468 * @since 5.3.8
469 *
470 * @param $post_id (int)
471 * @return $post_id (int)
472 */
473 function acf_get_post_latest_revision( $post_id ) {
474
475 // vars
476 $revisions = wp_get_post_revisions( $post_id );
477
478 // shift off and return first revision (will return null if no revisions)
479 $revision = array_shift( $revisions );
480
481 // return
482 return $revision;
483 }
484