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