PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 8.5
Jetpack – WP Security, Backup, Speed, & Growth v8.5
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 in Jetpack – WP Security, Backup, Speed, & Growth 8.5, at modules/publicize/publicize.php

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