Completed
Push — renovate/gridicons-3.x ( c004c1...f8ccd4 )
by
unknown
284:06 queued 275:32
created

modules/publicize/publicize.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
abstract class Publicize_Base {
4
5
	/**
6
	* Services that are currently connected to the given user
7
	* through publicize.
8
	*/
9
	public $connected_services = array();
10
11
	/**
12
	* Services that are supported by publicize. They don't
13
	* necessarily need to be connected to the current user.
14
	*/
15
	public $services;
16
17
	/**
18
	* key names for post meta
19
	*/
20
	public $ADMIN_PAGE        = 'wpas';
21
	public $POST_MESS         = '_wpas_mess';
22
	public $POST_SKIP         = '_wpas_skip_'; // connection id appended to indicate that a connection should NOT be publicized to
23
	public $POST_DONE         = '_wpas_done_'; // connection id appended to indicate a connection has already been publicized to
24
	public $USER_AUTH         = 'wpas_authorize';
25
	public $USER_OPT          = 'wpas_';
26
	public $PENDING           = '_publicize_pending'; // ready for Publicize to do its thing
27
	public $POST_SERVICE_DONE = '_publicize_done_external'; // array of external ids where we've Publicized
28
29
	/**
30
	* default pieces of the message used in constructing the
31
	* content pushed out to other social networks
32
	*/
33
34
	public $default_prefix  = '';
35
	public $default_message = '%title%';
36
	public $default_suffix  = ' ';
37
38
	/**
39
	 * What WP capability is require to create/delete global connections?
40
	 * All users with this cap can un-globalize all other global connections, and globalize any of their own
41
	 * Globalized connections cannot be unselected by users without this capability when publishing
42
	 */
43
	public $GLOBAL_CAP = 'publish_posts';
44
45
	/**
46
	* Sets up the basics of Publicize
47
	*/
48
	function __construct() {
49
		$this->default_message = self::build_sprintf( array(
50
			/**
51
			 * Filter the default Publicize message.
52
			 *
53
			 * @module publicize
54
			 *
55
			 * @since 2.0.0
56
			 *
57
			 * @param string $this->default_message Publicize's default message. Default is the post title.
58
			 */
59
			apply_filters( 'wpas_default_message', $this->default_message ),
60
			'title',
61
			'url',
62
		) );
63
64
		$this->default_prefix = self::build_sprintf( array(
65
			/**
66
			 * Filter the message prepended to the Publicize custom message.
67
			 *
68
			 * @module publicize
69
			 *
70
			 * @since 2.0.0
71
			 *
72
			 * @param string $this->default_prefix String prepended to the Publicize custom message.
73
			 */
74
			apply_filters( 'wpas_default_prefix', $this->default_prefix ),
75
			'url',
76
		) );
77
78
		$this->default_suffix = self::build_sprintf( array(
79
			/**
80
			 * Filter the message appended to the Publicize custom message.
81
			 *
82
			 * @module publicize
83
			 *
84
			 * @since 2.0.0
85
			 *
86
			 * @param string $this->default_suffix String appended to the Publicize custom message.
87
			 */
88
			apply_filters( 'wpas_default_suffix', $this->default_suffix ),
89
			'url',
90
		) );
91
92
		/**
93
		 * Filter the capability to change global Publicize connection options.
94
		 *
95
		 * All users with this cap can un-globalize all other global connections, and globalize any of their own
96
		 * Globalized connections cannot be unselected by users without this capability when publishing.
97
		 *
98
		 * @module publicize
99
		 *
100
		 * @since 2.2.1
101
		 *
102
		 * @param string $this->GLOBAL_CAP default capability in control of global Publicize connection options. Default to edit_others_posts.
103
		 */
104
		$this->GLOBAL_CAP = apply_filters( 'jetpack_publicize_global_connections_cap', $this->GLOBAL_CAP );
105
106
		// stage 1 and 2 of 3-stage Publicize. Flag for Publicize on creation, save meta,
107
		// then check meta and publicize based on that. stage 3 implemented on wpcom
108
		add_action( 'transition_post_status', array( $this, 'flag_post_for_publicize' ), 10, 3 );
109
		add_action( 'save_post', array( &$this, 'save_meta' ), 20, 2 );
110
111
		// Default checkbox state for each Connection
112
		add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 4 );
113
114
		// Alter the "Post Publish" admin notice to mention the Connections we Publicized to.
115
		add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
116
117
		// Connection test callback
118
		add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) );
119
120
		add_action( 'init', array( $this, 'add_post_type_support' ) );
121
		add_action( 'init', array( $this, 'register_post_meta' ), 20 );
122
		add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_extension' ) );
123
	}
124
125
/*
126
 * Services: Facebook, Twitter, etc.
127
 */
128
129
	/**
130
	 * Get services for the given blog and user.
131
	 *
132
	 * Can return all available services or just the ones with an active connection.
133
	 *
134
	 * @param string $filter
135
	 *        'all' (default) - Get all services available for connecting
136
	 *        'connected'     - Get all services currently connected
137
	 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
138
	 * @param false|int $_user_id The user ID. Use false (default) for the current user
139
	 * @return array
140
	 */
141
	abstract function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
142
143
	function can_connect_service( $service_name ) {
144
		return true;
145
	}
