Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.48% covered (warning)
70.48%
191 / 271
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connections_Post_Field
70.48% covered (warning)
70.48%
191 / 271
35.71% covered (danger)
35.71%
5 / 14
351.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_fields
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 get_schema
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 post_connection_schema
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
1 / 1
1
 permission_check
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
7.10
 get
52.78% covered (warning)
52.78%
19 / 36
0.00% covered (danger)
0.00%
0 / 1
42.96
 rest_pre_insert
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
7.10
 rest_insert
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 get_meta_to_update
51.16% covered (warning)
51.16%
22 / 43
0.00% covered (danger)
0.00%
0 / 1
72.37
 update
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
9.16
 save_connection_overrides
77.78% covered (warning)
77.78%
21 / 27
0.00% covered (danger)
0.00%
0 / 1
17.47
 sanitize_attached_media
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
8.12
 filter_response_by_context
31.82% covered (danger)
31.82%
7 / 22
0.00% covered (danger)
0.00%
0 / 1
57.64
 is_valid_for_context
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Registers the API field for Publicize connections.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize\REST_API;
9
10use Automattic\Jetpack\Publicize\Publicize_Base;
11use WP_Error;
12use WP_Post;
13use WP_REST_Request;
14
15/**
16 * The class to register the field and augment requests
17 * to Publicize supported post types.
18 *
19 * @phan-constructor-used-for-side-effects
20 */
21class Connections_Post_Field {
22
23    const FIELD_NAME = 'jetpack_publicize_connections';
24
25    /**
26     * Array of post IDs that have been updated.
27     *
28     * @var array
29     */
30    private $meta_saved = array();
31
32    /**
33     * Used to memoize the updates for a given post.
34     *
35     * @var array
36     */
37    public $memoized_updates = array();
38
39    /**
40     * Constructor.
41     */
42    public function __construct() {
43        add_action( 'rest_api_init', array( $this, 'register_fields' ) );
44    }
45
46    /**
47     * Registers the jetpack_publicize_connections field. Called
48     * automatically on `rest_api_init()`.
49     */
50    public function register_fields() {
51        $post_types = get_post_types_by_support( 'publicize' );
52        foreach ( $post_types as $post_type ) {
53            // Adds meta support for those post types that don't already have it.
54            // Only runs during REST API requests, so it doesn't impact UI.
55            if ( ! post_type_supports( $post_type, 'custom-fields' ) ) {
56                add_post_type_support( $post_type, 'custom-fields' );
57            }
58
59            // We use these hooks and not the update_callback because we must updateth meta
60            // before we set the post as published, otherwise the wrong connections could be used.
61            add_filter( 'rest_pre_insert_' . $post_type, array( $this, 'rest_pre_insert' ), 10, 2 );
62            add_action( 'rest_insert_' . $post_type, array( $this, 'rest_insert' ), 10, 3 );
63
64            register_rest_field(
65                $post_type,
66                self::FIELD_NAME,
67                array(
68                    'get_callback' => array( $this, 'get' ),
69                    'schema'       => $this->get_schema(),
70                )
71            );
72        }
73    }
74
75    /**
76     * Defines data structure and what elements are visible in which contexts
77     */
78    public function get_schema() {
79        return array(
80            '$schema' => 'http://json-schema.org/draft-04/schema#',
81            'title'   => 'jetpack-publicize-post-connections',
82            'type'    => 'array',
83            'context' => array( 'view', 'edit' ),
84            'items'   => $this->post_connection_schema(),
85            'default' => array(),
86        );
87    }
88
89    /**
90     * Schema for the endpoint.
91     */
92    private function post_connection_schema() {
93        $connection_fields = Connections_Controller::get_the_item_schema();
94        $deprecated_fields = array(
95            'id'             => array(
96                'type'        => 'string',
97                'description' => __( 'Unique identifier for the Jetpack Social connection.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
98                    /* translators: %s is the new field name */
99                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
100                    'connection_id'
101                ),
102            ),
103            'username'       => array(
104                'type'        => 'string',
105                'description' => __( 'Username of the connected account.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
106                    /* translators: %s is the new field name */
107                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
108                    'external_handle'
109                ),
110            ),
111            'can_disconnect' => array(
112                'description' => __( 'Whether the current user can disconnect this connection.', 'jetpack-publicize-pkg' ) . ' ' . __( 'Deprecated.', 'jetpack-publicize-pkg' ),
113                'type'        => 'boolean',
114                'context'     => array( 'view', 'edit' ),
115                'readonly'    => true,
116            ),
117        );
118
119        return array(
120            '$schema'    => 'http://json-schema.org/draft-04/schema#',
121            'title'      => 'jetpack-publicize-post-connection',
122            'type'       => 'object',
123            'properties' => array_merge(
124                $deprecated_fields,
125                $connection_fields,
126                array(
127                    'enabled'        => array(
128                        'description' => __( 'Whether to share to this connection.', 'jetpack-publicize-pkg' ),
129                        'type'        => 'boolean',
130                        'context'     => array( 'edit' ),
131                    ),
132                    'message'        => array(
133                        'description' => __( 'Custom message to use for this connection instead of the global message.', 'jetpack-publicize-pkg' ),
134                        'type'        => 'string',
135                        'context'     => array( 'edit' ),
136                    ),
137                    'attached_media' => array(
138                        'description' => __( 'Custom media to attach for this connection instead of the global media.', 'jetpack-publicize-pkg' ),
139                        'type'        => 'array',
140                        'context'     => array( 'edit' ),
141                        'items'       => array(
142                            'type'       => 'object',
143                            'properties' => array(
144                                'id'   => array( 'type' => 'number' ),
145                                'url'  => array( 'type' => 'string' ),
146                                'type' => array( 'type' => 'string' ),
147                            ),
148                        ),
149                    ),
150                    'media_source'   => array(
151                        'type' => 'string',
152                        'enum' => array(
153                            'featured-image',
154                            'sig',
155                            'media-library',
156                            'upload-video',
157                            'none',
158                        ),
159                    ),
160                )
161            ),
162        );
163    }
164
165    /**
166     * Permission check, based on module availability and user capabilities.
167     *
168     * @param int $post_id Post ID.
169     *
170     * @return true|WP_Error
171     */
172    public function permission_check( $post_id ) {
173        global $publicize;
174
175        if ( ! $publicize ) {
176            return new WP_Error(
177                'publicize_not_available',
178                __( 'Sorry, Jetpack Social is not available on your site right now.', 'jetpack-publicize-pkg' ),
179                array( 'status' => rest_authorization_required_code() )
180            );
181        }
182
183        if ( $publicize->current_user_can_access_publicize_data( $post_id ) ) {
184            return true;
185        }
186
187        return new WP_Error(
188            'invalid_user_permission_publicize',
189            __( 'Sorry, you are not allowed to access Jetpack Social data for this post.', 'jetpack-publicize-pkg' ),
190            array( 'status' => rest_authorization_required_code() )
191        );
192    }
193
194    /**
195     * The field's wrapped getter. Does permission checks and output preparation.
196     *
197     * This cannot be extended: implement `->get()` instead.
198     *
199     * @param mixed           $post_array Probably an array. Whatever the endpoint returns.
200     * @param string          $field_name  Should always match `->field_name`.
201     * @param WP_REST_Request $request     WP API request.
202     * @param string          $object_type Should always match `->object_type`.
203     *
204     * @return mixed
205     */
206    public function get( $post_array, $field_name, $request, $object_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
207        global $publicize;
208
209        $post_id          = $post_array['id'] ?? 0;
210        $full_schema      = $this->get_schema();
211        $permission_check = $this->permission_check( $post_id );
212        if ( is_wp_error( $permission_check ) ) {
213            return $full_schema['default'];
214        }
215
216        $schema      = $full_schema['items'];
217        $properties  = array_keys( $schema['properties'] );
218        $connections = $publicize->get_filtered_connection_data( $post_id );
219
220        if ( $publicize && $publicize->has_paid_features() ) {
221            // Check if per-network customization is enabled.
222            $customize_per_network = get_post_meta( $post_id, Publicize_Base::POST_CUSTOMIZE_PER_NETWORK, true );
223            // Get per-connection overrides from post meta.
224            $connection_overrides = get_post_meta( $post_id, Publicize_Base::POST_CONNECTION_OVERRIDES, true );
225
226            if ( ! is_array( $connection_overrides ) ) {
227                $connection_overrides = array();
228            }
229        } else {
230            $customize_per_network = false;
231            $connection_overrides  = array();
232        }
233
234        $output_connections = array();
235        foreach ( $connections as $connection ) {
236            $output_connection = array();
237            foreach ( $properties as $property ) {
238                if ( isset( $connection[ $property ] ) ) {
239                    $output_connection[ $property ] = $connection[ $property ];
240                }
241            }
242
243            // Merge per-connection overrides if global flag is enabled.
244            $connection_id = $connection['connection_id'] ?? '';
245            if ( $customize_per_network && ! empty( $connection_id ) && isset( $connection_overrides[ $connection_id ] ) ) {
246                $override = $connection_overrides[ $connection_id ];
247                if ( isset( $override['message'] ) ) {
248                    $output_connection['message'] = $override['message'];
249                }
250                if ( isset( $override['attached_media'] ) ) {
251                    $output_connection['attached_media'] = $override['attached_media'];
252                }
253                if ( isset( $override['media_source'] ) ) {
254                    $output_connection['media_source'] = $override['media_source'];
255                }
256            }
257
258            $output_connections[] = $output_connection;
259        }
260
261        // TODO: Work out if this is necessary. We shouldn't be creating an invalid value here.
262        $is_valid = rest_validate_value_from_schema( $output_connections, $full_schema, self::FIELD_NAME );
263        if ( is_wp_error( $is_valid ) ) {
264            return $is_valid;
265        }
266
267        $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
268        return $this->filter_response_by_context( $output_connections, $full_schema, $context );
269    }
270
271    /**
272     * Prior to updating the post, first calculate which Services to
273     * Publicize to and which to skip.
274     *
275     * @param object          $post    Post data to insert/update.
276     * @param WP_REST_Request $request API request.
277     *
278     * @return object|WP_Error Filtered $post
279     */
280    public function rest_pre_insert( $post, $request ) {
281        $request_connections = ! empty( $request['jetpack_publicize_connections'] ) ? $request['jetpack_publicize_connections'] : array();
282
283        $permission_check = $this->permission_check( empty( $post->ID ) ? 0 : $post->ID );
284        if ( is_wp_error( $permission_check ) ) {
285            return empty( $request_connections ) ? $post : $permission_check;
286        }
287        // memoize.
288        $this->get_meta_to_update( $request_connections, isset( $post->ID ) ? $post->ID : 0 );
289
290        if ( isset( $post->ID ) ) {
291            // Set the meta before we mark the post as published so that publicize works as expected.
292            // If this is not the case post end up on social media when they are marked as skipped.
293            $this->update( $request_connections, $post, $request );
294        }
295
296        return $post;
297    }
298
299    /**
300     * After creating a new post, update our cached data to reflect
301     * the new post ID.
302     *
303     * @param WP_Post         $post    Post data to update.
304     * @param WP_REST_Request $request API request.
305     * @param bool            $is_new  Is this a new post.
306     */
307    public function rest_insert( $post, $request, $is_new ) {
308        if ( ! $is_new ) {
309            // An existing post was edited - no need to update
310            // our cache - we started out knowing the correct
311            // post ID.
312            return;
313        }
314
315        if ( ! isset( $this->memoized_updates[0] ) ) {
316            return;
317        }
318
319        $this->memoized_updates[ $post->ID ] = $this->memoized_updates[0];
320        unset( $this->memoized_updates[0] );
321    }
322
323    /**
324     * Get list of meta data to update per post ID.
325     *
326     * @param array $requested_connections Publicize connections to update.
327     *              Items are either `{ id: (string) }` or `{ service_name: (string) }`.
328     * @param int   $post_id    Post ID.
329     */
330    protected function get_meta_to_update( $requested_connections, $post_id = 0 ) {
331        global $publicize;
332
333        if ( ! $publicize || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
334            return array();
335        }
336
337        $post = get_post( $post_id );
338        if ( isset( $post->post_status ) && 'publish' === $post->post_status ) {
339            return array();
340        }
341
342        if ( isset( $this->memoized_updates[ $post_id ] ) ) {
343            return $this->memoized_updates[ $post_id ];
344        }
345
346        $available_connections = $publicize->get_filtered_connection_data( $post_id );
347
348        $changed_connections = array();
349
350        // Build lookup mappings.
351        $available_connections_by_connection_id = array();
352        $available_connections_by_service_name  = array();
353        foreach ( $available_connections as $available_connection ) {
354            $available_connections_by_connection_id[ $available_connection['connection_id'] ] = $available_connection;
355
356            if ( ! isset( $available_connections_by_service_name[ $available_connection['service_name'] ] ) ) {
357                $available_connections_by_service_name[ $available_connection['service_name'] ] = array();
358            }
359            $available_connections_by_service_name[ $available_connection['service_name'] ][] = $available_connection;
360        }
361
362        // Handle { service_name: $service_name, enabled: (bool) }.
363        // If the service is not available, it will be skipped.
364        foreach ( $requested_connections as $requested_connection ) {
365            if ( ! isset( $requested_connection['service_name'] ) ) {
366                continue;
367            }
368
369            if ( ! isset( $available_connections_by_service_name[ $requested_connection['service_name'] ] ) ) {
370                continue;
371            }
372
373            foreach ( $available_connections_by_service_name[ $requested_connection['service_name'] ] as $available_connection ) {
374                if ( $requested_connection['connection_id'] === $available_connection['connection_id'] ) {
375                    $changed_connections[ $available_connection['connection_id'] ] = $requested_connection['enabled'];
376                    break;
377                }
378            }
379        }
380
381        // Handle { id: $id, enabled: (bool) }
382        // These override the service_name settings.
383        foreach ( $requested_connections as $requested_connection ) {
384            if ( ! isset( $requested_connection['connection_id'] ) ) {
385                continue;
386            }
387
388            if ( ! isset( $available_connections_by_connection_id[ $requested_connection['connection_id'] ] ) ) {
389                continue;
390            }
391
392            $changed_connections[ $requested_connection['connection_id'] ] = $requested_connection['enabled'];
393        }
394
395        // Set all changed connections to their new value.
396        foreach ( $changed_connections as $id => $enabled ) {
397            $connection = $available_connections_by_connection_id[ $id ];
398
399            if ( $connection['done'] ) {
400                continue;
401            }
402
403            $available_connections_by_connection_id[ $id ]['enabled'] = $enabled;
404        }
405
406        $meta_to_update = array();
407        // For all connections, ensure correct post_meta.
408        foreach ( $available_connections_by_connection_id as $connection_id => $available_connection ) {
409            if ( $available_connection['enabled'] ) {
410                $meta_to_update[ $publicize->POST_SKIP_PUBLICIZE . $connection_id ] = null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
411            } else {
412                $meta_to_update[ $publicize->POST_SKIP_PUBLICIZE . $connection_id ] = 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
413            }
414        }
415
416        $this->memoized_updates[ $post_id ] = $meta_to_update;
417
418        return $meta_to_update;
419    }
420
421    /**
422     * Update the connections slated to be shared to.
423     *
424     * @param array           $requested_connections Publicize connections to update.
425     *              Items are either `{ id: (string) }` or `{ service_name: (string) }`.
426     * @param WP_Post         $post    Post data.
427     * @param WP_REST_Request $request API request.
428     */
429    public function update( $requested_connections, $post, $request = null ) {
430        global $publicize;
431
432        if ( isset( $this->meta_saved[ $post->ID ] ) ) { // Make sure we only save it once - per request.
433            return;
434        }
435        foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) {
436            if ( null === $meta_value ) {
437                delete_post_meta( $post->ID, $meta_key );
438            } else {
439                update_post_meta( $post->ID, $meta_key, $meta_value );
440            }
441        }
442
443        // Save per-connection overrides.
444        if ( $publicize && $publicize->has_paid_features() ) {
445            $this->save_connection_overrides( $requested_connections, $post->ID, $request );
446        }
447
448        $this->meta_saved[ $post->ID ] = true;
449    }
450
451    /**
452     * Save per-connection customization overrides.
453     *
454     * Extracts message and attached_media from each connection and persists
455     * them to post meta when per-network customization is enabled.
456     *
457     * @param array           $requested_connections Array of connection data from the request.
458     * @param int             $post_id               Post ID.
459     * @param WP_REST_Request $request               API request.
460     */
461    private function save_connection_overrides( $requested_connections, $post_id, $request = null ) {
462        // Check if per-network customization is enabled - prefer request value over database.
463        $customize_per_network = null;
464        if ( $request && isset( $request['meta'][ Publicize_Base::POST_CUSTOMIZE_PER_NETWORK ] ) ) {
465            $customize_per_network = $request['meta'][ Publicize_Base::POST_CUSTOMIZE_PER_NETWORK ];
466        }
467        if ( null === $customize_per_network ) {
468            $customize_per_network = get_post_meta( $post_id, Publicize_Base::POST_CUSTOMIZE_PER_NETWORK, true );
469        }
470
471        // If customization is disabled, remove any existing overrides.
472        if ( ! $customize_per_network ) {
473            delete_post_meta( $post_id, Publicize_Base::POST_CONNECTION_OVERRIDES );
474            return;
475        }
476
477        // If the request does not have connections, skip.
478        if ( ! isset( $request[ self::FIELD_NAME ] ) ) {
479            return;
480        }
481
482        $overrides = array();
483
484        foreach ( $requested_connections as $connection ) {
485            // Only process if connection has a connection_id.
486            if ( empty( $connection['connection_id'] ) ) {
487                continue;
488            }
489
490            // Only save if connection has custom message or attached_media.
491            if ( ! isset( $connection['message'] ) && ! isset( $connection['attached_media'] ) && ! isset( $connection['media_source'] ) ) {
492                continue;
493            }
494
495            $connection_id               = $connection['connection_id'];
496            $overrides[ $connection_id ] = array();
497
498            // Save message (can be empty to use empty message).
499            if ( isset( $connection['message'] ) ) {
500                $overrides[ $connection_id ]['message'] = sanitize_textarea_field( $connection['message'] );
501            }
502
503            // Save attached_media (can be empty array to clear media).
504            if ( isset( $connection['attached_media'] ) ) {
505                $overrides[ $connection_id ]['attached_media'] = $this->sanitize_attached_media( $connection['attached_media'] );
506            }
507
508            // Save media_source (can be empty to use default).
509            if ( isset( $connection['media_source'] ) ) {
510                $overrides[ $connection_id ]['media_source'] = sanitize_text_field( $connection['media_source'] );
511            }
512        }
513
514        // Only save if there are overrides, otherwise delete the meta.
515        if ( ! empty( $overrides ) ) {
516            update_post_meta( $post_id, Publicize_Base::POST_CONNECTION_OVERRIDES, $overrides );
517        } else {
518            delete_post_meta( $post_id, Publicize_Base::POST_CONNECTION_OVERRIDES );
519        }
520    }
521
522    /**
523     * Sanitize attached media array.
524     *
525     * @param array $attached_media Array of media items.
526     * @return array Sanitized array of media items.
527     */
528    private function sanitize_attached_media( $attached_media ) {
529        if ( empty( $attached_media ) ) {
530            return array();
531        }
532
533        $sanitized = array();
534
535        foreach ( $attached_media as $media_item ) {
536            if ( ! is_array( $media_item ) ) {
537                continue;
538            }
539
540            $sanitized_item = array();
541
542            if ( isset( $media_item['id'] ) ) {
543                $sanitized_item['id'] = absint( $media_item['id'] );
544            }
545
546            if ( isset( $media_item['url'] ) ) {
547                $sanitized_item['url'] = esc_url_raw( $media_item['url'] );
548            }
549
550            if ( isset( $media_item['type'] ) ) {
551                $sanitized_item['type'] = sanitize_text_field( $media_item['type'] );
552            }
553
554            if ( ! empty( $sanitized_item ) ) {
555                $sanitized[] = $sanitized_item;
556            }
557        }
558
559        return $sanitized;
560    }
561
562    /**
563     * Removes properties that should not appear in the current
564     * request's context
565     *
566     * $context is a Core REST API Framework request attribute that is
567     * always one of:
568     * * view (what you see on the blog)
569     * * edit (what you see in an editor)
570     * * embed (what you see in, e.g., an oembed)
571     *
572     * Fields (and sub-fields, and sub-sub-...) can be flagged for a
573     * set of specific contexts via the field's schema.
574     *
575     * The Core API will filter out top-level fields with the wrong
576     * context, but will not recurse deeply enough into arrays/objects
577     * to remove all levels of sub-fields with the wrong context.
578     *
579     * This function handles that recursion.
580     *
581     * @param mixed  $value   Value passed to API request.
582     * @param array  $schema  Schema to validate against.
583     * @param string $context REST API Request context.
584     *
585     * @return mixed Filtered $value
586     */
587    public function filter_response_by_context( $value, $schema, $context ) {
588        if ( ! $this->is_valid_for_context( $schema, $context ) ) {
589            // We use this intentionally odd looking WP_Error object
590            // internally only in this recursive function (see below
591            // in the `object` case). It will never be output by the REST API.
592            // If we return this for the top level object, Core
593            // correctly remove the top level object from the response
594            // for us.
595            return new WP_Error( '__wrong-context__' );
596        }
597
598        switch ( $schema['type'] ) {
599            case 'array':
600                if ( ! isset( $schema['items'] ) ) {
601                    return $value;
602                }
603
604                // Shortcircuit if we know none of the items are valid for this context.
605                // This would only happen in a strangely written schema.
606                if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
607                    return array();
608                }
609
610                // Recurse to prune sub-properties of each item.
611                foreach ( $value as $key => $item ) {
612                    $value[ $key ] = $this->filter_response_by_context( $item, $schema['items'], $context );
613                }
614
615                return $value;
616            case 'object':
617                if ( ! isset( $schema['properties'] ) ) {
618                    return $value;
619                }
620
621                foreach ( $value as $field_name => $field_value ) {
622                    if ( isset( $schema['properties'][ $field_name ] ) ) {
623                        $field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context );
624                        if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
625                            unset( $value[ $field_name ] );
626                        } else {
627                            // Respect recursion that pruned sub-properties of each property.
628                            $value[ $field_name ] = $field_value;
629                        }
630                    }
631                }
632
633                return (object) $value;
634        }
635
636        return $value;
637    }
638
639    /**
640     * Ensure that our request matches its expected context.
641     *
642     * @param array  $schema  Schema to validate against.
643     * @param string $context REST API Request context.
644     * @return bool
645     */
646    private function is_valid_for_context( $schema, $context ) {
647        return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
648    }
649}