summaryrefslogtreecommitdiff
blob: 1aa8ec866c2bdfb4861ac44c315f3101a7ddf0e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
<?php

/**
 * Add per-post Publicize Connection data.
 *
 * { # Post Object
 *   ...
 *   jetpack_publicize_connections: { # Defined below in this file. See schema for more detail.
 *     id:           (string)  Connection unique_id
 *     service_name: (string)  Service slug
 *     display_name: (string)  User name/display name of user/connection on Service
 *     enabled:      (boolean) Is this connection slated to be shared to? context=edit only
 *     done:         (boolean) Is this post (or connection) done sharing? context=edit only
 *     toggleable:   (boolean) Can the current user change the `enabled` setting for this Connection+Post? context=edit only
 *   }
 *   ...
 *   meta: { # Not defined in this file. Handled in modules/publicize/publicize.php via `register_meta()`
 *     jetpack_publicize_message: (string) The message to use instead of the post's title when sharing.
 *   }
 *   ...
 * }
 *
 * @since 6.8.0
 */
class WPCOM_REST_API_V2_Post_Publicize_Connections_Field extends WPCOM_REST_API_V2_Field_Controller {
	protected $object_type = 'post';
	protected $field_name  = 'jetpack_publicize_connections';

	public $memoized_updates = array();

	/**
	 * Registers the jetpack_publicize_connections field. Called
	 * automatically on `rest_api_init()`.
	 */
	public function register_fields() {
		$this->object_type = get_post_types_by_support( 'publicize' );

		foreach ( $this->object_type as $post_type ) {
			// Adds meta support for those post types that don't already have it.
			// Only runs during REST API requests, so it doesn't impact UI.
			if ( ! post_type_supports( $post_type, 'custom-fields' ) ) {
				add_post_type_support( $post_type, 'custom-fields' );
			}

			add_filter( 'rest_pre_insert_' . $post_type, array( $this, 'rest_pre_insert' ), 10, 2 );
			add_action( 'rest_insert_' . $post_type, array( $this, 'rest_insert' ), 10, 3 );
		}

		parent::register_fields();
	}

	/**
	 * Defines data structure and what elements are visible in which contexts
	 */
	public function get_schema() {
		return array(
			'$schema' => 'http://json-schema.org/draft-04/schema#',
			'title'   => 'jetpack-publicize-post-connections',
			'type'    => 'array',
			'context' => array( 'view', 'edit' ),
			'items'   => $this->post_connection_schema(),
			'default' => array(),
		);
	}