146
147
	/**
148
	 * Does the given user have a connection to the service on the given blog?
149
	 *
150
	 * @param string $service_name 'facebook', 'twitter', etc.
151
	 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
152
	 * @param false|int $_user_id The user ID. Use false (default) for the current user
153
	 * @return bool
154
	 */
155
	function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) {
156
		if ( !$_blog_id )
157
			$_blog_id = $this->blog_id();
158
159
		if ( !$_user_id )
160
			$_user_id = $this->user_id();
161
162
		$connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
163
		return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
164
	}
165
166
	/**
167
	 * Generates a connection URL.
168
	 *
169
	 * This is the URL, which, when visited by the user, starts the authentication
170
	 * process required to forge a connection.
171
	 *
172
	 * @param string $service_name 'facebook', 'twitter', etc.
173
	 * @return string
174
	 */
175
	abstract function connect_url( $service_name );
176
177
	/**
178
	 * Generates a Connection refresh URL.
179
	 *
180
	 * This is the URL, which, when visited by the user, re-authenticates their
181
	 * connection to the service.
182
	 *
183
	 * @param string $service_name 'facebook', 'twitter', etc.
184
	 * @return string
185
	 */
186
	abstract function refresh_url( $service_name );
187
188
	/**
189
	 * Generates a disconnection URL.
190
	 *
191
	 * This is the URL, which, when visited by the user, breaks their connection
192
	 * with the service.
193
	 *
194
	 * @param string $service_name 'facebook', 'twitter', etc.
195
	 * @param string $connection_id Connection ID
196
	 * @return string
197
	 */
198
	abstract function disconnect_url( $service_name, $connection_id );
199
200
	/**
201
	 * Returns a display name for the Service
202
	 *
203
	 * @param string $service_name 'facebook', 'twitter', etc.
204
	 * @return string
205
	 */
206
	public static function get_service_label( $service_name ) {
207
		switch ( $service_name ) {
208
			case 'linkedin':
209
				return 'LinkedIn';
210
				break;
211
			case 'twitter':
212
			case 'facebook':
213
			case 'tumblr':
214
			default:
215
				return ucfirst( $service_name );
216
				break;
217
		}
218
	}
219
220
/*
221
 * Connections: For each Service, there can be multiple connections
222
 * for a given user. For example, one user could be connected to Twitter
223
 * as both @jetpack and as @wordpressdotcom
224
 *
225
 * For historical reasons, Connections are represented as an object
226
 * on WordPress.com and as an array in Jetpack.
227
 */
228
229
	/**
230
	 * Get the active Connections of a Service
231
	 *
232
	 * @param string $service_name 'facebook', 'twitter', etc.
233
	 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
234
	 * @param false|int $_user_id The user ID. Use false (default) for the current user
235
	 * @return false|object[]|array[] false if no connections exist
236
	 */
237
	abstract function get_connections( $service_name, $_blog_id = false, $_user_id = false );
238
239
	/**
240
	 * Get a single Connection of a Service
241
	 *
242
	 * @param string $service_name 'facebook', 'twitter', etc.
243
	 * @param string $connection_id Connection ID
244
	 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
245
	 * @param false|int $_user_id The user ID. Use false (default) for the current user
246
	 * @return false|object[]|array[] false if no connections exist
247
	 */
248
	abstract function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false );
249
250
	/**
251
	 * Get the Connection ID.
252
	 *
253
	 * Note that this is different than the Connection's uniqueid.
254
	 *
255
	 * Via a quirk of history, ID is globally unique and unique_id
256
	 * is only unique per site.
257
	 *
258
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
259
	 * @return string
260
	 */
261
	abstract function get_connection_id( $connection );
262
263
	/**
264
	 * Get the Connection unique_id
265
	 *
266
	 * Note that this is different than the Connections ID.
267
	 *
268
	 * Via a quirk of history, ID is globally unique and unique_id
269
	 * is only unique per site.
270
	 *
271
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
272
	 * @return string
273
	 */
274
	abstract function get_connection_unique_id( $connection );
275
276
	/**
277
	 * Get the Connection's Meta data
278
	 *
279
	 * @param object|array Connection
280
	 * @return array Connection Meta
281
	 */
282
	abstract function get_connection_meta( $connection );
283
284
	/**
285
	 * Disconnect a Connection
286
	 *
287
	 * @param string $service_name 'facebook', 'twitter', etc.
288
	 * @param string $connection_id Connection ID
289
	 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
290
	 * @param false|int $_user_id The user ID. Use false (default) for the current user
291
	 * @param bool $force_delete Whether to skip permissions checks
292
	 * @return false|void False on failure. Void on success.
293
	 */
294
	abstract function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
295
296
	/**
297
	 * Globalizes a Connection
298
	 *
299
	 * @param string $connection_id Connection ID
300
	 * @return bool Falsey on failure. Truthy on success.
301
	 */
302
	abstract function globalize_connection( $connection_id );
303
304
	/**
305
	 * Unglobalizes a Connection
306
	 *
307
	 * @param string $connection_id Connection ID
308
	 * @return bool Falsey on failure. Truthy on success.
309
	 */
310
	abstract function unglobalize_connection( $connection_id );
311
312
	/**
313
	 * Returns an external URL to the Connection's profile
314
	 *
315
	 * @param string $service_name 'facebook', 'twitter', etc.
316
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
317
	 * @return false|string False on failure. URL on success.
318
	 */
