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

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