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 / REST_Save.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
REST_Save.php
264 lines
1 <?php
2 /**
3 * SCF datastore integration.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 namespace SCF\Datastore;
9
10 /**
11 * Handles SCF datastore saves during Gutenberg / REST post requests.
12 *
13 * Decodes the _acf transport meta included on REST post requests, writes
14 * individual field meta to the post and (when applicable) to the revision,
15 * then cleans up the transport blob. Also strips the transport blob from
16 * REST responses so it never leaks to clients.
17 */
18 class REST_Save {
19
20 /**
21 * Decoded SCF values from the current REST save.
22 * Set in save_post_rest(), consumed in save_revision_meta().
23 *
24 * @since ACF 6.8.1
25 * @var array|null
26 */
27 private $current_acf_values = null;
28
29 /**
30 * Post ID pending _acf cleanup.
31 * Set in save_post_rest(), consumed in cleanup_acf_transport_meta().
32 *
33 * @since ACF 6.8.1
34 * @var integer|null
35 */
36 private $pending_cleanup_post_id = null;
37
38 /**
39 * Constructor.
40 *
41 * Defers hook registration to rest_api_init so the
42 * acf/settings/enable_datastore filter is available to themes
43 * and plugins by the time the gate is evaluated.
44 *
45 * @since ACF 6.8.1
46 */
47 public function __construct() {
48 add_action( 'rest_api_init', array( $this, 'maybe_register_rest_save_hooks' ) );
49
50 // Skip the legacy ACF_Form_Post::save_post() during the metabox AJAX
51 // (meta-box-loader) that follows each Gutenberg REST save -- the REST
52 // path has already saved the values, so re-running the legacy save
53 // would clobber them.
54 add_filter( 'acf/form-post/skip_save', array( $this, 'skip_metabox_loader_save' ), 10, 3 );
55 }
56
57 /**
58 * Returns true when the current request is the meta-box-loader AJAX
59 * and the datastore is enabled.
60 *
61 * @since ACF 6.8.1
62 *
63 * @param boolean $skip Whether the save should be skipped.
64 * @param integer $post_id The post ID being saved.
65 * @param mixed $post The post being saved.
66 * @return boolean
67 */
68 public function skip_metabox_loader_save( $skip, $post_id, $post ) {
69 unset( $post_id, $post );
70
71 if ( $skip ) {
72 return $skip;
73 }
74
75 return acf_maybe_get_GET( 'meta-box-loader', false ) && acf_is_using_datastore();
76 }
77
78 /**
79 * Conditionally registers REST save hooks for all public post types.
80 *
81 * @since ACF 6.8.1
82 *
83 * @return void
84 */
85 public function maybe_register_rest_save_hooks() {
86 if ( ! acf_is_using_datastore() ) {
87 return;
88 }
89
90 add_action( '_wp_put_post_revision', array( $this, 'save_revision_meta' ), 11, 2 );
91
92 foreach ( get_post_types( array( 'show_in_rest' => true ) ) as $post_type ) {
93 // Post-save: decode _acf and write individual meta keys to post + revision.
94 add_action( "rest_after_insert_{$post_type}", array( $this, 'save_post_rest' ), 10, 2 );
95
96 // Strip _acf from post REST responses. Revisions use
97 // rest_prepare_revision instead, so _acf passes through
98 // for the revision viewer.
99 add_filter( "rest_prepare_{$post_type}", array( $this, 'strip_acf_transport_meta' ) );
100 }
101
102 /**
103 * Autosave: WP_REST_Autosaves_Controller does not fire
104 * rest_after_insert_{post_type}, so this response filter is the only
105 * hook available in all autosave paths that carries the request object.
106 * No-ops on GET requests since the request body is empty.
107 */
108 add_filter( 'rest_prepare_autosave', array( $this, 'save_autosave_rest' ), 10, 3 );
109
110 // Fallback cleanup for the _acf transport meta. Runs at priority 20,
111 // after the revision system (priority 9) has finished deciding whether
112 // to create a revision. Catches orphaned _acf when no revision is
113 // created (e.g., post type doesn't support revisions).
114 add_action( 'wp_after_insert_post', array( $this, 'cleanup_acf_transport_meta' ), 20 );
115 }
116
117 /**
118 * Writes SCF field values to the revision after WordPress creates it.
119 *
120 * Called via _wp_put_post_revision, which fires AFTER rest_after_insert
121 * (where save_post_rest writes values to the post). At this point the
122 * decoded values are available in $this->current_acf_values.
123 *
124 * @since ACF 6.8.1
125 *
126 * @param integer $revision_id The revision ID.
127 * @param integer $post_id The parent post ID.
128 * @return void
129 */
130 public function save_revision_meta( $revision_id, $post_id ) {
131 if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
132 return;
133 }
134
135 $is_autosave = wp_is_post_autosave( $revision_id );
136
137 // Clean up the transport-only _acf blob from the post.
138 // wp_save_revisioned_meta_fields (priority 10) has already copied
139 // it to the revision for future comparison.
140 if ( ! $is_autosave ) {
141 delete_post_meta( $post_id, '_acf' );
142 }
143
144 if ( empty( $this->current_acf_values ) || $is_autosave ) {
145 return;
146 }
147
148 $values = $this->current_acf_values;
149
150 if ( ! acf_allow_unfiltered_html() ) {
151 $values = wp_kses_post_deep( $values );
152 }
153
154 acf_update_values( $values, $revision_id );
155
156 $this->current_acf_values = null;
157 }
158
159 /**
160 * Processes SCF field values from the REST request.
161 * Decodes the _acf blob and saves individual meta keys to the post.
162 *
163 * Revision meta is handled separately by save_revision_meta(), which
164 * fires later via _wp_put_post_revision after WordPress creates the
165 * revision inside wp_after_insert_post().
166 *
167 * @since ACF 6.8.1
168 *
169 * @param \WP_Post $post The post object.
170 * @param \WP_REST_Request $request The REST request.
171 * @return void
172 */
173 public function save_post_rest( $post, $request ) {
174 // Check if _acf data was included in the request.
175 $meta = $request->get_param( 'meta' );
176 if ( empty( $meta['_acf'] ) ) {
177 return;
178 }
179
180 // Retrieve and decode the JSON.
181 $acf_json = get_post_meta( $post->ID, '_acf', true );
182 $values = is_string( $acf_json ) ? json_decode( $acf_json, true ) : null;
183
184 if ( empty( $values ) || ! is_array( $values ) ) {
185 return;
186 }
187
188 // Save individual meta keys to the post.
189 // Fires acf/save_post action, runs all ACF processing hooks.
190 acf_save_post( $post->ID, $values );
191
192 // Store values for save_revision_meta(), which fires later
193 // via _wp_put_post_revision (after wp_after_insert_post creates
194 // the revision). Don't delete _acf yet -- the revision comparison
195 // needs it on the post when deciding whether to create a revision.
196 // cleanup_acf_transport_meta() handles deletion after the revision
197 // system finishes.
198 $this->current_acf_values = $values;
199 $this->pending_cleanup_post_id = $post->ID;
200 }
201
202 /**
203 * Handles SCF values during autosave REST requests.
204 *
205 * @since ACF 6.8.1
206 *
207 * @param \WP_REST_Response $response The response object.
208 * @param \WP_Post $post The post object.
209 * @param \WP_REST_Request $request The REST request.
210 * @return \WP_REST_Response
211 */
212 public function save_autosave_rest( $response, $post, $request ) {
213 $this->save_post_rest( $post, $request );
214 return $response;
215 }
216
217 /**
218 * Cleans up the transport-only _acf meta after the revision system finishes.
219 *
220 * Hooked to wp_after_insert_post at priority 20, which runs after
221 * wp_save_post_revision_on_insert (priority 9). This catches orphaned
222 * _acf when no revision is created (e.g., post type doesn't support
223 * revisions). When a revision IS created, save_revision_meta() already
224 * deleted _acf, so this is a harmless no-op.
225 *
226 * @since ACF 6.8.1
227 *
228 * @param integer $post_id The post ID.
229 * @return void
230 */
231 public function cleanup_acf_transport_meta( $post_id ) {
232 if ( null === $this->pending_cleanup_post_id || (int) $this->pending_cleanup_post_id !== (int) $post_id ) {
233 return;
234 }
235
236 delete_post_meta( $post_id, '_acf' );
237 $this->current_acf_values = null;
238 $this->pending_cleanup_post_id = null;
239 }
240
241 /**
242 * Strips _acf from post REST responses.
243 *
244 * The _acf meta is transport-only and should not appear in post
245 * responses. Revision responses use rest_prepare_revision instead,
246 * so _acf passes through for the revision viewer.
247 *
248 * @since ACF 6.8.1
249 *
250 * @param \WP_REST_Response $response The response object.
251 * @return \WP_REST_Response
252 */
253 public function strip_acf_transport_meta( $response ) {
254 $data = $response->get_data();
255
256 if ( isset( $data['meta']['_acf'] ) ) {
257 $data['meta']['_acf'] = '';
258 $response->set_data( $data );
259 }
260
261 return $response;
262 }
263 }
264