319
	function get_profile_link( $service_name, $connection ) {
320
		$cmeta = $this->get_connection_meta( $connection );
321
322
		if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
323
			if ( 'facebook' == $service_name && 0 === strpos( parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
324
				// App-scoped Facebook user IDs are not usable profile links
325
				return false;
326
			}
327
328
			return $cmeta['connection_data']['meta']['link'];
329 View Code Duplication
		} elseif ( 'facebook' == $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
330
			return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
331
		} elseif ( 'tumblr' == $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
332
			 return 'http://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
333
		} elseif ( 'twitter' == $service_name ) {
334
			return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'
335
		} else if ( 'linkedin' == $service_name ) {
336
			if ( !isset( $cmeta['connection_data']['meta']['profile_url'] ) ) {
337
				return false;
338
			}
339
340
			$profile_url_query = parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY );
341
			wp_parse_str( $profile_url_query, $profile_url_query_args );
342
			if ( isset( $profile_url_query_args['key'] ) ) {
343
				$id = $profile_url_query_args['key'];
344
			} elseif ( isset( $profile_url_query_args['id'] ) ) {
345
				$id = $profile_url_query_args['id'];
346
			} else {
347
				return false;
348
			}
349
350
			return esc_url_raw( add_query_arg( 'id', urlencode( $id ), 'http://www.linkedin.com/profile/view' ) );
351
		} else {
352
			return false; // no fallback. we just won't link it
353
		}
354
	}
355
356
	/**
357
	 * Returns a display name for the Connection
358
	 *
359
	 * @param string $service_name 'facebook', 'twitter', etc.
360
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
361
	 * @return string
362
	 */
363
	function get_display_name( $service_name, $connection ) {
364
		$cmeta = $this->get_connection_meta( $connection );
365
366
		if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
367
			return $cmeta['connection_data']['meta']['display_name'];
368 View Code Duplication
		} elseif ( $service_name == 'tumblr' && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
369
			 return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
370
		} elseif ( $service_name == 'twitter' ) {
371
			return $cmeta['external_display'];
372
		} else {
373
			$connection_display = $cmeta['external_display'];
374
			if ( empty( $connection_display ) )
375
				$connection_display = $cmeta['external_name'];
376
			return $connection_display;
377
		}
378
	}
379
380
	/**
381
	 * Whether the user needs to select additional options after connecting
382
	 *
383
	 * @param string $service_name 'facebook', 'twitter', etc.
384
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
385
	 * @return bool
386
	 */
387
	function show_options_popup( $service_name, $connection ) {
388
		$cmeta = $this->get_connection_meta( $connection );
389
390
		// always show if no selection has been made for facebook
391
		if ( 'facebook' == $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) )
392
			return true;
393
394
		// always show if no selection has been made for tumblr
395
		if ( 'tumblr' == $service_name && empty ( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) )
396
			return true;
397
398
		// if we have the specific connection info..
399
		if ( isset( $_GET['id'] ) ) {
400
			if ( $cmeta['connection_data']['id'] == $_GET['id'] )
401
				return true;
402
		} else {
403
			// otherwise, just show if this is the completed step / first load
404
			if ( !empty( $_GET['action'] ) && 'completed' == $_GET['action'] && !empty( $_GET['service'] ) && $service_name == $_GET['service'] && ! in_array( $_GET['service'], array( 'facebook', 'tumblr' ) ) )
405
				return true;
406
		}
407
408
		return false;
409
	}
410
411
	/**
412
	 * Whether the Connection is "valid" wrt Facebook's requirements.
413
	 *
414
	 * Must be connected to a Page (not a Profile).
415
	 * (Also returns true if we're in the middle of the connection process)
416
	 *
417
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
418
	 * @return bool
419
	 */
420
	function is_valid_facebook_connection( $connection ) {
421
		if ( $this->is_connecting_connection( $connection ) ) {
422
			return true;
423
		}
424
		$connection_meta = $this->get_connection_meta( $connection );
425
		$connection_data = $connection_meta['connection_data'];
426
		return isset( $connection_data[ 'meta' ][ 'facebook_page' ] );
427
	}
428
429
	/**
430
	 * LinkedIn needs to be reauthenticated to use v2 of their API.
431
	 * If it's using LinkedIn old API, it's an 'invalid' connection
432
	 *
433
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
434
	 * @return bool
435
	 */
436
	function is_invalid_linkedin_connection( $connection ) {
437
		// LinkedIn API v1 included the profile link in the connection data.
438
		$connection_meta = $this->get_connection_meta( $connection );
439
		return isset( $connection_meta['connection_data']['meta']['profile_url'] );
440
	}
441
442
	/**
443
	 * Whether the Connection currently being connected
444
	 *
445
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
446
	 * @return bool
447
	 */
448
	function is_connecting_connection( $connection ) {
449
		$connection_meta = $this->get_connection_meta( $connection );
450
		$connection_data = $connection_meta['connection_data'];
451
		return isset( $connection_data[ 'meta' ]['options_responses'] );
452
	}
453
454
	/**
455
	 * AJAX Handler to run connection tests on all Connections
456
	 * @return void
457
	 */
458
	function test_publicize_conns() {
459
		wp_send_json_success( $this->get_publicize_conns_test_results() );
460
	}
