PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / includes / Datastore / Revisions.php
secure-custom-fields / includes / Datastore Last commit date
Check_Screen.php 1 month ago Localization.php 1 month ago REST_Save.php 1 month ago Revisions.php 1 month ago
Revisions.php
154 lines
1 <?php
2 /**
3 * SCF datastore integration.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 namespace SCF\Datastore;
9
10 /**
11 * SCF datastore integration with the WordPress revisions system.
12 *
13 * Registers the _acf transport meta as a revisioned key so changes to ACF
14 * field values trigger revision creation, and short-circuits the legacy
15 * metabox-AJAX-driven revision path during REST requests (where REST_Save
16 * is in charge instead).
17 */
18 class Revisions {
19
20 /**
21 * Constructor.
22 *
23 * Register_meta is deferred to the `init` hook so themes and plugins
24 * have a chance to filter `acf/settings/enable_datastore` before the
25 * gate is evaluated.
26 *
27 * @since ACF 6.8.1
28 */
29 public function __construct() {
30 add_action( 'init', array( $this, 'register_meta' ) );
31 add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_acf_to_revision_meta_keys' ) );
32 add_filter( 'acf/revisions/skip_legacy_metabox_handling', array( $this, 'skip_during_rest' ) );
33 }
34
35 /**
36 * Registers the _acf transport meta when the datastore is enabled.
37 *
38 * _acf carries field values in the REST request. revisions_enabled is
39 * false here -- _acf is conditionally added to wp_post_revision_meta_keys
40 * only during REST requests so it triggers revision creation without
41 * causing duplicate revisions from the metabox AJAX (meta-box-loader)
42 * that follows each REST save. _acf is stripped from non-revision REST
43 * responses via rest_prepare_{post_type} in REST_Save.
44 *
45 * @since ACF 6.8.1
46 *
47 * @return void
48 */
49 public function register_meta() {
50 if ( ! acf_is_using_datastore() ) {
51 return;
52 }
53
54 register_meta(
55 'post',
56 '_acf',
57 array(
58 'type' => 'string',
59 'single' => true,
60 'show_in_rest' => true,
61 'revisions_enabled' => false,
62 'auth_callback' => function ( $allowed, $meta_key, $object_id, $user_id ) {
63 return user_can( $user_id, 'edit_post', $object_id );
64 },
65 'sanitize_callback' => function ( $value ) {
66 if ( ! is_string( $value ) ) {
67 return '';
68 }
69 $decoded = json_decode( $value, true );
70 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
71 return '';
72 }
73 return wp_json_encode( self::canonicalize_acf_value( $decoded ) );
74 },
75 )
76 );
77 }
78
79 /**
80 * Adds _acf to the list of revisioned meta keys during REST requests.
81 *
82 * _acf triggers revision creation when SCF values change. During the
83 * metabox AJAX (meta-box-loader) that follows each Gutenberg REST save,
84 * _acf must not be compared -- wp_update_post() fires again for metabox
85 * re-rendering and would create a duplicate revision.
86 *
87 * @since ACF 6.8.1
88 *
89 * @param array $keys The meta keys that should be revisioned.
90 * @return array
91 */
92 public function add_acf_to_revision_meta_keys( $keys ) {
93 if ( acf_is_using_datastore() && defined( 'REST_REQUEST' ) && REST_REQUEST ) {
94 $keys[] = '_acf';
95 }
96
97 return $keys;
98 }
99
100 /**
101 * Tells acf_revisions to skip the legacy metabox handling on REST requests.
102 *
103 * Hooked to the acf/revisions/skip_legacy_metabox_handling filter. During
104 * a REST save, REST_Save copies field values to the post and revision,
105 * so the legacy metabox-AJAX-driven path in acf_revisions must not run.
106 *
107 * @since ACF 6.8.1
108 *
109 * @param boolean $skip Whether to skip the legacy handling.
110 * @return boolean
111 */
112 public function skip_during_rest( $skip ) {
113 if ( ! acf_is_using_datastore() ) {
114 return $skip;
115 }
116
117 return $skip || ( defined( 'REST_REQUEST' ) && REST_REQUEST );
118 }
119
120 /**
121 * Recursively sorts associative array keys for canonical JSON encoding.
122 *
123 * Sequential arrays (repeater rows, flexible content layouts, multi-value
124 * selections) keep their user-defined order; associative arrays -- at any
125 * level, regardless of whether keys are ACF field keys, the acf_fc_layout
126 * discriminator, or other string keys (e.g. link field title/url/target) --
127 * are sorted by key. JSON object keys are semantically unordered, so this
128 * is a no-op for consumers but makes the stored bytes byte-stable across
129 * saves so WordPress's revision meta byte comparison treats reordered
130 * saves as equal.
131 *
132 * @since ACF 6.8.1
133 *
134 * @param mixed $value Decoded JSON value.
135 * @return mixed
136 */
137 private static function canonicalize_acf_value( $value ) {
138 if ( ! is_array( $value ) || array() === $value ) {
139 return $value;
140 }
141
142 $is_sequential = array_keys( $value ) === range( 0, count( $value ) - 1 );
143 if ( ! $is_sequential ) {
144 ksort( $value );
145 }
146
147 foreach ( $value as $k => $v ) {
148 $value[ $k ] = self::canonicalize_acf_value( $v );
149 }
150
151 return $value;
152 }
153 }
154