	private function post_connection_schema() {
		return array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'jetpack-publicize-post-connection',
			'type'       => 'object',
			'properties' => array(
				'id'           => array(
					'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'service_name' => array(
					'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'display_name' => array(
					'description' => __( 'Username of the connected account', 'jetpack' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'enabled'      => array(
					'description' => __( 'Whether to share to this connection', 'jetpack' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
				),
				'done'         => array(
					'description' => __( 'Whether Publicize has already finished sharing for this post', 'jetpack' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'toggleable'   => array(
					'description' => __( 'Whether `enable` can be changed for this post/connection', 'jetpack' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
			),
		);
	}

	/**
	 * @param int $post_id
	 * @return true|WP_Error
	 */
	function permission_check( $post_id ) {
		global $publicize;

		if ( $publicize->current_user_can_access_publicize_data( $post_id ) ) {
			return true;
		}

		return new WP_Error(
			'invalid_user_permission_publicize',
			__( 'Sorry, you are not allowed to access Publicize data for this post.', 'jetpack' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Getter permission check
	 *
	 * @param array $post_array Response data from Post Endpoint
	 * @return true|WP_Error
	 */
	function get_permission_check( $post_array, $request ) {
		return $this->permission_check( isset( $post_array['id'] ) ? $post_array['id'] : 0 );

	}

	/**
	 * Setter permission check
	 *
	 * @param WP_Post $post
	 * @return true|WP_Error
	 */
	public function update_permission_check( $value, $post, $request ) {
		return $this->permission_check( isset( $post->ID ) ? $post->ID : 0 );
	}

	/**
	 * Getter: Retrieve current list of connected social accounts for a given post.
	 *
	 * @see Publicize::get_filtered_connection_data()
	 *
	 * @param array           $post_array Response from Post Endpoint
	 * @param WP_REST_Request
	 *
	 * @return array List of connections
	 */
	public function get( $post_array, $request ) {
		global $publicize;

		$schema     = $this->post_connection_schema();
		$properties = array_keys( $schema['properties'] );

		$connections = $publicize->get_filtered_connection_data( $post_array['id'] );

		$output_connections = array();
		foreach ( $connections as $connection ) {
			$output_connection = array();
			foreach ( $properties as $property ) {
				if ( isset( $connection[ $property ] ) ) {
					$output_connection[ $property ] = $connection[ $property ];
				}
			}

			$output_connection['id'] = (string) $connection['unique_id'];

			$output_connections[] = $output_connection;
		}

		return $output_connections;
	}

	/**
	 * Prior to updating the post, first calculate which Services to
	 * Publicize to and which to skip.
	 *
	 * @param object $post Post data to insert/update.
	 * @param WP_REST_Request $request
	 * @return Filtered $post
	 */
	public function rest_pre_insert( $post, $request ) {
		if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
			return $post;
		}

		$permission_check = $this->update_permission_check( $request['jetpack_publicize_connections'], $post, $request );

		if ( is_wp_error( $permission_check ) ) {
			return $permission_check;
		}

		// memoize
		$this->get_meta_to_update( $request['jetpack_publicize_connections'], isset( $post->ID ) ? $post->ID : 0 );

		return $post;
	}

	/**
	 * After creating a new post, update our cached data to reflect
	 * the new post ID.
	 *
	 * @param WP_Post $post
	 * @param WP_REST_Request $request
	 * @param bool $is_new
	 */
	public function rest_insert( $post, $request, $is_new ) {
		if ( ! $is_new ) {
			// An existing post was edited - no need to update
			// our cache - we started out knowing the correct
			// post ID.
			return;
		}

		if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
			return;
		}

		if ( ! isset( $this->memoized_updates[0] ) ) {
			return;
		}

		$this->memoized_updates[ $post->ID ] = $this->memoized_updates[0];
		unset( $this->memoized_updates[0] );
	}

	protected function get_meta_to_update( $requested_connections, $post_id = 0 ) {
		global $publicize;

		if ( isset( $this->memoized_updates[$post_id] ) ) {
			return $this->memoized_updates[$post_id];
		}

		$available_connections = $publicize->get_filtered_connection_data( $post_id );

		$changed_connections = array();

		// Build lookup mappings
		$available_connections_by_unique_id    = array();
		$available_connections_by_service_name = array();
		foreach ( $available_connections as $available_connection ) {
			$available_connections_by_unique_id[ $available_connection['unique_id'] ] = $available_connection;

			if ( ! isset( $available_connections_by_service_name[ $available_connection['service_name'] ] ) ) {
				$available_connections_by_service_name[ $available_connection['service_name'] ] = array();
			}
			$available_connections_by_service_name[ $available_connection['service_name'] ][] = $available_connection;
		}

		// Handle { service_name: $service_name, enabled: (bool) }
		foreach ( $requested_connections as $requested_connection ) {
			if ( ! isset( $requested_connection['service_name'] ) ) {
				continue;
			}

			if ( ! isset( $available_connections_by_service_name[ $requested_connection['service_name'] ] ) ) {
				continue;
			}

			foreach ( $available_connections_by_service_name[ $requested_connection['service_name'] ] as $available_connection ) {
				$changed_connections[ $available_connection['unique_id'] ] = $requested_connection['enabled'];
			}
		}

		// Handle { id: $id, enabled: (bool) }
		// These override the service_name settings
		foreach ( $requested_connections as $requested_connection ) {
			if ( ! isset( $requested_connection['id'] ) ) {
				continue;
			}

			if ( ! isset( $available_connections_by_unique_id[ $requested_connection['id'] ] ) ) {
				continue;
			}

			$changed_connections[ $requested_connection['id'] ] = $requested_connection['enabled'];
		}

		// Set all changed connections to their new value
		foreach ( $changed_connections as $unique_id => $enabled ) {
			$connection = $available_connections_by_unique_id[ $unique_id ];

			if ( $connection['done'] || ! $connection['toggleable'] ) {
				continue;
			}

			$available_connections_by_unique_id[ $unique_id ]['enabled'] = $enabled;
		}

		$meta_to_update = array();
		// For all connections, ensure correct post_meta
		foreach ( $available_connections_by_unique_id as $unique_id => $available_connection ) {
			if ( $available_connection['enabled'] ) {
				$meta_to_update[$publicize->POST_SKIP . $unique_id] = null;
			} else {
				$meta_to_update[$publicize->POST_SKIP . $unique_id] = 1;
			}
		}

		$this->memoized_updates[$post_id] = $meta_to_update;

		return $meta_to_update;
	}

	/**
	 * Update the connections slated to be shared to.
	 *
	 * @param array           $requested_connections
	 *              Items are either `{ id: (string) }` or `{ service_name: (string) }`
	 * @param WP_Post         $post
	 * @param WP_REST_Request
	 */
	public function update( $requested_connections, $post, $request ) {
		foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) {
			if ( is_null( $meta_value ) ) {
				delete_post_meta( $post->ID, $meta_key );
			} else {
				update_post_meta( $post->ID, $meta_key, $meta_value );
			}
		}
	}
}

if ( Jetpack::is_module_active( 'publicize' ) ) {
	wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Post_Publicize_Connections_Field' );
}