461
462
	/**
463
	 * Run connection tests on all Connections
464
	 *
465
	 * @return array {
466
	 *     Array of connection test results.
467
	 *
468
	 *     @type string 'connectionID'          Connection identifier string that is unique for each connection
469
	 *     @type string 'serviceName'           Slug of the connection's service (facebook, twitter, ...)
470
	 *     @type bool   'connectionTestPassed'  Whether the connection test was successful
471
	 *     @type string 'connectionTestMessage' Test success or error message
472
	 *     @type bool   'userCanRefresh'        Whether the user can re-authenticate their connection to the service
473
	 *     @type string 'refreshText'           Message instructing user to re-authenticate their connection to the service
474
	 *     @type string 'refreshURL'            URL, which, when visited by the user, re-authenticates their connection to the service.
475
	 *     @type string 'unique_id'             ID string representing connection
476
	 * }
477
	 */
478
	function get_publicize_conns_test_results() {
479
		$test_results = array();
480
481
		foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
482
			foreach ( $connections as $connection ) {
483
484
				$id = $this->get_connection_id( $connection );
485
486
				$connection_test_passed = true;
487
				$connection_test_message = __( 'This connection is working correctly.' , 'jetpack' );
488
				$user_can_refresh = false;
489
				$refresh_text = '';
490
				$refresh_url = '';
491
492
				$connection_test_result = true;
493
				if ( method_exists( $this, 'test_connection' ) ) {
494
					$connection_test_result = $this->test_connection( $service_name, $connection );
495
				}
496
497
				if ( is_wp_error( $connection_test_result ) ) {
498
					$connection_test_passed = false;
499
					$connection_test_message = $connection_test_result->get_error_message();
0 ignored issues
show
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
500
					$error_data = $connection_test_result->get_error_data();
0 ignored issues
show
The method get_error_data() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
501
502
					$user_can_refresh = $error_data['user_can_refresh'];
503
					$refresh_text = $error_data['refresh_text'];
504
					$refresh_url = $error_data['refresh_url'];
505
				}
506
				// Mark facebook profiles as deprecated
507
				if ( 'facebook' === $service_name ) {
508
					if ( ! $this->is_valid_facebook_connection( $connection ) ) {
509
						$connection_test_passed = false;
510
						$user_can_refresh = false;
511
						$connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack' );
512
					}
513
				}
514
515
				// LinkedIn needs reauthentication to be compatible with v2 of their API
516
				if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) {
517
					$connection_test_passed = 'must_reauth';
518
					$user_can_refresh = false;
519
					$connection_test_message = esc_html__( 'Your LinkedIn connection needs to be reauthenticated to continue working – head to Sharing to take care of it.', 'jetpack' );
520
				}
521
522
				$unique_id = null;
523 View Code Duplication
				if ( ! empty( $connection->unique_id ) ) {
524
					$unique_id = $connection->unique_id;
525
				} else if ( ! empty( $connection['connection_data']['token_id'] ) ) {
526
					$unique_id = $connection['connection_data']['token_id'];
527
				}
528
529
				$test_results[] = array(
530
					'connectionID'          => $id,
531
					'serviceName'           => $service_name,
532
					'connectionTestPassed'  => $connection_test_passed,
533
					'connectionTestMessage' => esc_attr( $connection_test_message ),
534
					'userCanRefresh'        => $user_can_refresh,
535
					'refreshText'           => esc_attr( $refresh_text ),
536
					'refreshURL'            => $refresh_url,
537
					'unique_id'             => $unique_id,
538
				);
539
			}
540
		}
541
542
		return $test_results;
543
	}
544
545
	/**
546
	 * Run the connection test for the Connection
547
	 *
548
	 * @param string $service_name 'facebook', 'twitter', etc.
549
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
550
	 * @return WP_Error|true WP_Error on failure. True on success
551
	 */
552
	abstract function test_connection( $service_name, $connection );
553
554
	/**
555
	 * Retrieves current list of connections and applies filters.
556
	 *
557
	 * Retrieves current available connections and checks if the connections
558
	 * have already been used to share current post. Finally, the checkbox
559
	 * form UI fields are calculated. This function exposes connection form
560
	 * data directly as array so it can be retrieved for static HTML generation
561
	 * or JSON consumption.
562
	 *
563
	 * @since 6.7.0
564
	 *
565
	 * @param integer $selected_post_id Optional. Post ID to query connection status for.
566
	 *
567
	 * @return array {
568
	 *     Array of UI setup data for connection list form.
569
	 *
570
	 *     @type string 'unique_id'     ID string representing connection
571
	 *     @type string 'service_name'  Slug of the connection's service (facebook, twitter, ...)
572
	 *     @type string 'service_label' Service Label (Facebook, Twitter, ...)
573
	 *     @type string 'display_name'  Connection's human-readable Username: "@jetpack"
574
	 *     @type bool   'enabled'       Default value for the connection (e.g., for a checkbox).
575
	 *     @type bool   'done'          Has this connection already been publicized to?
576
	 *     @type bool   'toggleable'    Is the user allowed to change the value for the connection?
577
	 *     @type bool   'global'        Is this connection a global one?
578
	 * }
579
	 */
