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