PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 6.4
Jetpack – WP Security, Backup, Speed, & Growth v6.4
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
633 lines 21.3 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 ( 'facebook' === $service_name ) {
185 if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
186 return $cmeta['connection_data']['meta']['display_name'];
187 }
188
189 return __( 'Connecting...', 'jetpack' );
190
191 }
192
193 if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
194 return $cmeta['connection_data']['meta']['display_name'];
195 } elseif ( $service_name == 'tumblr' && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
196 return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
197 } elseif ( $service_name == 'twitter' ) {
198 return $cmeta['external_display'];
199 } else {
200 $connection_display = $cmeta['external_display'];
201 if ( empty( $connection_display ) )
202 $connection_display = $cmeta['external_name'];
203 return $connection_display;
204 }
205 }
206
207 public static function get_service_label( $service_name ) {
208 switch ( $service_name ) {
209 case 'linkedin':
210 return 'LinkedIn';
211 break;
212 case 'google_plus':
213 return 'Google+';
214 break;
215 case 'twitter':
216 case 'facebook':
217 case 'tumblr':
218 default:
219 return ucfirst( $service_name );
220 break;
221 }
222 }
223
224 function show_options_popup( $service_name, $connection ) {
225 $cmeta = $this->get_connection_meta( $connection );
226
227 // always show if no selection has been made for facebook
228 if ( 'facebook' == $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) )
229 return true;
230
231 // always show if no selection has been made for tumblr
232 if ( 'tumblr' == $service_name && empty ( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) )
233 return true;
234
235 // if we have the specific connection info..
236 if ( isset( $_GET['id'] ) ) {
237 if ( $cmeta['connection_data']['id'] == $_GET['id'] )
238 return true;
239 } else {
240 // otherwise, just show if this is the completed step / first load
241 if ( !empty( $_GET['action'] ) && 'completed' == $_GET['action'] && !empty( $_GET['service'] ) && $service_name == $_GET['service'] && ! in_array( $_GET['service'], array( 'facebook', 'tumblr' ) ) )
242 return true;
243 }
244
245 return false;
246 }
247
248 function user_id() {
249 global $current_user;
250 return $current_user->ID;
251 }
252
253 function blog_id() {
254 return get_current_blog_id();
255 }
256
257 /**
258 * Returns true if a user has a connection to a particular service, false otherwise
259 */
260 function is_enabled( $service, $_blog_id = false, $_user_id = false ) {
261 if ( !$_blog_id )
262 $_blog_id = $this->blog_id();
263
264 if ( !$_user_id )
265 $_user_id = $this->user_id();
266
267 $connections = $this->get_connections( $service, $_blog_id, $_user_id );
268 return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
269 }
270
271 /**
272 * Fires when a post is saved, checks conditions and saves state in postmeta so that it
273 * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
274 */
275 function save_meta( $post_id, $post ) {
276 $cron_user = null;
277 $submit_post = true;
278
279 if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
280 return;
281
282 // Don't Publicize during certain contexts:
283
284 // - import
285 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
286 $submit_post = false;
287 }
288
289 // - on quick edit, autosave, etc but do fire on p2, quickpress, and instapost ajax
290 if (
291 defined( 'DOING_AJAX' )
292 &&
293 DOING_AJAX
294 &&
295 !did_action( 'p2_ajax' )
296 &&
297 !did_action( 'wp_ajax_json_quickpress_post' )
298 &&
299 !did_action( 'wp_ajax_instapost_publish' )
300 &&
301 !did_action( 'wp_ajax_post_reblog' )
302 &&
303 !did_action( 'wp_ajax_press-this-save-post' )
304 ) {
305 $submit_post = false;
306 }
307
308 // - bulk edit
309 if ( isset( $_GET['bulk_edit'] ) ) {
310 $submit_post = false;
311 }
312
313 // - API/XML-RPC Test Posts
314 if (
315 (
316 defined( 'XMLRPC_REQUEST' )
317 &&
318 XMLRPC_REQUEST
319 ||
320 defined( 'APP_REQUEST' )
321 &&
322 APP_REQUEST
323 )
324 &&
325 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
326 ) {
327 $submit_post = false;
328 }
329
330 // only work with certain statuses (avoids inherits, auto drafts etc)
331 if ( !in_array( $post->post_status, array( 'publish', 'draft', 'future' ) ) ) {
332 $submit_post = false;
333 }
334
335 // don't publish password protected posts
336 if ( '' !== $post->post_password ) {
337 $submit_post = false;
338 }
339
340 // Did this request happen via wp-admin?
341 $from_web = isset( $_SERVER['REQUEST_METHOD'] )
342 &&
343 'post' == strtolower( $_SERVER['REQUEST_METHOD'] )
344 &&
345 isset( $_POST[$this->ADMIN_PAGE] );
346
347 if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && isset( $_POST['wpas_title'] ) ) {
348 if ( empty( $_POST['wpas_title'] ) ) {
349 delete_post_meta( $post_id, $this->POST_MESS );
350 } else {
351 update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $_POST['wpas_title'] ) ) );
352 }
353 }
354
355 // change current user to provide context for get_services() if we're running during cron
356 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
357 $cron_user = (int) $GLOBALS['user_ID'];
358 wp_set_current_user( $post->post_author );
359 }
360
361 /**
362 * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
363 * it will Publicize to everything *except* those marked for skipping.
364 */
365 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
366 foreach ( $connections as $connection ) {
367 $connection_data = '';
368 if ( method_exists( $connection, 'get_meta' ) )
369 $connection_data = $connection->get_meta( 'connection_data' );
370 elseif ( ! empty( $connection['connection_data'] ) )
371 $connection_data = $connection['connection_data'];
372
373 /** This action is documented in modules/publicize/ui.php */
374 if ( false == apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
375 delete_post_meta( $post_id, $this->PENDING );
376 continue;
377 }
378
379 if ( !empty( $connection->unique_id ) )
380 $unique_id = $connection->unique_id;
381 else if ( !empty( $connection['connection_data']['token_id'] ) )
382 $unique_id = $connection['connection_data']['token_id'];
383
384 // This was a wp-admin request, so we need to check the state of checkboxes
385 if ( $from_web ) {
386 // delete stray service-based post meta
387 delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
388
389 // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition
390 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$unique_id] ) ) {
391 // Also make sure that the service-specific input isn't there.
392 // If the user connected to a new service 'in-page' then a hidden field with the service
393 // name is added, so we just assume they wanted to Publicize to that service.
394 if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$service_name] ) ) {
395 // Nothing seems to be checked, so we're going to mark this one to be skipped
396 update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
397 continue;
398 } else {
399 // clean up any stray post meta
400 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
401 }
402 } else {
403 // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it
404 delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
405 }
406 }
407
408 /**
409 * Fires right before the post is processed for Publicize.
410 * Users may hook in here and do anything else they need to after meta is written,
411 * and before the post is processed for Publicize.
412 *
413 * @since 2.1.2
414 *
415 * @param bool $submit_post Should the post be publicized.
416 * @param int $post->ID Post ID.
417 * @param string $service_name Service name.
418 * @param array $connection Array of connection details.
419 */
420 do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
421 }
422 }
423
424 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
425 wp_set_current_user( $cron_user );
426 }
427
428 // Next up will be ::publicize_post()
429 }
430
431 public function update_published_message( $messages ) {
432 global $post_type, $post_type_object, $post;
433 if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
434 return $messages;
435 }
436 $view_post_link_html = '';
437 $viewable = is_post_type_viewable( $post_type_object );
438 if ( $viewable ) {
439 $view_text = esc_html__( 'View post' ); // intentionally omitted domain
440
441 if ( 'jetpack-portfolio' == $post_type ) {
442 $view_text = esc_html__( 'View project', 'jetpack' );
443 }
444
445 $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
446 esc_url( get_permalink( $post ) ),
447 $view_text
448 );
449 }
450
451 $services = $this->get_publicizing_services( $post->ID );
452 if ( empty( $services ) ) {
453 return $messages;
454 }
455
456 $labels = array();
457 foreach ( $services as $service => $display_names ) {
458 $labels[] = sprintf(
459 /* translators: Service name is %1$s, and account name is %2$s. */
460 esc_html__( '%1$s (%2$s)', 'jetpack' ),
461 esc_html( $service ),
462 esc_html( implode( ', ', $display_names ) )
463 );
464 }
465
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 and sharing on %1$s.', 'jetpack' ),
469 implode( ', ', $labels )
470 ) . $view_post_link_html;
471
472 if ( $post_type == 'post' && class_exists('Jetpack_Subscriptions' ) ) {
473 $subscription = Jetpack_Subscriptions::init();
474 if ( $subscription->should_email_post_to_subscribers( $post ) ) {
475 $messages['post'][6] = sprintf(
476 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
477 esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack' ),
478 implode( ', ', $labels )
479 ) . $view_post_link_html;
480 }
481 }
482
483 $messages['jetpack-portfolio'][6] = sprintf(
484 /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
485 esc_html__( 'Project published and sharing project on %1$s.', 'jetpack' ),
486 implode( ', ', $labels )
487 ) . $view_post_link_html;
488
489 return $messages;
490 }
491
492 function get_publicizing_services( $post_id ) {
493 $services = array();
494
495 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
496 // services have multiple connections.
497 foreach ( $connections as $connection ) {
498 $unique_id = '';
499 if ( ! empty( $connection->unique_id ) )
500 $unique_id = $connection->unique_id;
501 else if ( ! empty( $connection['connection_data']['token_id'] ) )
502 $unique_id = $connection['connection_data']['token_id'];
503
504 // Did we skip this connection?
505 if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) {
506 continue;
507 }
508 $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
509 }
510 }
511
512 return $services;
513 }
514
515 /**
516 * Is a given post type Publicize-able?
517 *
518 * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via
519 * the publicize_post_types array filter.
520 *
521 * @param string $post_type The post type to check.
522 * @return bool True if the post type can be Publicized.
523 */
524 function post_type_is_publicizeable( $post_type ) {
525 if ( 'post' == $post_type )
526 return true;
527
528 return post_type_supports( $post_type, 'publicize' );
529 }
530
531 function is_valid_facebook_connection( $connection ) {
532 if ( $this->is_connecting_connection( $connection ) ) {
533 return true;
534 }
535 return isset( $connection['connection_data']['meta']['facebook_page'] );
536 }
537
538 function is_connecting_connection( $connection ) {
539 return isset( $connection['connection_data']['meta']['options_responses'] );
540 }
541
542 /**
543 * Runs tests on all the connections and returns the results to the caller
544 */
545 function test_publicize_conns() {
546 $test_results = array();
547
548 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
549 foreach ( $connections as $connection ) {
550
551 $id = $this->get_connection_id( $connection );
552
553 $connection_test_passed = true;
554 $connection_test_message = __( 'This connection is working correctly.' , 'jetpack' );
555 $user_can_refresh = false;
556 $refresh_text = '';
557 $refresh_url = '';
558
559 $connection_test_result = true;
560 if ( method_exists( $this, 'test_connection' ) ) {
561 $connection_test_result = $this->test_connection( $service_name, $connection );
562 }
563
564 if ( is_wp_error( $connection_test_result ) ) {
565 $connection_test_passed = false;
566 $connection_test_message = $connection_test_result->get_error_message();
567 $error_data = $connection_test_result->get_error_data();
568
569 $user_can_refresh = $error_data['user_can_refresh'];
570 $refresh_text = $error_data['refresh_text'];
571 $refresh_url = $error_data['refresh_url'];
572 }
573
574 // Mark facebook profiles as deprecated
575 if ( 'facebook' === $service_name && ! $this->is_valid_facebook_connection( $connection ) ) {
576 $connection_test_passed = false;
577 $user_can_refresh = false;
578 $connection_test_message = __( 'Facebook no longer supports Publicize connections to Facebook Profiles, but you can still connect Facebook Pages. Please select a Facebook Page to publish updates to.', 'jetpack');
579 }
580
581 $unique_id = null;
582 if ( ! empty( $connection->unique_id ) ) {
583 $unique_id = $connection->unique_id;
584 } else if ( ! empty( $connection['connection_data']['token_id'] ) ) {
585 $unique_id = $connection['connection_data']['token_id'];
586 }
587
588 $test_results[] = array(
589 'connectionID' => $id,
590 'serviceName' => $service_name,
591 'connectionTestPassed' => $connection_test_passed,
592 'connectionTestMessage' => esc_attr( $connection_test_message ),
593 'userCanRefresh' => $user_can_refresh,
594 'refreshText' => esc_attr( $refresh_text ),
595 'refreshURL' => $refresh_url,
596 'unique_id' => $unique_id,
597 );
598 }
599 }
600
601 wp_send_json_success( $test_results );
602 }
603
604 protected static function build_sprintf( $args ) {
605 $search = array();
606 $replace = array();
607 foreach ( $args as $k => $arg ) {
608 if ( 0 == $k ) {
609 $string = $arg;
610 continue;
611 }
612 $search[] = "%$arg%";
613 $replace[] = "%$k\$s";
614 }
615 return str_replace( $search, $replace, $string );
616 }
617 }
618
619 function publicize_calypso_url() {
620 $calypso_sharing_url = 'https://wordpress.com/sharing/';
621 if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'build_raw_urls' ) ) {
622 $site_suffix = Jetpack::build_raw_urls( home_url() );
623 } elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
624 $site_suffix = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
625 }
626
627 if ( $site_suffix ) {
628 return $calypso_sharing_url . $site_suffix;
629 } else {
630 return $calypso_sharing_url;
631 }
632 }
633