580
	public function get_filtered_connection_data( $selected_post_id = null ) {
581
		$connection_list = array();
582
583
		$post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null.
584
		// Handle case where there is no current post.
585
		if ( ! empty( $post ) ) {
586
			$post_id = $post->ID;
587
		} else {
588
			$post_id = null;
589
		}
590
591
		$services = $this->get_services( 'connected' );
592
		$all_done = $this->post_is_done_sharing( $post_id );
593
594
		// We don't allow Publicizing to the same external id twice, to prevent spam.
595
		$service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
596
597
		foreach ( $services as $service_name => $connections ) {
598
			foreach ( $connections as $connection ) {
599
				$connection_meta = $this->get_connection_meta( $connection );
600
				$connection_data = $connection_meta['connection_data'];
601
602
				$unique_id = $this->get_connection_unique_id( $connection );
603
604
605
				// Was this connection (OR, old-format service) already Publicized to?
606
				$done = ! empty( $post ) && (
607
					// New flags
608
					1 == get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
609
					||
610
					// old flags
611
					1 == get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
612
				);
613
614
				/**
615
				 * Filter whether a post should be publicized to a given service.
616
				 *
617
				 * @module publicize
618
				 *
619
				 * @since 2.0.0
620
				 *
621
				 * @param bool true Should the post be publicized to a given service? Default to true.
622
				 * @param int $post_id Post ID.
623
				 * @param string $service_name Service name.
624
				 * @param array $connection_data Array of information about all Publicize details for the site.
625
				 */
626
				if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection_data ) ) {
627
					continue;
628
				}
629
630
				// Should we be skipping this one?
631
				$skip = (
632
					(
633
						! empty( $post )
634
						&&
635
						in_array( $post->post_status, array( 'publish', 'draft', 'future' ) )
636
						&&
637
						(
638
							// New flags
639
							get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
640
							||
641
							// Old flags
642
							get_post_meta( $post->ID, $this->POST_SKIP . $service_name )
643
						)
644
					)
645
					||
646
					(
647
						is_array( $connection )
648
						&&
649
						isset( $connection_meta['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection_meta['external_id'] ] )
650
					)
651
				);
652
653
				// If this one has already been publicized to, don't let it happen again.
654
				$toggleable = ! $done && ! $all_done;
655
656
				// Determine the state of the checkbox (on/off) and allow filtering.
657
				$enabled = $done || ! $skip;
658
				/**
659
				 * Filter the checkbox state of each Publicize connection appearing in the post editor.
660
				 *
661
				 * @module publicize
662
				 *
663
				 * @since 2.0.1
664
				 *
665
				 * @param bool $enabled Should the Publicize checkbox be enabled for a given service.
666
				 * @param int $post_id Post ID.
667
				 * @param string $service_name Service name.
668
				 * @param array $connection Array of connection details.
669
				 */
670
				$enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
671
672
				/**
673
				 * If this is a global connection and this user doesn't have enough permissions to modify
674
				 * those connections, don't let them change it.
675
				 */
676
				if ( ! $done && ( 0 == $connection_data['user_id'] && ! current_user_can( $this->GLOBAL_CAP ) ) ) {
677
					$toggleable = false;
678
679
					/**
680
					 * Filters the checkboxes for global connections with non-prilvedged users.
681
					 *
682
					 * @module publicize
683
					 *
684
					 * @since 3.7.0
685
					 *
686
					 * @param bool   $enabled Indicates if this connection should be enabled. Default true.
687
					 * @param int    $post_id ID of the current post
688
					 * @param string $service_name Name of the connection (Facebook, Twitter, etc)
689
					 * @param array  $connection Array of data about the connection.
690
					 */
691
					$enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection );
692
				}
693
694
				// Force the checkbox to be checked if the post was DONE, regardless of what the filter does.
695
				if ( $done ) {
696
					$enabled = true;
697
				}
698
699
				$connection_list[] = array(
700
					'unique_id'     => $unique_id,
701
					'service_name'  => $service_name,
702
					'service_label' => $this->get_service_label( $service_name ),
703
					'display_name'  => $this->get_display_name( $service_name, $connection ),
704
705
					'enabled'      => $enabled,
706
					'done'         => $done,
707
					'toggleable'   => $toggleable,
708
					'global'       => 0 == $connection_data['user_id'],
709
				);
710
			}
711
		}
712
713
		return $connection_list;
714
	}
715
716
	/**
717
	 * Checks if post has already been shared by Publicize in the past.
718
	 *
719
	 * @since 6.7.0
720
	 *
721
	 * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
722
	 *
723
	 * @return bool True if post has already been shared by Publicize, false otherwise.
724
	 */
725
	abstract public function post_is_done_sharing( $post_id = null );
726
727
	/**
728
	 * Retrieves full list of available Publicize connection services.
729
	 *
730
	 * Retrieves current available publicize service connections
731
	 * with associated labels and URLs.
732
	 *
733
	 * @since 6.7.0
734
	 *
735
	 * @return array {
736
	 *     Array of UI service connection data for all services
737
	 *
738
	 *     @type string 'name'  Name of service.
739
	 *     @type string 'label' Display label for service.
740
	 *     @type string 'url'   URL for adding connection to service.
741
	 * }
742
	 */
743
	function get_available_service_data() {
744
		$available_services     = $this->get_services( 'all' );
745
		$available_service_data = array();
746
747
		foreach ( $available_services as $service_name => $service ) {
748
			$available_service_data[] = array(
749
				'name'  => $service_name,
750
				'label' => $this->get_service_label( $service_name ),
751
				'url'   => $this->connect_url( $service_name ),
752
			);
753
		}
754
755
		return $available_service_data;
756
	}
757
758
/*
759
 * Site Data
760
 */
