PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / rest-api / class-connections-post-field.php

class-connections-post-field.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3, at jetpack_vendor/automattic/jetpack-publicize/src/rest-api/class-connections-post-field.php

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