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 |