761
762
	function user_id() {
763
		return get_current_user_id();
764
	}
765
766
	function blog_id() {
767
		return get_current_blog_id();
768
	}
769
770
/*
771
 * Posts
772
 */
773
774
	/**
775
	 * Checks old and new status to see if the post should be flagged as
776
	 * ready to Publicize.
777
	 *
778
	 * Attached to the `transition_post_status` filter.
779
	 *
780
	 * @param string $new_status
781
	 * @param string $old_status
782
	 * @param WP_Post $post
783
	 * @return void
784
	 */
785
	abstract function flag_post_for_publicize( $new_status, $old_status, $post );
786
787
	/**
788
	 * Ensures the Post internal post-type supports `publicize`
789
	 *
790
	 * This feature support flag is used by the REST API.
791
	 */
792
	function add_post_type_support() {
793
		add_post_type_support( 'post', 'publicize' );
794
	}
795
796
	/**
797
	 * Register the Publicize Gutenberg extension
798
	 */
799
	function register_gutenberg_extension() {
800
		// TODO: The `gutenberg/available-extensions` endpoint currently doesn't accept a post ID,
801
		// so we cannot pass one to `$this->current_user_can_access_publicize_data()`.
802
803
		if ( $this->current_user_can_access_publicize_data() ) {
804
			Jetpack_Gutenberg::set_extension_available( 'jetpack/publicize' );
805
		} else {
806
			Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/publicize', 'unauthorized' );
807
808
		}
809
	}
810
811
	/**
812
	 * Can the current user access Publicize Data.
813
	 *
814
	 * @param int $post_id. 0 for general access. Post_ID for specific access.
815
	 * @return bool
816
	 */
817
	function current_user_can_access_publicize_data( $post_id = 0 ) {
818
		/**
819
		 * Filter what user capability is required to use the publicize form on the edit post page. Useful if publish post capability has been removed from role.
820
		 *
821
		 * @module publicize
822
		 *
823
		 * @since 4.1.0
824
		 *
825
		 * @param string $capability User capability needed to use publicize
826
		 */
827
		$capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' );
828
829
		if ( 'publish_posts' === $capability && $post_id ) {
830
			return current_user_can( 'publish_post', $post_id );
831
		}
832
833
		return current_user_can( $capability );
834
	}
835
836
	/**
837
	 * Auth callback for the protected ->POST_MESS post_meta
838
	 *
839
	 * @param bool $allowed
840
	 * @param string $meta_key
841
	 * @param int $object_id Post ID
842
	 * @return bool
843
	 */
844
	function message_meta_auth_callback( $allowed, $meta_key, $object_id ) {
845
		return $this->current_user_can_access_publicize_data( $object_id );
846
	}
847
848
	/**
849
	 * Registers the ->POST_MESS post_meta for use in the REST API.
850
	 *
851
	 * Registers for each post type that with `publicize` feature support.
852
	 */
853
	function register_post_meta() {
854
		$args = array(
855
			'type' => 'string',
856
			'description' => __( 'The message to use instead of the title when sharing to Publicize Services', 'jetpack' ),
857
			'single' => true,
858
			'default' => '',
859
			'show_in_rest' => array(
860
				'name' => 'jetpack_publicize_message'
861
			),
862
			'auth_callback' => array( $this, 'message_meta_auth_callback' ),
863
		);
864
865
		foreach ( get_post_types() as $post_type ) {
866
			if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
867
				continue;
868
			}
869
870
			$args['object_subtype'] = $post_type;
871
872
			register_meta( 'post', $this->POST_MESS, $args );
873
		}
874
	}
875
876
	/**
877
	 * Fires when a post is saved, checks conditions and saves state in postmeta so that it
878
	 * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
879
	 *
880
	 * Attached to the `save_post` action.
881
	 *
882
	 * @param int $post_id
883
	 * @param WP_Post $post
884
	 * @return void
885
	 */
886
	function save_meta( $post_id, $post ) {
887
		$cron_user = null;
888
		$submit_post = true;
889
890
		if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
891
			return;
892
893
		// Don't Publicize during certain contexts:
894
895
		// - import
896
		if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING  ) {
897
			$submit_post = false;
898
		}
899
900
		// - on quick edit, autosave, etc but do fire on p2, quickpress, and instapost ajax
901
		if (
902
			defined( 'DOING_AJAX' )
903
		&&
904
			DOING_AJAX
905
		&&
906
			!did_action( 'p2_ajax' )
907
		&&
908
			!did_action( 'wp_ajax_json_quickpress_post' )
909
		&&
910
			!did_action( 'wp_ajax_instapost_publish' )
911
		&&
912
			!did_action( 'wp_ajax_post_reblog' )
913
		&&
914
			!did_action( 'wp_ajax_press-this-save-post' )
915
		) {
916
			$submit_post = false;
917
		}
918
919
		// - bulk edit
920
		if ( isset( $_GET['bulk_edit'] ) ) {
921
			$submit_post = false;
922
		}
923
924
		// - API/XML-RPC Test Posts
925
		if (
926
			(
927
				defined( 'XMLRPC_REQUEST' )
928
			&&
929
				XMLRPC_REQUEST
930
			||
931
				defined( 'APP_REQUEST' )
932
			&&
933
				APP_REQUEST
934
			)
935
		&&
936
			0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
937
		) {
938
			$submit_post = false;
939
		}
