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