PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 6.2.2
Jetpack – WP Security, Backup, Speed, & Growth v6.2.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / modules / publicize / publicize.php
publicize.php
598 lines 20.1 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 = 'edit_others_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 add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
111
112 // Connection test callback
113 add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) );
114 }
115
116 /**
117 * Functions to be implemented by the extended class (publicize-wpcom or publicize-jetpack)
118 */
119 abstract function get_connection_id( $connection );
120 abstract function connect_url( $service_name );
121 abstract function disconnect_url( $service_name, $id );
122 abstract function get_connection_meta( $connection );
123 abstract function get_services( $filter = 'all' );
124 abstract function get_connections( $service, $_blog_id = false, $_user_id = false );
125 abstract function get_connection( $service, $id, $_blog_id = false, $_user_id = false );
126 abstract function flag_post_for_publicize( $new_status, $old_status, $post );
127 abstract function test_connection( $service_name, $connection );
128 abstract function disconnect( $service, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
129
130 /**
131 * Shared Functions
132 */
133
134 /**
135 * Returns an external URL to the connection's profile
136 */
137 function get_profile_link( $service_name, $connection ) {
138 $cmeta = $this->get_connection_meta( $connection );
139
140 if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
141 if ( 'facebook' == $service_name && 0 === strpos( parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
142 // App-scoped Facebook user IDs are not usable profile links
143 return false;
144 }
145
146 return $cmeta['connection_data']['meta']['link'];
147 } elseif ( 'facebook' == $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
148 return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
149 } elseif ( 'tumblr' == $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
150 return 'http://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
151 } elseif ( 'twitter' == $service_name ) {
152 return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'
153 } elseif ( 'google_plus' == $service_name && isset( $cmeta['connection_data']['meta']['google_plus_page'] ) ) {
154 return 'https://plus.google.com/' . $cmeta['connection_data']['meta']['google_plus_page'];
155 } elseif ( 'google_plus' == $service_name ) {
156 return 'https://plus.google.com/' . $cmeta['external_id'];
157 } else if ( 'linkedin' == $service_name ) {
158 if ( !isset( $cmeta['connection_data']['meta']['profile_url'] ) ) {
159 return false;
160 }
161
162 $profile_url_query = parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY );
163 wp_parse_str( $profile_url_query, $profile_url_query_args );
164 if ( isset( $profile_url_query_args['key'] ) ) {
165 $id = $profile_url_query_args['key'];
166 } elseif ( isset( $profile_url_query_args['id'] ) ) {
167 $id = $profile_url_query_args['id'];
168 } else {
169 return false;
170 }
171
172 return esc_url_raw( add_query_arg( 'id', urlencode( $id ), 'http://www.linkedin.com/profile/view' ) );
173 } else {
174 return false; // no fallback. we just won't link it
175 }
176 }
177
178 /**
179 * Returns a display name for the connection
180 */
181 function get_display_name( $service_name, $connection ) {
182 $cmeta = $this->get_connection_meta( $connection );
183
184 if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
185 return $cmeta['connection_data']['meta']['display_name'];
186 } elseif ( $service_name == 'tumblr' && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
187 return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
188 } elseif ( $service_name == 'twitter' ) {
189 return $cmeta['external_display'];
190 } else {
191 $connection_display = $cmeta['external_display'];
192 if ( empty( $connection_display ) )
193 $connection_display = $cmeta['external_name'];
194 return $connection_display;
195 }
196 }
197
198 public static function get_service_label( $service_name ) {
199 switch ( $service_name ) {
200 case 'linkedin':
201 return 'LinkedIn';
202 break;
203 case 'google_plus':
204 return 'Google+';
205 break;
206 case 'twitter':
207 case 'facebook':
208 case 'tumblr':
209 default:
210 return ucfirst( $service_name );
211 break;
212 }
213 }
214
215 function show_options_popup( $service_name, $connection ) {
216 $cmeta = $this->get_connection_meta( $connection );
217
218 // always show if no selection has been made for facebook
219 if ( 'facebook' == $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) )
220 return true;
221
222 // always show if no selection has been made for tumblr
223 if ( 'tumblr' == $service_name && empty ( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) )
224 return true;
225
226 // if we have the specific connection info..
227 if ( isset( $_GET['id'] ) ) {
228 if ( $cmeta['connection_data']['id'] == $_GET['id'] )
229 return true;
230 } else {
231 // otherwise, just show if this is the completed step / first load
232 if ( !empty( $_GET['action'] ) && 'completed' == $_GET['action'] && !empty( $_GET['service'] ) && $service_name == $_GET['service'] && ! in_array( $_GET['service'], array( 'facebook', 'tumblr' ) ) )
233 return true;
234 }
235
236 return false;
237 }
238
239 function user_id() {
240 global $current_user;
241 return $current_user->ID;
242 }
243
244 function blog_id() {
245 return get_current_blog_id();
246 }
247
248 /**
249 * Returns true if a user has a connection to a particular service, false otherwise
250 */
251 function is_enabled( $service, $_blog_id = false, $_user_id = false ) {
252 if ( !$_blog_id )
253 $_blog_id = $this->blog_id();
254
255 if ( !$_user_id )
256 $_user_id = $this->user_id();
257
258 $connections = $this->get_connections( $service, $_blog_id, $_user_id );
259 return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
260 }
261
262 /**
263 * Fires when a post is saved, checks conditions and saves state in postmeta so that it
264 * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
265 */
266 function save_meta( $post_id, $post ) {
267 $cron_user = null;
268 $submit_post = true;
269
270 if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
271 return;
272
273 // Don't Publicize during certain contexts:
274
275 // - import
276 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
277 $submit_post = false;
278 }
279
280 // - on quick edit, autosave, etc but do fire on p2, quickpress, and instapost ajax
281 if (
282 defined( 'DOING_AJAX' )
283 &&
284 DOING_AJAX
285 &&
286 !did_action( 'p2_ajax' )
287 &&
288 !did_action( 'wp_ajax_json_quickpress_post' )
289 &&
290 !did_action( 'wp_ajax_instapost_publish' )
291 &&
292 !did_action( 'wp_ajax_post_reblog' )
293 &&
294 !did_action( 'wp_ajax_press-this-save-post' )
295 ) {
296 $submit_post = false;
297 }
298
299 // - bulk edit
300 if ( isset( $_GET['bulk_edit'] ) ) {
301 $submit_post = false;
302 }
303
304 // - API/XML-RPC Test Posts
305 if (
306 (
307 defined( 'XMLRPC_REQUEST' )
308 &&
309 XMLRPC_REQUEST
310 ||
311 defined( 'APP_REQUEST' )
312 &&
313 APP_REQUEST
314 )
315 &&
316 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
317 ) {
318 $submit_post = false;
319 }
320
321 // only work with certain statuses (avoids inherits, auto drafts etc)
322 if ( !in_array( $post->post_status, array( 'publish', 'draft', 'future' ) ) ) {
323 $submit_post = false;
324 }
325
326 // don't publish password protected posts
327 if ( '' !== $post->post_password ) {
328 $submit_post = false;
329 }
330
331 // Did this request happen via wp-admin?
332 $from_web = isset( $_SERVER['REQUEST_METHOD'] )
333 &&
334 'post' == strtolower( $_SERVER['REQUEST_METHOD'] )
335 &&
336 isset( $_POST[$this->ADMIN_PAGE] );
337
338 if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && isset( $_POST['wpas_title'] ) ) {
339 if ( empty( $_POST['wpas_title'] ) ) {
340 delete_post_meta( $post_id, $this->POST_MESS );
341 } else {
342 update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $_POST['wpas_title'] ) ) );
343 }
344 }
345
346 // change current user to provide context for get_services() if we're running during cron
347 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
348 $cron_user = (int) $GLOBALS['user_ID'];
349 wp_set_current_user( $post->post_author );
350 }
351
352 /**
353 * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
354 * it will Publicize to everything *except* those marked for skipping.
355 */
356 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
357 foreach ( $connections as $connection ) {
358 $connection_data = '';
359 if ( method_exists( $connection, 'get_meta' ) )
360 $connection_data = $connection->get_meta( 'connection_data' );
361 elseif ( ! empty( $connection['connection_data'] ) )
362 $connection_data = $connection['connection_data'];
363
364 /** This action is documented in modules/publicize/ui.php */
365 if ( false == apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
366 delete_post_meta( $post_id, $this->PENDING );
367 continue;
368 }
369
370 if ( !empty( $connection->unique_id ) )
371 $unique_id = $connection->unique_id;
372 else if ( !empty( $connection['connection_data']['token_id'] ) )
373 $unique_id = $connection['connection_data']['token_id'];
374
375 // This was a wp-admin request, so we need to check the state of checkboxes
376 if ( $from_web ) {
377 // delete stray service-based post meta
378 delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
379
380 // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition
381 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$unique_id] ) ) {
382 // Also make sure that the service-specific input isn't there.
383 // If the user connected to a new service 'in-page' then a hidden field with the service
384 // name is added, so we just assume they wanted to Publicize to that service.
385 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$service_name] ) ) {
386 // Nothing seems to be checked, so we're going to mark this one to be skipped
387 update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
388 continue;
389 } else {
390 // clean up any stray post meta
391 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
392 }
393 } else {
394 // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it
395 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
396 }
397 }
398
399 /**
400 * Fires right before the post is processed for Publicize.
401 * Users may hook in here and do anything else they need to after meta is written,
402 * and before the post is processed for Publicize.
403 *
404 * @since 2.1.2
405 *
406 * @param bool $submit_post Should the post be publicized.
407 * @param int $post->ID Post ID.
408 * @param string $service_name Service name.
409 * @param array $connection Array of connection details.
410 */
411 do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
412 }
413 }
414
415 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
416 wp_set_current_user( $cron_user );
417 }
418
419 // Next up will be ::publicize_post()
420 }
421
422 public function update_published_message( $messages ) {
423 global $post_type, $post_type_object, $post;
424 if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
425 return $messages;
426 }
427 $view_post_link_html = '';
428 $viewable = is_post_type_viewable( $post_type_object );
429 if ( $viewable ) {
430 $view_text = esc_html__( 'View post' ); // intentionally omitted domain
431
432 if ( 'jetpack-portfolio' == $post_type ) {
433 $view_text = esc_html__( 'View project', 'jetpack' );
434 }
435
436 $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
437 esc_url( get_permalink( $post ) ),
438 $view_text
439 );
440 }
441
442 $services = $this->get_publicizing_services( $post->ID );
443 if ( empty( $services ) ) {
444 return $messages;
445 }
446
447 $labels = array();
448 foreach ( $services as $service => $display_names ) {
449 $labels[] = sprintf(
450 /* translators: Service name is %1$s, and account name is %2$s. */
451 esc_html__( '%1$s (%2$s)', 'jetpack' ),
452 esc_html( $service ),
453 esc_html( implode( ', ', $display_names ) )
454 );
455 }
456
457 $messages['post'][6] = sprintf(
458 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
459 esc_html__( 'Post published and sharing on %1$s.', 'jetpack' ),
460 implode( ', ', $labels )
461 ) . $view_post_link_html;
462
463 if ( $post_type == 'post' && class_exists('Jetpack_Subscriptions' ) ) {
464 $subscription = Jetpack_Subscriptions::init();
465 if ( $subscription->should_email_post_to_subscribers( $post ) ) {
466 $messages['post'][6] = sprintf(
467 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
468 esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack' ),
469 implode( ', ', $labels )
470 ) . $view_post_link_html;
471 }
472 }
473
474 $messages['jetpack-portfolio'][6] = sprintf(
475 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
476 esc_html__( 'Project published and sharing project on %1$s.', 'jetpack' ),
477 implode( ', ', $labels )
478 ) . $view_post_link_html;
479
480 return $messages;
481 }
482
483 function get_publicizing_services( $post_id ) {
484 $services = array();
485
486 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
487 // services have multiple connections.
488 foreach ( $connections as $connection ) {
489 $unique_id = '';
490 if ( ! empty( $connection->unique_id ) )
491 $unique_id = $connection->unique_id;
492 else if ( ! empty( $connection['connection_data']['token_id'] ) )
493 $unique_id = $connection['connection_data']['token_id'];
494
495 // Did we skip this connection?
496 if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) {
497 continue;
498 }
499 $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
500 }
501 }
502
503 return $services;
504 }
505
506 /**
507 * Is a given post type Publicize-able?
508 *
509 * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via
510 * the publicize_post_types array filter.
511 *
512 * @param string $post_type The post type to check.
513 * @return bool True if the post type can be Publicized.
514 */
515 function post_type_is_publicizeable( $post_type ) {
516 if ( 'post' == $post_type )
517 return true;
518
519 return post_type_supports( $post_type, 'publicize' );
520 }
521
522 /**
523 * Runs tests on all the connections and returns the results to the caller
524 */
525 function test_publicize_conns() {
526 $test_results = array();
527
528 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
529 foreach ( $connections as $connection ) {
530
531 $id = $this->get_connection_id( $connection );
532
533 $connection_test_passed = true;
534 $connection_test_message = __( 'This connection is working correctly.' , 'jetpack' );
535 $user_can_refresh = false;
536 $refresh_text = '';
537 $refresh_url = '';
538
539 $connection_test_result = true;
540 if ( method_exists( $this, 'test_connection' ) ) {
541 $connection_test_result = $this->test_connection( $service_name, $connection );
542 }
543
544 if ( is_wp_error( $connection_test_result ) ) {
545 $connection_test_passed = false;
546 $connection_test_message = $connection_test_result->get_error_message();
547 $error_data = $connection_test_result->get_error_data();
548
549 $user_can_refresh = $error_data['user_can_refresh'];
550 $refresh_text = $error_data['refresh_text'];
551 $refresh_url = $error_data['refresh_url'];
552 }
553
554 $test_results[] = array(
555 'connectionID' => $id,
556 'serviceName' => $service_name,
557 'connectionTestPassed' => $connection_test_passed,
558 'connectionTestMessage' => esc_attr( $connection_test_message ),
559 'userCanRefresh' => $user_can_refresh,
560 'refreshText' => esc_attr( $refresh_text ),
561 'refreshURL' => $refresh_url
562 );
563 }
564 }
565
566 wp_send_json_success( $test_results );
567 }
568
569 protected static function build_sprintf( $args ) {
570 $search = array();
571 $replace = array();
572 foreach ( $args as $k => $arg ) {
573 if ( 0 == $k ) {
574 $string = $arg;
575 continue;
576 }
577 $search[] = "%$arg%";
578 $replace[] = "%$k\$s";
579 }
580 return str_replace( $search, $replace, $string );
581 }
582 }
583
584 function publicize_calypso_url() {
585 $calypso_sharing_url = 'https://wordpress.com/sharing/';
586 if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'build_raw_urls' ) ) {
587 $site_suffix = Jetpack::build_raw_urls( home_url() );
588 } elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
589 $site_suffix = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
590 }
591
592 if ( $site_suffix ) {
593 return $calypso_sharing_url . $site_suffix;
594 } else {
595 return $calypso_sharing_url;
596 }
597 }
598