940
941
		// only work with certain statuses (avoids inherits, auto drafts etc)
942
		if ( !in_array( $post->post_status, array( 'publish', 'draft', 'future' ) ) ) {
943
			$submit_post = false;
944
		}
945
946
		// don't publish password protected posts
947
		if ( '' !== $post->post_password ) {
948
			$submit_post = false;
949
		}
950
951
		// Did this request happen via wp-admin?
952
		$from_web = isset( $_SERVER['REQUEST_METHOD'] )
953
			&&
954
			'post' == strtolower( $_SERVER['REQUEST_METHOD'] )
955
			&&
956
			isset( $_POST[$this->ADMIN_PAGE] );
957
958
		if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && isset( $_POST['wpas_title'] ) ) {
959
			if ( empty( $_POST['wpas_title'] ) ) {
960
				delete_post_meta( $post_id, $this->POST_MESS );
961
			} else {
962
				update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $_POST['wpas_title'] ) ) );
963
			}
964
		}
965
966
		// change current user to provide context for get_services() if we're running during cron
967
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
968
			$cron_user = (int) $GLOBALS['user_ID'];
969
			wp_set_current_user( $post->post_author );
970
		}
971
972
		/**
973
		 * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
974
		 * it will Publicize to everything *except* those marked for skipping.
975
		 */
976
		foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
977
			foreach ( $connections as $connection ) {
978
				$connection_data = '';
979
				if ( method_exists( $connection, 'get_meta' ) )
980
					$connection_data = $connection->get_meta( 'connection_data' );
981
				elseif ( ! empty( $connection['connection_data'] ) )
982
					$connection_data = $connection['connection_data'];
983
984
				/** This action is documented in modules/publicize/ui.php */
985
				if ( false == apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
986
					delete_post_meta( $post_id, $this->PENDING );
987
					continue;
988
				}
989
990 View Code Duplication
				if ( !empty( $connection->unique_id ) )
991
					$unique_id = $connection->unique_id;
992
				else if ( !empty( $connection['connection_data']['token_id'] ) )
993
					$unique_id = $connection['connection_data']['token_id'];
994
995
				// This was a wp-admin request, so we need to check the state of checkboxes
996
				if ( $from_web ) {
997
					// delete stray service-based post meta
998
					delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
999
1000
					// We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition
1001
					if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$unique_id] ) ) {
1002
						// Also make sure that the service-specific input isn't there.
1003
						// If the user connected to a new service 'in-page' then a hidden field with the service
1004
						// name is added, so we just assume they wanted to Publicize to that service.
1005
						if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$service_name] ) ) {
1006
							// Nothing seems to be checked, so we're going to mark this one to be skipped
1007
							update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
1008
							continue;
1009
						} else {
1010
							// clean up any stray post meta
1011
							delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1012
						}
1013
					} else {
1014
						// The checkbox for this connection is explicitly checked -- make sure we DON'T skip it
1015
						delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1016
					}
1017
				}
1018
1019
				/**
1020
				 * Fires right before the post is processed for Publicize.
1021
				 * Users may hook in here and do anything else they need to after meta is written,
1022
				 * and before the post is processed for Publicize.
1023
				 *
1024
				 * @since 2.1.2
1025
				 *
1026
				 * @param bool $submit_post Should the post be publicized.
1027
				 * @param int $post->ID Post ID.
1028
				 * @param string $service_name Service name.
1029
				 * @param array $connection Array of connection details.
1030
				 */
1031
				do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
1032
			}
1033
		}
1034
1035
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1036
			wp_set_current_user( $cron_user );
1037
		}
1038
1039
		// Next up will be ::publicize_post()
1040
	}
1041
1042
	/**
1043
	 * Alters the "Post Published" message to include information about where the post
1044
	 * was Publicized to.
1045
	 *
1046
	 * Attached to the `post_updated_messages` filter
1047
	 *
1048
	 * @param string[] $messages
1049
	 * @return string[]
1050
	 */
1051
	public function update_published_message( $messages ) {
1052
		global $post_type, $post_type_object, $post;
1053
		if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1054
			return $messages;
1055
		}
1056
1057
		// Bail early if the post is private.
1058
		if ( 'publish' !== $post->post_status ) {
1059
			return $messages;
1060
		}
1061
1062
		$view_post_link_html = '';
1063
		$viewable = is_post_type_viewable( $post_type_object );
1064
		if ( $viewable ) {
1065
			$view_text = esc_html__( 'View post' ); // intentionally omitted domain
1066
1067
			if ( 'jetpack-portfolio' == $post_type ) {
1068
				$view_text = esc_html__( 'View project', 'jetpack' );
1069
			}
1070
1071
			$view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
1072
				esc_url( get_permalink( $post ) ),
1073
				$view_text
1074
			);
1075
		}
1076
1077
		$services = $this->get_publicizing_services( $post->ID );
1078
		if ( empty( $services ) ) {
1079
			return $messages;
1080
		}
1081
1082
		$labels = array();
1083
		foreach ( $services as $service_name => $display_names ) {
1084
			$labels[] = sprintf(
1085
				/* translators: Service name is %1$s, and account name is %2$s. */
1086
				esc_html__( '%1$s (%2$s)', 'jetpack' ),
1087
				esc_html( $service_name ),
1088
				esc_html( implode( ', ', $display_names ) )
1089
			);
1090
		}
