PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 10.3
Jetpack – WP Security, Backup, Speed, & Growth v10.3
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / modules / publicize / publicize.php
publicize.php
1,289 lines 42.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPress.NamingConventions.ValidVariableName
3
4 use Automattic\Jetpack\Redirect;
5 use Automattic\Jetpack\Status;
6
7 abstract class Publicize_Base {
8
9 /**
10 * Services that are currently connected to the given user
11 * through publicize.
12 */
13 public $connected_services = array();
14
15 /**
16 * Services that are supported by publicize. They don't
17 * necessarily need to be connected to the current user.
18 */
19 public $services;
20
21 /**
22 * key names for post meta
23 */
24 public $ADMIN_PAGE = 'wpas';
25 public $POST_MESS = '_wpas_mess';
26
27 /**
28 * Post meta key for flagging when the post is a tweetstorm.
29 *
30 * @var string
31 */
32 public $POST_TWEETSTORM = '_wpas_is_tweetstorm';
33
34 /**
35 * Post meta key for the flagging when the post share feature is disabled.
36 *
37 * @var string
38 */
39 const POST_PUBLICIZE_FEATURE_ENABLED = '_wpas_feature_enabled';
40
41 public $POST_SKIP = '_wpas_skip_'; // connection id appended to indicate that a connection should NOT be publicized to
42 public $POST_DONE = '_wpas_done_'; // connection id appended to indicate a connection has already been publicized to
43 public $USER_AUTH = 'wpas_authorize';
44 public $USER_OPT = 'wpas_';
45 public $PENDING = '_publicize_pending'; // ready for Publicize to do its thing
46 public $POST_SERVICE_DONE = '_publicize_done_external'; // array of external ids where we've Publicized
47
48 /**
49 * default pieces of the message used in constructing the
50 * content pushed out to other social networks
51 */
52
53 public $default_prefix = '';
54 public $default_message = '%title%';
55 public $default_suffix = ' ';
56
57 /**
58 * What WP capability is require to create/delete global connections?
59 * All users with this cap can un-globalize all other global connections, and globalize any of their own
60 * Globalized connections cannot be unselected by users without this capability when publishing
61 */
62 public $GLOBAL_CAP = 'publish_posts';
63
64 /**
65 * Sets up the basics of Publicize
66 */
67 function __construct() {
68 $this->default_message = self::build_sprintf( array(
69 /**
70 * Filter the default Publicize message.
71 *
72 * @module publicize
73 *
74 * @since 2.0.0
75 *
76 * @param string $this->default_message Publicize's default message. Default is the post title.
77 */
78 apply_filters( 'wpas_default_message', $this->default_message ),
79 'title',
80 'url',
81 ) );
82
83 $this->default_prefix = self::build_sprintf( array(
84 /**
85 * Filter the message prepended to the Publicize custom message.
86 *
87 * @module publicize
88 *
89 * @since 2.0.0
90 *
91 * @param string $this->default_prefix String prepended to the Publicize custom message.
92 */
93 apply_filters( 'wpas_default_prefix', $this->default_prefix ),
94 'url',
95 ) );
96
97 $this->default_suffix = self::build_sprintf( array(
98 /**
99 * Filter the message appended to the Publicize custom message.
100 *
101 * @module publicize
102 *
103 * @since 2.0.0
104 *
105 * @param string $this->default_suffix String appended to the Publicize custom message.
106 */
107 apply_filters( 'wpas_default_suffix', $this->default_suffix ),
108 'url',
109 ) );
110
111 /**
112 * Filter the capability to change global Publicize connection options.
113 *
114 * All users with this cap can un-globalize all other global connections, and globalize any of their own
115 * Globalized connections cannot be unselected by users without this capability when publishing.
116 *
117 * @module publicize
118 *
119 * @since 2.2.1
120 *
121 * @param string $this->GLOBAL_CAP default capability in control of global Publicize connection options. Default to edit_others_posts.
122 */
123 $this->GLOBAL_CAP = apply_filters( 'jetpack_publicize_global_connections_cap', $this->GLOBAL_CAP );
124
125 // stage 1 and 2 of 3-stage Publicize. Flag for Publicize on creation, save meta,
126 // then check meta and publicize based on that. stage 3 implemented on wpcom
127 add_action( 'transition_post_status', array( $this, 'flag_post_for_publicize' ), 10, 3 );
128 add_action( 'save_post', array( &$this, 'save_meta' ), 20, 2 );
129
130 // Default checkbox state for each Connection
131 add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 4 );
132
133 // Alter the "Post Publish" admin notice to mention the Connections we Publicized to.
134 add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
135
136 // Connection test callback
137 add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) );
138
139 add_action( 'init', array( $this, 'add_post_type_support' ) );
140 add_action( 'init', array( $this, 'register_post_meta' ), 20 );
141 add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_extension' ) );
142 }
143
144 /*
145 * Services: Facebook, Twitter, etc.
146 */
147
148 /**
149 * Get services for the given blog and user.
150 *
151 * Can return all available services or just the ones with an active connection.
152 *
153 * @param string $filter
154 * 'all' (default) - Get all services available for connecting
155 * 'connected' - Get all services currently connected
156 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
157 * @param false|int $_user_id The user ID. Use false (default) for the current user
158 * @return array
159 */
160 abstract function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
161
162 function can_connect_service( $service_name ) {
163 return true;
164 }
165
166 /**
167 * Does the given user have a connection to the service on the given blog?
168 *
169 * @param string $service_name 'facebook', 'twitter', etc.
170 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
171 * @param false|int $_user_id The user ID. Use false (default) for the current user
172 * @return bool
173 */
174 function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) {
175 if ( !$_blog_id )
176 $_blog_id = $this->blog_id();
177
178 if ( !$_user_id )
179 $_user_id = $this->user_id();
180
181 $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
182 return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
183 }
184
185 /**
186 * Generates a connection URL.
187 *
188 * This is the URL, which, when visited by the user, starts the authentication
189 * process required to forge a connection.
190 *
191 * @param string $service_name 'facebook', 'twitter', etc.
192 * @return string
193 */
194 abstract function connect_url( $service_name );
195
196 /**
197 * Generates a Connection refresh URL.
198 *
199 * This is the URL, which, when visited by the user, re-authenticates their
200 * connection to the service.
201 *
202 * @param string $service_name 'facebook', 'twitter', etc.
203 * @return string
204 */
205 abstract function refresh_url( $service_name );
206
207 /**
208 * Generates a disconnection URL.
209 *
210 * This is the URL, which, when visited by the user, breaks their connection
211 * with the service.
212 *
213 * @param string $service_name 'facebook', 'twitter', etc.
214 * @param string $connection_id Connection ID
215 * @return string
216 */
217 abstract function disconnect_url( $service_name, $connection_id );
218
219 /**
220 * Returns a display name for the Service
221 *
222 * @param string $service_name 'facebook', 'twitter', etc.
223 * @return string
224 */
225 public static function get_service_label( $service_name ) {
226 switch ( $service_name ) {
227 case 'linkedin':
228 return 'LinkedIn';
229 break;
230 case 'google_drive': // google-drive used to be called google_drive.
231 case 'google-drive':
232 return 'Google Drive';
233 break;
234 case 'twitter':
235 case 'facebook':
236 case 'tumblr':
237 default:
238 return ucfirst( $service_name );
239 break;
240 }
241 }
242
243 /*
244 * Connections: For each Service, there can be multiple connections
245 * for a given user. For example, one user could be connected to Twitter
246 * as both @jetpack and as @wordpressdotcom
247 *
248 * For historical reasons, Connections are represented as an object
249 * on WordPress.com and as an array in Jetpack.
250 */
251
252 /**
253 * Get the active Connections of a Service
254 *
255 * @param string $service_name 'facebook', 'twitter', etc.
256 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
257 * @param false|int $_user_id The user ID. Use false (default) for the current user
258 * @return false|object[]|array[] false if no connections exist
259 */
260 abstract function get_connections( $service_name, $_blog_id = false, $_user_id = false );
261
262 /**
263 * Get a single Connection of a Service
264 *
265 * @param string $service_name 'facebook', 'twitter', etc.
266 * @param string $connection_id Connection ID
267 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
268 * @param false|int $_user_id The user ID. Use false (default) for the current user
269 * @return false|object[]|array[] false if no connections exist
270 */
271 abstract function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false );
272
273 /**
274 * Get the Connection ID.
275 *
276 * Note that this is different than the Connection's uniqueid.
277 *
278 * Via a quirk of history, ID is globally unique and unique_id
279 * is only unique per site.
280 *
281 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
282 * @return string
283 */
284 abstract function get_connection_id( $connection );
285
286 /**
287 * Get the Connection unique_id
288 *
289 * Note that this is different than the Connections ID.
290 *
291 * Via a quirk of history, ID is globally unique and unique_id
292 * is only unique per site.
293 *
294 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
295 * @return string
296 */
297 abstract function get_connection_unique_id( $connection );
298
299 /**
300 * Get the Connection's Meta data
301 *
302 * @param object|array Connection
303 * @return array Connection Meta
304 */
305 abstract function get_connection_meta( $connection );
306
307 /**
308 * Disconnect a Connection
309 *
310 * @param string $service_name 'facebook', 'twitter', etc.
311 * @param string $connection_id Connection ID
312 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
313 * @param false|int $_user_id The user ID. Use false (default) for the current user
314 * @param bool $force_delete Whether to skip permissions checks
315 * @return false|void False on failure. Void on success.
316 */
317 abstract function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
318
319 /**
320 * Globalizes a Connection
321 *
322 * @param string $connection_id Connection ID
323 * @return bool Falsey on failure. Truthy on success.
324 */
325 abstract function globalize_connection( $connection_id );
326
327 /**
328 * Unglobalizes a Connection
329 *
330 * @param string $connection_id Connection ID
331 * @return bool Falsey on failure. Truthy on success.
332 */
333 abstract function unglobalize_connection( $connection_id );
334
335 /**
336 * Returns an external URL to the Connection's profile
337 *
338 * @param string $service_name 'facebook', 'twitter', etc.
339 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
340 * @return false|string False on failure. URL on success.
341 */
342 function get_profile_link( $service_name, $connection ) {
343 $cmeta = $this->get_connection_meta( $connection );
344
345 if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
346 if ( 'facebook' == $service_name && 0 === strpos( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
347 // App-scoped Facebook user IDs are not usable profile links
348 return false;
349 }
350
351 return $cmeta['connection_data']['meta']['link'];
352 } elseif ( 'facebook' == $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
353 return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
354 } elseif ( 'tumblr' == $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
355 return 'https://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
356 } elseif ( 'twitter' == $service_name ) {
357 return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'
358 } else if ( 'linkedin' == $service_name ) {
359 if ( !isset( $cmeta['connection_data']['meta']['profile_url'] ) ) {
360 return false;
361 }
362
363 $profile_url_query = wp_parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY );
364 wp_parse_str( $profile_url_query, $profile_url_query_args );
365 if ( isset( $profile_url_query_args['key'] ) ) {
366 $id = $profile_url_query_args['key'];
367 } elseif ( isset( $profile_url_query_args['id'] ) ) {
368 $id = $profile_url_query_args['id'];
369 } else {
370 return false;
371 }
372
373 return esc_url_raw( add_query_arg( 'id', urlencode( $id ), 'https://www.linkedin.com/profile/view' ) );
374 } else {
375 return false; // no fallback. we just won't link it
376 }
377 }
378
379 /**
380 * Returns a display name for the Connection
381 *
382 * @param string $service_name 'facebook', 'twitter', etc.
383 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
384 * @return string
385 */
386 function get_display_name( $service_name, $connection ) {
387 $cmeta = $this->get_connection_meta( $connection );
388
389 if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
390 return $cmeta['connection_data']['meta']['display_name'];
391 } elseif ( $service_name == 'tumblr' && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
392 return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
393 } elseif ( $service_name == 'twitter' ) {
394 return $cmeta['external_display'];
395 } else {
396 $connection_display = $cmeta['external_display'];
397 if ( empty( $connection_display ) )
398 $connection_display = $cmeta['external_name'];
399 return $connection_display;
400 }
401 }
402
403 /**
404 * Whether the user needs to select additional options after connecting
405 *
406 * @param string $service_name 'facebook', 'twitter', etc.
407 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
408 * @return bool
409 */
410 function show_options_popup( $service_name, $connection ) {
411 $cmeta = $this->get_connection_meta( $connection );
412
413 // always show if no selection has been made for facebook
414 if ( 'facebook' == $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) )
415 return true;
416
417 // always show if no selection has been made for tumblr
418 if ( 'tumblr' == $service_name && empty ( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) )
419 return true;
420
421 // if we have the specific connection info..
422 if ( isset( $_GET['id'] ) ) {
423 if ( $cmeta['connection_data']['id'] == $_GET['id'] )
424 return true;
425 } else {
426 // otherwise, just show if this is the completed step / first load
427 if ( !empty( $_GET['action'] ) && 'completed' == $_GET['action'] && !empty( $_GET['service'] ) && $service_name == $_GET['service'] && ! in_array( $_GET['service'], array( 'facebook', 'tumblr' ) ) )
428 return true;
429 }
430
431 return false;
432 }
433
434 /**
435 * Whether the Connection is "valid" wrt Facebook's requirements.
436 *
437 * Must be connected to a Page (not a Profile).
438 * (Also returns true if we're in the middle of the connection process)
439 *
440 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
441 * @return bool
442 */
443 function is_valid_facebook_connection( $connection ) {
444 if ( $this->is_connecting_connection( $connection ) ) {
445 return true;
446 }
447 $connection_meta = $this->get_connection_meta( $connection );
448 $connection_data = $connection_meta['connection_data'];
449 return isset( $connection_data[ 'meta' ][ 'facebook_page' ] );
450 }
451
452 /**
453 * LinkedIn needs to be reauthenticated to use v2 of their API.
454 * If it's using LinkedIn old API, it's an 'invalid' connection
455 *
456 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
457 * @return bool
458 */
459 function is_invalid_linkedin_connection( $connection ) {
460 // LinkedIn API v1 included the profile link in the connection data.
461 $connection_meta = $this->get_connection_meta( $connection );
462 return isset( $connection_meta['connection_data']['meta']['profile_url'] );
463 }
464
465 /**
466 * Whether the Connection currently being connected
467 *
468 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
469 * @return bool
470 */
471 function is_connecting_connection( $connection ) {
472 $connection_meta = $this->get_connection_meta( $connection );
473 $connection_data = $connection_meta['connection_data'];
474 return isset( $connection_data[ 'meta' ]['options_responses'] );
475 }
476
477 /**
478 * AJAX Handler to run connection tests on all Connections
479 * @return void
480 */
481 function test_publicize_conns() {
482 wp_send_json_success( $this->get_publicize_conns_test_results() );
483 }
484
485 /**
486 * Run connection tests on all Connections
487 *
488 * @return array {
489 * Array of connection test results.
490 *
491 * @type string 'connectionID' Connection identifier string that is unique for each connection
492 * @type string 'serviceName' Slug of the connection's service (facebook, twitter, ...)
493 * @type bool 'connectionTestPassed' Whether the connection test was successful
494 * @type string 'connectionTestMessage' Test success or error message
495 * @type bool 'userCanRefresh' Whether the user can re-authenticate their connection to the service
496 * @type string 'refreshText' Message instructing user to re-authenticate their connection to the service
497 * @type string 'refreshURL' URL, which, when visited by the user, re-authenticates their connection to the service.
498 * @type string 'unique_id' ID string representing connection
499 * }
500 */
501 function get_publicize_conns_test_results() {
502 $test_results = array();
503
504 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
505 foreach ( $connections as $connection ) {
506
507 $id = $this->get_connection_id( $connection );
508
509 $connection_test_passed = true;
510 $connection_test_message = __( 'This connection is working correctly.' , 'jetpack' );
511 $user_can_refresh = false;
512 $refresh_text = '';
513 $refresh_url = '';
514
515 $connection_test_result = true;
516 if ( method_exists( $this, 'test_connection' ) ) {
517 $connection_test_result = $this->test_connection( $service_name, $connection );
518 }
519
520 if ( is_wp_error( $connection_test_result ) ) {
521 $connection_test_passed = false;
522 $connection_test_message = $connection_test_result->get_error_message();
523 $error_data = $connection_test_result->get_error_data();
524
525 $user_can_refresh = $error_data['user_can_refresh'];
526 $refresh_text = $error_data['refresh_text'];
527 $refresh_url = $error_data['refresh_url'];
528 }
529 // Mark facebook profiles as deprecated
530 if ( 'facebook' === $service_name ) {
531 if ( ! $this->is_valid_facebook_connection( $connection ) ) {
532 $connection_test_passed = false;
533 $user_can_refresh = false;
534 $connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack' );
535 }
536 }
537
538 // LinkedIn needs reauthentication to be compatible with v2 of their API
539 if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) {
540 $connection_test_passed = 'must_reauth';
541 $user_can_refresh = false;
542 $connection_test_message = esc_html__( 'Your LinkedIn connection needs to be reauthenticated to continue working – head to Sharing to take care of it.', 'jetpack' );
543 }
544
545 $unique_id = null;
546 if ( ! empty( $connection->unique_id ) ) {
547 $unique_id = $connection->unique_id;
548 } else if ( ! empty( $connection['connection_data']['token_id'] ) ) {
549 $unique_id = $connection['connection_data']['token_id'];
550 }
551
552 $test_results[] = array(
553 'connectionID' => $id,
554 'serviceName' => $service_name,
555 'connectionTestPassed' => $connection_test_passed,
556 'connectionTestMessage' => esc_attr( $connection_test_message ),
557 'userCanRefresh' => $user_can_refresh,
558 'refreshText' => esc_attr( $refresh_text ),
559 'refreshURL' => $refresh_url,
560 'unique_id' => $unique_id,
561 );
562 }
563 }
564
565 return $test_results;
566 }
567
568 /**
569 * Run the connection test for the Connection
570 *
571 * @param string $service_name 'facebook', 'twitter', etc.
572 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
573 * @return WP_Error|true WP_Error on failure. True on success
574 */
575 abstract function test_connection( $service_name, $connection );
576
577 /**
578 * Retrieves current list of connections and applies filters.
579 *
580 * Retrieves current available connections and checks if the connections
581 * have already been used to share current post. Finally, the checkbox
582 * form UI fields are calculated. This function exposes connection form
583 * data directly as array so it can be retrieved for static HTML generation
584 * or JSON consumption.
585 *
586 * @since 6.7.0
587 *
588 * @param integer $selected_post_id Optional. Post ID to query connection status for.
589 *
590 * @return array {
591 * Array of UI setup data for connection list form.
592 *
593 * @type string 'unique_id' ID string representing connection
594 * @type string 'service_name' Slug of the connection's service (facebook, twitter, ...)
595 * @type string 'service_label' Service Label (Facebook, Twitter, ...)
596 * @type string 'display_name' Connection's human-readable Username: "@jetpack"
597 * @type bool 'enabled' Default value for the connection (e.g., for a checkbox).
598 * @type bool 'done' Has this connection already been publicized to?
599 * @type bool 'toggleable' Is the user allowed to change the value for the connection?
600 * @type bool 'global' Is this connection a global one?
601 * }
602 */
603 public function get_filtered_connection_data( $selected_post_id = null ) {
604 $connection_list = array();
605
606 $post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null.
607 // Handle case where there is no current post.
608 if ( ! empty( $post ) ) {
609 $post_id = $post->ID;
610 } else {
611 $post_id = null;
612 }
613
614 $services = $this->get_services( 'connected' );
615 $all_done = $this->post_is_done_sharing( $post_id );
616
617 // We don't allow Publicizing to the same external id twice, to prevent spam.
618 $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
619
620 foreach ( $services as $service_name => $connections ) {
621 foreach ( $connections as $connection ) {
622 $connection_meta = $this->get_connection_meta( $connection );
623 $connection_data = $connection_meta['connection_data'];
624
625 $unique_id = $this->get_connection_unique_id( $connection );
626
627
628 // Was this connection (OR, old-format service) already Publicized to?
629 $done = ! empty( $post ) && (
630 // New flags
631 1 == get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
632 ||
633 // old flags
634 1 == get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
635 );
636
637 /**
638 * Filter whether a post should be publicized to a given service.
639 *
640 * @module publicize
641 *
642 * @since 2.0.0
643 *
644 * @param bool true Should the post be publicized to a given service? Default to true.
645 * @param int $post_id Post ID.
646 * @param string $service_name Service name.
647 * @param array $connection_data Array of information about all Publicize details for the site.
648 */
649 if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection_data ) ) {
650 continue;
651 }
652
653 // Should we be skipping this one?
654 $skip = (
655 (
656 ! empty( $post )
657 &&
658 in_array( $post->post_status, array( 'publish', 'draft', 'future' ) )
659 &&
660 (
661 // New flags
662 get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
663 ||
664 // Old flags
665 get_post_meta( $post->ID, $this->POST_SKIP . $service_name )
666 )
667 )
668 ||
669 (
670 is_array( $connection )
671 &&
672 isset( $connection_meta['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection_meta['external_id'] ] )
673 )
674 );
675
676 // If this one has already been publicized to, don't let it happen again.
677 $toggleable = ! $done && ! $all_done;
678
679 // Determine the state of the checkbox (on/off) and allow filtering.
680 $enabled = $done || ! $skip;
681 /**
682 * Filter the checkbox state of each Publicize connection appearing in the post editor.
683 *
684 * @module publicize
685 *
686 * @since 2.0.1
687 *
688 * @param bool $enabled Should the Publicize checkbox be enabled for a given service.
689 * @param int $post_id Post ID.
690 * @param string $service_name Service name.
691 * @param array $connection Array of connection details.
692 */
693 $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
694
695 /**
696 * If this is a global connection and this user doesn't have enough permissions to modify
697 * those connections, don't let them change it.
698 */
699 if ( ! $done && ( 0 == $connection_data['user_id'] && ! current_user_can( $this->GLOBAL_CAP ) ) ) {
700 $toggleable = false;
701
702 /**
703 * Filters the checkboxes for global connections with non-prilvedged users.
704 *
705 * @module publicize
706 *
707 * @since 3.7.0
708 *
709 * @param bool $enabled Indicates if this connection should be enabled. Default true.
710 * @param int $post_id ID of the current post
711 * @param string $service_name Name of the connection (Facebook, Twitter, etc)
712 * @param array $connection Array of data about the connection.
713 */
714 $enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection );
715 }
716
717 // Force the checkbox to be checked if the post was DONE, regardless of what the filter does.
718 if ( $done ) {
719 $enabled = true;
720 }
721
722 $connection_list[] = array(
723 'unique_id' => $unique_id,
724 'service_name' => $service_name,
725 'service_label' => $this->get_service_label( $service_name ),
726 'display_name' => $this->get_display_name( $service_name, $connection ),
727
728 'enabled' => $enabled,
729 'done' => $done,
730 'toggleable' => $toggleable,
731 'global' => 0 == $connection_data['user_id'],
732 );
733 }
734 }
735
736 return $connection_list;
737 }
738
739 /**
740 * Checks if post has already been shared by Publicize in the past.
741 *
742 * @since 6.7.0
743 *
744 * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
745 *
746 * @return bool True if post has already been shared by Publicize, false otherwise.
747 */
748 abstract public function post_is_done_sharing( $post_id = null );
749
750 /**
751 * Retrieves full list of available Publicize connection services.
752 *
753 * Retrieves current available publicize service connections
754 * with associated labels and URLs.
755 *
756 * @since 6.7.0
757 *
758 * @return array {
759 * Array of UI service connection data for all services
760 *
761 * @type string 'name' Name of service.
762 * @type string 'label' Display label for service.
763 * @type string 'url' URL for adding connection to service.
764 * }
765 */
766 function get_available_service_data() {
767 $available_services = $this->get_services( 'all' );
768 $available_service_data = array();
769
770 foreach ( $available_services as $service_name => $service ) {
771 $available_service_data[] = array(
772 'name' => $service_name,
773 'label' => $this->get_service_label( $service_name ),
774 'url' => $this->connect_url( $service_name ),
775 );
776 }
777
778 return $available_service_data;
779 }
780
781 /*
782 * Site Data
783 */
784
785 function user_id() {
786 return get_current_user_id();
787 }
788
789 function blog_id() {
790 return get_current_blog_id();
791 }
792
793 /*
794 * Posts
795 */
796
797 /**
798 * Checks old and new status to see if the post should be flagged as
799 * ready to Publicize.
800 *
801 * Attached to the `transition_post_status` filter.
802 *
803 * @param string $new_status
804 * @param string $old_status
805 * @param WP_Post $post
806 * @return void
807 */
808 abstract function flag_post_for_publicize( $new_status, $old_status, $post );
809
810 /**
811 * Ensures the Post internal post-type supports `publicize`
812 *
813 * This feature support flag is used by the REST API.
814 */
815 function add_post_type_support() {
816 add_post_type_support( 'post', 'publicize' );
817 }
818
819 /**
820 * Register the Publicize Gutenberg extension
821 */
822 function register_gutenberg_extension() {
823 // TODO: The `gutenberg/available-extensions` endpoint currently doesn't accept a post ID,
824 // so we cannot pass one to `$this->current_user_can_access_publicize_data()`.
825
826 if ( $this->current_user_can_access_publicize_data() ) {
827 Jetpack_Gutenberg::set_extension_available( 'jetpack/publicize' );
828 } else {
829 Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/publicize', 'unauthorized' );
830
831 }
832 }
833
834 /**
835 * Can the current user access Publicize Data.
836 *
837 * @param int $post_id. 0 for general access. Post_ID for specific access.
838 * @return bool
839 */
840 function current_user_can_access_publicize_data( $post_id = 0 ) {
841 /**
842 * 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.
843 *
844 * @module publicize
845 *
846 * @since 4.1.0
847 *
848 * @param string $capability User capability needed to use publicize
849 */
850 $capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' );
851
852 if ( 'publish_posts' === $capability && $post_id ) {
853 return current_user_can( 'publish_post', $post_id );
854 }
855
856 return current_user_can( $capability );
857 }
858
859 /**
860 * Auth callback for the protected ->POST_MESS post_meta
861 *
862 * @param bool $allowed
863 * @param string $meta_key
864 * @param int $object_id Post ID
865 * @return bool
866 */
867 function message_meta_auth_callback( $allowed, $meta_key, $object_id ) {
868 return $this->current_user_can_access_publicize_data( $object_id );
869 }
870
871 /**
872 * Registers the post_meta for use in the REST API.
873 *
874 * Registers for each post type that with `publicize` feature support.
875 */
876 function register_post_meta() {
877 $message_args = array(
878 'type' => 'string',
879 'description' => __( 'The message to use instead of the title when sharing to Publicize Services', 'jetpack' ),
880 'single' => true,
881 'default' => '',
882 'show_in_rest' => array(
883 'name' => 'jetpack_publicize_message',
884 ),
885 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
886 );
887
888 $tweetstorm_args = array(
889 'type' => 'boolean',
890 'description' => __( 'Whether or not the post should be treated as a Twitter thread.', 'jetpack' ),
891 'single' => true,
892 'default' => false,
893 'show_in_rest' => array(
894 'name' => 'jetpack_is_tweetstorm',
895 ),
896 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
897 );
898
899 $publicize_feature_enable_args = array(
900 'type' => 'boolean',
901 'description' => __( 'Whether or not the Share Post feature is enabled.', 'jetpack' ),
902 'single' => true,
903 'default' => true,
904 'show_in_rest' => array(
905 'name' => 'jetpack_publicize_feature_enabled',
906 ),
907 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
908 );
909
910 foreach ( get_post_types() as $post_type ) {
911 if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
912 continue;
913 }
914
915 $message_args['object_subtype'] = $post_type;
916 $tweetstorm_args['object_subtype'] = $post_type;
917 $publicize_feature_enable_args['object_subtype'] = $post_type;
918
919 register_meta( 'post', $this->POST_MESS, $message_args );
920 register_meta( 'post', $this->POST_TWEETSTORM, $tweetstorm_args );
921 register_meta( 'post', self::POST_PUBLICIZE_FEATURE_ENABLED, $publicize_feature_enable_args );
922 }
923 }
924
925 /**
926 * Fires when a post is saved, checks conditions and saves state in postmeta so that it
927 * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
928 *
929 * Attached to the `save_post` action.
930 *
931 * @param int $post_id
932 * @param WP_Post $post
933 * @return void
934 */
935 function save_meta( $post_id, $post ) {
936 $cron_user = null;
937 $submit_post = true;
938
939 if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
940 return;
941
942 // Don't Publicize during certain contexts:
943
944 // - import
945 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
946 $submit_post = false;
947 }
948
949 // - on quick edit, autosave, etc but do fire on p2, quickpress, and instapost ajax
950 if (
951 defined( 'DOING_AJAX' )
952 &&
953 DOING_AJAX
954 &&
955 !did_action( 'p2_ajax' )
956 &&
957 !did_action( 'wp_ajax_json_quickpress_post' )
958 &&
959 !did_action( 'wp_ajax_instapost_publish' )
960 &&
961 !did_action( 'wp_ajax_post_reblog' )
962 &&
963 !did_action( 'wp_ajax_press-this-save-post' )
964 ) {
965 $submit_post = false;
966 }
967
968 // - bulk edit
969 if ( isset( $_GET['bulk_edit'] ) ) {
970 $submit_post = false;
971 }
972
973 // - API/XML-RPC Test Posts
974 if (
975 (
976 defined( 'XMLRPC_REQUEST' )
977 &&
978 XMLRPC_REQUEST
979 ||
980 defined( 'APP_REQUEST' )
981 &&
982 APP_REQUEST
983 )
984 &&
985 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
986 ) {
987 $submit_post = false;
988 }
989
990 // only work with certain statuses (avoids inherits, auto drafts etc)
991 if ( !in_array( $post->post_status, array( 'publish', 'draft', 'future' ) ) ) {
992 $submit_post = false;
993 }
994
995 // don't publish password protected posts
996 if ( '' !== $post->post_password ) {
997 $submit_post = false;
998 }
999
1000 // Did this request happen via wp-admin?
1001 $from_web = isset( $_SERVER['REQUEST_METHOD'] )
1002 &&
1003 'post' == strtolower( $_SERVER['REQUEST_METHOD'] )
1004 &&
1005 isset( $_POST[$this->ADMIN_PAGE] );
1006
1007 if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && isset( $_POST['wpas_title'] ) ) {
1008 if ( empty( $_POST['wpas_title'] ) ) {
1009 delete_post_meta( $post_id, $this->POST_MESS );
1010 } else {
1011 update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $_POST['wpas_title'] ) ) );
1012 }
1013 }
1014
1015 // change current user to provide context for get_services() if we're running during cron
1016 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1017 $cron_user = (int) $GLOBALS['user_ID'];
1018 wp_set_current_user( $post->post_author );
1019 }
1020
1021 /**
1022 * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
1023 * it will Publicize to everything *except* those marked for skipping.
1024 */
1025 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1026 foreach ( $connections as $connection ) {
1027 $connection_data = '';
1028 if ( is_object( $connection ) && method_exists( $connection, 'get_meta' ) ) {
1029 $connection_data = $connection->get_meta( 'connection_data' );
1030 } elseif ( ! empty( $connection['connection_data'] ) ) {
1031 $connection_data = $connection['connection_data'];
1032 }
1033
1034 /** This action is documented in modules/publicize/ui.php */
1035 if ( false == apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
1036 delete_post_meta( $post_id, $this->PENDING );
1037 continue;
1038 }
1039
1040 if ( !empty( $connection->unique_id ) )
1041 $unique_id = $connection->unique_id;
1042 else if ( !empty( $connection['connection_data']['token_id'] ) )
1043 $unique_id = $connection['connection_data']['token_id'];
1044
1045 // This was a wp-admin request, so we need to check the state of checkboxes
1046 if ( $from_web ) {
1047 // delete stray service-based post meta
1048 delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
1049
1050 // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition
1051 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$unique_id] ) ) {
1052 // Also make sure that the service-specific input isn't there.
1053 // If the user connected to a new service 'in-page' then a hidden field with the service
1054 // name is added, so we just assume they wanted to Publicize to that service.
1055 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$service_name] ) ) {
1056 // Nothing seems to be checked, so we're going to mark this one to be skipped
1057 update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
1058 continue;
1059 } else {
1060 // clean up any stray post meta
1061 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1062 }
1063 } else {
1064 // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it
1065 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1066 }
1067 }
1068
1069 /**
1070 * Fires right before the post is processed for Publicize.
1071 * Users may hook in here and do anything else they need to after meta is written,
1072 * and before the post is processed for Publicize.
1073 *
1074 * @since 2.1.2
1075 *
1076 * @param bool $submit_post Should the post be publicized.
1077 * @param int $post->ID Post ID.
1078 * @param string $service_name Service name.
1079 * @param array $connection Array of connection details.
1080 */
1081 do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
1082 }
1083 }
1084
1085 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1086 wp_set_current_user( $cron_user );
1087 }
1088
1089 // Next up will be ::publicize_post()
1090 }
1091
1092 /**
1093 * Alters the "Post Published" message to include information about where the post
1094 * was Publicized to.
1095 *
1096 * Attached to the `post_updated_messages` filter
1097 *
1098 * @param string[] $messages
1099 * @return string[]
1100 */
1101 public function update_published_message( $messages ) {
1102 global $post_type, $post_type_object, $post;
1103 if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1104 return $messages;
1105 }
1106
1107 // Bail early if the post is private.
1108 if ( 'publish' !== $post->post_status ) {
1109 return $messages;
1110 }
1111
1112 $view_post_link_html = '';
1113 $viewable = is_post_type_viewable( $post_type_object );
1114 if ( $viewable ) {
1115 $view_text = esc_html__( 'View post' ); // intentionally omitted domain
1116
1117 if ( 'jetpack-portfolio' == $post_type ) {
1118 $view_text = esc_html__( 'View project', 'jetpack' );
1119 }
1120
1121 $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
1122 esc_url( get_permalink( $post ) ),
1123 $view_text
1124 );
1125 }
1126
1127 $services = $this->get_publicizing_services( $post->ID );
1128 if ( empty( $services ) ) {
1129 return $messages;
1130 }
1131
1132 $labels = array();
1133 foreach ( $services as $service_name => $display_names ) {
1134 $labels[] = sprintf(
1135 /* translators: Service name is %1$s, and account name is %2$s. */
1136 esc_html__( '%1$s (%2$s)', 'jetpack' ),
1137 esc_html( $service_name ),
1138 esc_html( implode( ', ', $display_names ) )
1139 );
1140 }
1141
1142 $messages['post'][6] = sprintf(
1143 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1144 esc_html__( 'Post published and sharing on %1$s.', 'jetpack' ),
1145 implode( ', ', $labels )
1146 ) . $view_post_link_html;
1147
1148 if ( $post_type == 'post' && class_exists('Jetpack_Subscriptions' ) ) {
1149 $subscription = Jetpack_Subscriptions::init();
1150 if ( $subscription->should_email_post_to_subscribers( $post ) ) {
1151 $messages['post'][6] = sprintf(
1152 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1153 esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack' ),
1154 implode( ', ', $labels )
1155 ) . $view_post_link_html;
1156 }
1157 }
1158
1159 $messages['jetpack-portfolio'][6] = sprintf(
1160 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1161 esc_html__( 'Project published and sharing project on %1$s.', 'jetpack' ),
1162 implode( ', ', $labels )
1163 ) . $view_post_link_html;
1164
1165 return $messages;
1166 }
1167
1168 /**
1169 * Get the Connections the Post was just Publicized to.
1170 *
1171 * Only reliable just after the Post was published.
1172 *
1173 * @param int $post_id
1174 * @return string[] Array of Service display name => Connection display name
1175 */
1176 function get_publicizing_services( $post_id ) {
1177 $services = array();
1178
1179 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1180 // services have multiple connections.
1181 foreach ( $connections as $connection ) {
1182 $unique_id = '';
1183 if ( ! empty( $connection->unique_id ) )
1184 $unique_id = $connection->unique_id;
1185 else if ( ! empty( $connection['connection_data']['token_id'] ) )
1186 $unique_id = $connection['connection_data']['token_id'];
1187
1188 // Did we skip this connection?
1189 if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) {
1190 continue;
1191 }
1192 $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1193 }
1194 }
1195
1196 return $services;
1197 }
1198
1199 /**
1200 * Is the post Publicize-able?
1201 *
1202 * Only valid prior to Publicizing a Post.
1203 *
1204 * @param WP_Post $post
1205 * @return bool
1206 */
1207 function post_is_publicizeable( $post ) {
1208 if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
1209 return false;
1210
1211 // This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites)
1212 if ( 'publish' !== $post->post_status ) {
1213 return false;
1214 }
1215
1216 // If it's not flagged as ready, then abort. @see ::flag_post_for_publicize()
1217 if ( ! get_post_meta( $post->ID, $this->PENDING, true ) )
1218 return false;
1219
1220 return true;
1221 }
1222
1223 /**
1224 * Is a given post type Publicize-able?
1225 *
1226 * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via
1227 * the publicize_post_types array filter.
1228 *
1229 * @param string $post_type The post type to check.
1230 * @return bool True if the post type can be Publicized.
1231 */
1232 function post_type_is_publicizeable( $post_type ) {
1233 if ( 'post' == $post_type )
1234 return true;
1235
1236 return post_type_supports( $post_type, 'publicize' );
1237 }
1238
1239 /**
1240 * Already-published posts should not be Publicized by default. This filter sets checked to
1241 * false if a post has already been published.
1242 *
1243 * Attached to the `publicize_checkbox_default` filter
1244 *
1245 * @param bool $checked
1246 * @param int $post_id
1247 * @param string $service_name 'facebook', 'twitter', etc
1248 * @param object|array The Connection object (WordPress.com) or array (Jetpack)
1249 * @return bool
1250 */
1251 function publicize_checkbox_default( $checked, $post_id, $service_name, $connection ) {
1252 if ( 'publish' == get_post_status( $post_id ) ) {
1253 return false;
1254 }
1255
1256 return $checked;
1257 }
1258
1259 /*
1260 * Util
1261 */
1262
1263 /**
1264 * Converts a Publicize message template string into a sprintf format string
1265 *
1266 * @param string[] $args
1267 * 0 - The Publicize message template: 'Check out my post: %title% @ %url'
1268 * ... - The template tags 'title', 'url', etc.
1269 * @return string
1270 */
1271 protected static function build_sprintf( $args ) {
1272 $search = array();
1273 $replace = array();
1274 foreach ( $args as $k => $arg ) {
1275 if ( 0 == $k ) {
1276 $string = $arg;
1277 continue;
1278 }
1279 $search[] = "%$arg%";
1280 $replace[] = "%$k\$s";
1281 }
1282 return str_replace( $search, $replace, $string );
1283 }
1284 }
1285
1286 function publicize_calypso_url() {
1287 return Redirect::get_url( 'calypso-marketing-connections', array( 'site' => ( new Status() )->get_site_suffix() ) );
1288 }
1289