1091
1092
		$messages['post'][6] = sprintf(
1093
			/* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1094
			esc_html__( 'Post published and sharing on %1$s.', 'jetpack' ),
1095
			implode( ', ', $labels )
1096
		) . $view_post_link_html;
1097
1098
		if ( $post_type == 'post' && class_exists('Jetpack_Subscriptions' ) ) {
1099
			$subscription = Jetpack_Subscriptions::init();
1100
			if ( $subscription->should_email_post_to_subscribers( $post ) ) {
1101
				$messages['post'][6] = sprintf(
1102
					/* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1103
					esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack' ),
1104
					implode( ', ', $labels )
1105
				) . $view_post_link_html;
1106
			}
1107
		}
1108
1109
		$messages['jetpack-portfolio'][6] = sprintf(
1110
			/* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1111
			esc_html__( 'Project published and sharing project on %1$s.', 'jetpack' ),
1112
			implode( ', ', $labels )
1113
		) . $view_post_link_html;
1114
1115
		return $messages;
1116
	}
1117
1118
	/**
1119
	 * Get the Connections the Post was just Publicized to.
1120
	 *
1121
	 * Only reliable just after the Post was published.
1122
	 *
1123
	 * @param int $post_id
1124
	 * @return string[] Array of Service display name => Connection display name
1125
	 */
1126
	function get_publicizing_services( $post_id ) {
1127
		$services = array();
1128
1129
		foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1130
			// services have multiple connections.
1131
			foreach ( $connections as $connection ) {
1132
				$unique_id = '';
1133 View Code Duplication
				if ( ! empty( $connection->unique_id ) )
1134
					$unique_id = $connection->unique_id;
1135
				else if ( ! empty( $connection['connection_data']['token_id'] ) )
1136
					$unique_id = $connection['connection_data']['token_id'];
1137
1138
				// Did we skip this connection?
1139
				if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id,  true ) ) {
1140
					continue;
1141
				}
1142
				$services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1143
			}
1144
		}
1145
1146
		return $services;
1147
	}
1148
1149
	/**
1150
	 * Is the post Publicize-able?
1151
	 *
1152
	 * Only valid prior to Publicizing a Post.
1153
	 *
1154
	 * @param WP_Post $post
1155
	 * @return bool
1156
	 */
1157
	function post_is_publicizeable( $post ) {
1158
		if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
1159
			return false;
1160
1161
		// This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites)
1162
		if ( 'publish' !== $post->post_status ) {
1163
			return false;
1164
		}
1165
1166
		// If it's not flagged as ready, then abort. @see ::flag_post_for_publicize()
1167
		if ( ! get_post_meta( $post->ID, $this->PENDING, true ) )
1168
			return false;
1169
1170
		return true;
1171
	}
1172
1173
	/**
1174
	 * Is a given post type Publicize-able?
1175
	 *
1176
	 * Not every CPT lends itself to Publicize-ation.  Allow CPTs to register by adding their CPT via
1177
	 * the publicize_post_types array filter.
1178
	 *
1179
	 * @param string $post_type The post type to check.
1180
	 * @return bool True if the post type can be Publicized.
1181
	 */
1182
	function post_type_is_publicizeable( $post_type ) {
1183
		if ( 'post' == $post_type )
1184
			return true;
1185
1186
		return post_type_supports( $post_type, 'publicize' );
1187
	}
1188
1189
	/**
1190
	 * Already-published posts should not be Publicized by default. This filter sets checked to
1191
	 * false if a post has already been published.
1192
	 *
1193
	 * Attached to the `publicize_checkbox_default` filter
1194
	 *
1195
	 * @param bool $checked
1196
	 * @param int $post_id
1197
	 * @param string $service_name 'facebook', 'twitter', etc
1198
	 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
1199
	 * @return bool
1200
	 */
1201
	function publicize_checkbox_default( $checked, $post_id, $service_name, $connection ) {
1202
		if ( 'publish' == get_post_status( $post_id ) ) {
1203
			return false;
1204
		}
1205
1206
		return $checked;
1207
	}
1208
1209
/*
1210
 * Util
1211
 */
1212
1213
	/**
1214
	 * Converts a Publicize message template string into a sprintf format string
1215
	 *
1216
	 * @param string[] $args
1217
	 *               0 - The Publicize message template: 'Check out my post: %title% @ %url'
1218
	 *             ... - The template tags 'title', 'url', etc.
1219
	 * @return string
1220
	 */
1221
	protected static function build_sprintf( $args ) {
1222
		$search = array();
1223
		$replace = array();
1224
		foreach ( $args as $k => $arg ) {
1225
			if ( 0 == $k ) {
1226
				$string = $arg;
1227
				continue;
1228
			}
1229
			$search[] = "%$arg%";
1230
			$replace[] = "%$k\$s";
1231
		}
1232
		return str_replace( $search, $replace, $string );
1233
	}
1234
}
1235
1236
function publicize_calypso_url() {
1237
	$calypso_sharing_url = 'https://wordpress.com/marketing/connections/';
1238
	if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'build_raw_urls' ) ) {
1239
		$site_suffix = Jetpack::build_raw_urls( home_url() );
1240
	} elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
1241
		$site_suffix = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
1242
	}
1243
1244
	if ( $site_suffix ) {
1245
		return $calypso_sharing_url . $site_suffix;
1246
	} else {
1247
		return $calypso_sharing_url;
1248
	}
1249
}
1250