PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.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 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / class-publicize.php

class-publicize.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-publicize/src/class-publicize.php

671 lines 20.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Publicize class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8 namespace Automattic\Jetpack\Publicize;
9
10 use Automattic\Jetpack\Connection\Client;
11 use Jetpack_IXR_Client;
12 use Jetpack_Options;
13 use WP_Error;
14 use WP_Post;
15
16 /**
17 * Extend the base class with Jetpack-specific functionality.
18 */
19 class Publicize extends Publicize_Base {
20
21 const JETPACK_SOCIAL_CONNECTIONS_TRANSIENT = 'jetpack_social_connections';
22
23 /**
24 * Transitory storage of connection testing results.
25 *
26 * @var array
27 */
28 private $test_connection_results = array();
29
30 /**
31 * Property to store the results of fetching the connections.
32 *
33 * @var null|array
34 */
35 private $current_connections = null;
36
37 /**
38 * Add hooks.
39 */
40 public function __construct() {
41 parent::__construct();
42
43 add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( $this, 'register_update_publicize_connections_xmlrpc_method' ) );
44
45 add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
46
47 add_action( 'wp_insert_post', array( $this, 'save_publicized' ), 11, 2 );
48
49 add_action( 'connection_disconnected', array( $this, 'add_disconnect_notice' ) );
50 }
51
52 /**
53 * Add a notice when a connection has been disconnected.
54 */
55 public function add_disconnect_notice() {
56 add_action( 'admin_notices', array( $this, 'display_disconnected' ) );
57 }
58
59 /**
60 * Force user connection before showing the Publicize UI.
61 *
62 * @deprecated 0.88.0 Settings > Sharing no longer hosts the Publicize UI.
63 *
64 * @return void
65 */
66 public function force_user_connection() {
67 _deprecated_function( __METHOD__, 'publicize-0.88.0' );
68 }
69
70 /**
71 * Show a warning when Publicize does not have a connection.
72 *
73 * @deprecated 0.88.0 Settings > Sharing no longer hosts the Publicize UI.
74 *
75 * @return void
76 */
77 public function admin_page_warning() {
78 _deprecated_function( __METHOD__, 'publicize-0.88.0' );
79 }
80
81 /**
82 * Show error on settings page if applicable.
83 *
84 * @deprecated 0.88.0 Settings > Sharing no longer hosts connection errors.
85 *
86 * @return void
87 */
88 public function admin_page_load() {
89 _deprecated_function( __METHOD__, 'publicize-0.88.0' );
90 }
91
92 /**
93 * Display an error message.
94 *
95 * @deprecated 0.88.0 Settings > Sharing no longer hosts connection errors.
96 *
97 * @return void
98 */
99 public function display_connection_error() {
100 _deprecated_function( __METHOD__, 'publicize-0.88.0' );
101 }
102
103 /**
104 * Remove a Publicize Connection.
105 *
106 * @param string $service_name 'facebook', 'twitter', etc.
107 * @param string $connection_id Connection ID.
108 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
109 * @param false|int $_user_id The user ID. Use false (default) for the current user.
110 * @param bool $force_delete Whether to skip permissions checks.
111 * @return false|void False on failure. Void on success.
112 */
113 public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) {
114 return Keyring_Helper::disconnect( $service_name, $connection_id, $_blog_id, $_user_id, $force_delete );
115 }
116
117 /**
118 * Set updated Publicize connections in a transient.
119 *
120 * @param mixed $publicize_connections Updated connections.
121 * @return true
122 */
123 public function receive_updated_publicize_connections( $publicize_connections ) {
124
125 $publicize_connections = self::filter_usable_connections( $publicize_connections );
126
127 // Populate the cache with the new data.
128 Connections::get_all( array( 'ignore_cache' => true ) );
129
130 $expiry = 3600 * 4;
131 if ( ! set_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT, $publicize_connections, $expiry ) ) {
132 // If the transient has beeen set in another request, the call to set_transient can fail. If so,
133 // we can delete the transient and try again.
134 $this->clear_connections_transient();
135 set_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT, $publicize_connections, $expiry );
136 }
137 // Regardless of whether the transient is set ok, let's set and use the local property for this request.
138 $this->current_connections = $publicize_connections;
139 return true;
140 }
141
142 /**
143 * Drop connections we cannot attribute to a user.
144 *
145 * A missing user ID reads as "shared with everyone" in is_global_connection(), so such a
146 * connection would be exposed to every user on the site. User ID 0 is a genuine shared
147 * connection and is kept; unknown fields pass through, as WPCOM adds to this payload.
148 *
149 * @param mixed $publicize_connections Connections, keyed by service name.
150 * @return array
151 */
152 private static function filter_usable_connections( $publicize_connections ) {
153 // An empty payload is valid - it means the site has no connections left.
154 if ( ! is_array( $publicize_connections ) ) {
155 return array();
156 }
157
158 $usable_connections = array();
159
160 foreach ( $publicize_connections as $service_name => $connections_for_service ) {
161 if ( ! is_array( $connections_for_service ) ) {
162 continue;
163 }
164
165 foreach ( $connections_for_service as $id => $connection ) {
166 if ( ! isset( $connection['connection_data']['user_id'] ) || ! is_numeric( $connection['connection_data']['user_id'] ) ) {
167 continue;
168 }
169
170 $usable_connections[ $service_name ][ $id ] = $connection;
171 }
172 }
173
174 return $usable_connections;
175 }
176
177 /**
178 * Add method to update Publicize connections.
179 *
180 * @todo Kept as a fallback for the jetpack/v4/publicize/connections/sync REST endpoint that replaced it; remove after a few releases (CONNECT-446).
181 *
182 * @param array $methods Array of registered methods.
183 * @return array
184 */
185 public function register_update_publicize_connections_xmlrpc_method( $methods ) {
186 return array_merge(
187 $methods,
188 array(
189 'jetpack.updatePublicizeConnections' => array( $this, 'receive_updated_publicize_connections' ),
190 )
191 );
192 }
193
194 /**
195 * Get a list of all connections.
196 *
197 * Google Plus is no longer a functional service, so we remove it from the list.
198 *
199 * @return array
200 */
201 public function get_all_connections() {
202 $this->refresh_connections();
203
204 $connections = $this->current_connections;
205
206 if ( empty( $connections ) ) {
207 $connections = array();
208 }
209
210 if ( isset( $connections['google_plus'] ) ) {
211 unset( $connections['google_plus'] );
212 }
213 return $connections;
214 }
215
216 /**
217 * Get connections for a specific service.
218 *
219 * @param string $service_name 'facebook', 'twitter', etc.
220 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
221 * @param false|int $_user_id The user ID. Use false (default) for the current user.
222 * @return false|object[]|array[]
223 */
224 public function get_connections( $service_name, $_blog_id = false, $_user_id = false ) {
225 if ( false === $_user_id ) {
226 $_user_id = $this->user_id();
227 }
228
229 $connections = $this->get_all_connections();
230 $connections_to_return = array();
231
232 if ( ! empty( $connections ) && is_array( $connections ) ) {
233 if ( ! empty( $connections[ $service_name ] ) ) {
234 foreach ( $connections[ $service_name ] as $id => $connection ) {
235 if ( $this->is_global_connection( $connection ) || $_user_id === (int) $connection['connection_data']['user_id'] ) {
236 $connections_to_return[ $id ] = $connection;
237 }
238 }
239 }
240
241 return $connections_to_return;
242 }
243
244 return false;
245 }
246
247 /**
248 * Get all connections for a specific user.
249 *
250 * @param array $args Arguments to run operations such as force refresh and connection test results.
251 * @return array
252 */
253 public function get_all_connections_for_user( $args = array() ) {
254 if ( ( isset( $args['clear_cache'] ) && $args['clear_cache'] )
255 || ( isset( $args['test_connections'] ) && $args['test_connections'] ) ) {
256 $this->clear_connections_transient();
257 }
258 $connections = $this->get_all_connections();
259
260 $connections_to_return = array();
261 if ( ! empty( $connections ) ) {
262 foreach ( (array) $connections as $service_name => $connections_for_service ) {
263 foreach ( $connections_for_service as $id => $connection ) {
264 $user_id = (int) $connection['connection_data']['user_id'];
265 if ( $user_id === 0 || $this->user_id() === $user_id ) {
266 $connections_to_return[ $service_name ][ $id ] = $connection;
267 }
268 }
269 }
270 }
271
272 return $connections_to_return;
273 }
274
275 /**
276 * To add the connection test results to the connections.
277 *
278 * @param array $connections The Jetpack Social connections.
279
280 * @return array
281 */
282 public function add_connection_test_results( $connections ) {
283 $path = sprintf( '/sites/%d/publicize/connection-test-results', absint( Jetpack_Options::get_option( 'id' ) ) );
284 $response = Client::wpcom_json_api_request_as_user( $path, '2', array(), null, 'wpcom' );
285 $connection_results = json_decode( wp_remote_retrieve_body( $response ), true );
286 $connection_results_map = array();
287
288 foreach ( $connection_results as $connection_result ) {
289 $connection_results_map[ $connection_result['connection_id'] ] = $connection_result['test_success'] ? 'ok' : 'broken';
290 }
291 foreach ( $connections as $key => $connection ) {
292 if ( isset( $connection_results_map[ $connection['connection_id'] ] ) ) {
293 $connections[ $key ]['status'] = $connection_results_map[ $connection['connection_id'] ];
294 }
295 }
296
297 return $connections;
298 }
299
300 /**
301 * Get a connections for a user.
302 *
303 * @param int $connection_id The connection_id.
304
305 * @return array
306 */
307 public function get_connection_for_user( $connection_id ) {
308 foreach ( $this->get_all_connections_for_user() as $connection ) {
309 if ( (int) $connection['connection_id'] === (int) $connection_id ) {
310 return $connection;
311 }
312 }
313 return array();
314 }
315
316 /**
317 * Get the ID of a connection.
318 *
319 * @param array $connection The connection.
320 * @return string
321 */
322 public function get_connection_id( $connection ) {
323 return $connection['connection_data']['id'];
324 }
325
326 /**
327 * Get the unique ID of a connection.
328 *
329 * @param array $connection The connection.
330 * @return string
331 */
332 public function get_connection_unique_id( $connection ) {
333 return $connection['connection_data']['token_id'];
334 }
335
336 /**
337 * Get the meta of a connection.
338 *
339 * @param array $connection The connection.
340 * @return array
341 */
342 public function get_connection_meta( $connection ) {
343 $connection['user_id'] = $connection['connection_data']['user_id']; // Allows for shared connections.
344 return $connection;
345 }
346
347 /**
348 * Show a message that the connection has been removed.
349 */
350 public function display_disconnected() {
351 echo "<div class='updated'>\n";
352 echo '<p>' . esc_html( __( 'That connection has been removed.', 'jetpack-publicize-pkg' ) ) . "</p>\n";
353 echo "</div>\n\n";
354 }
355
356 /**
357 * If applicable, globalize a connection.
358 *
359 * @param string $connection_id Connection ID.
360 */
361 public function globalization( $connection_id ) {
362 if ( isset( $_REQUEST['global'] ) && 'on' === $_REQUEST['global'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce check happens earlier in the process before we get here
363 if ( ! current_user_can( $this->GLOBAL_CAP ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
364 return;
365 }
366
367 $this->globalize_connection( $connection_id );
368 }
369 }
370
371 /**
372 * Globalize a connection.
373 *
374 * @param string $connection_id Connection ID.
375 */
376 public function globalize_connection( $connection_id ) {
377 $xml = new Jetpack_IXR_Client();
378 $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'globalize' );
379
380 if ( ! $xml->isError() ) {
381 $response = $xml->getResponse();
382 $this->receive_updated_publicize_connections( $response );
383 }
384 }
385
386 /**
387 * Unglobalize a connection.
388 *
389 * @param string $connection_id Connection ID.
390 */
391 public function unglobalize_connection( $connection_id ) {
392 $xml = new Jetpack_IXR_Client();
393 $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'unglobalize' );
394
395 if ( ! $xml->isError() ) {
396 $response = $xml->getResponse();
397 $this->receive_updated_publicize_connections( $response );
398 }
399 }
400
401 /**
402 * Grabs a fresh copy of the publicize connections data, if the cache is busted.
403 */
404 public function refresh_connections() {
405 if ( null !== $this->current_connections ) {
406 return;
407 }
408
409 $connections = get_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT );
410 if ( false === $connections ) {
411 $xml = new Jetpack_IXR_Client();
412 $xml->query( 'jetpack.fetchPublicizeConnections' );
413 if ( ! $xml->isError() ) {
414 $response = $xml->getResponse();
415 $this->receive_updated_publicize_connections( $response );
416 }
417 return;
418 }
419 $this->current_connections = $connections;
420 }
421
422 /**
423 * Delete the transient.
424 */
425 public function clear_connections_transient() {
426 delete_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT );
427 }
428
429 /**
430 * Get the Publicize connect URL from Keyring.
431 *
432 * @param string $service_name Name of the service to get connect URL for.
433 * @param string $for What the URL is for. Default 'publicize'.
434 * @return string
435 */
436 public function connect_url( $service_name, $for = 'publicize' ) {
437 return Keyring_Helper::connect_url( $service_name, $for );
438 }
439
440 /**
441 * Get the Publicize refresh URL from Keyring.
442 *
443 * @param string $service_name Name of the service to get refresh URL for.
444 * @param string $for What the URL is for. Default 'publicize'.
445 * @return string
446 */
447 public function refresh_url( $service_name, $for = 'publicize' ) {
448 return Keyring_Helper::refresh_url( $service_name, $for );
449 }
450
451 /**
452 * Get the Publicize disconnect URL from Keyring.
453 *
454 * @param string $service_name Name of the service to get disconnect URL for.
455 * @param mixed $id ID of the conenction to disconnect.
456 * @return string
457 */
458 public function disconnect_url( $service_name, $id ) {
459 return Keyring_Helper::disconnect_url( $service_name, $id );
460 }
461
462 /**
463 * Get social networks, either all available or only those that the site is connected to.
464 *
465 * @since 0.1.0
466 * @since-jetpack 2.0.0
467 *
468 * @since-jetpack 6.6.0 Removed Path. Service closed October 2018.
469 *
470 * @param string $filter Select the list of services that will be returned. Defaults to 'all', accepts 'connected'.
471 * @param false|int $_blog_id Get services for a specific blog by ID, or set to false for current blog. Default false.
472 * @param false|int $_user_id Get services for a specific user by ID, or set to false for current user. Default false.
473 * @return array List of social networks.
474 */
475 public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false ) {
476 $services = array(
477 'facebook' => array(),
478 'twitter' => array(),
479 'linkedin' => array(),
480 'tumblr' => array(),
481 'mastodon' => array(),
482 'instagram-business' => array(),
483 'nextdoor' => array(),
484 'threads' => array(),
485 'bluesky' => array(),
486 );
487
488 if ( 'all' === $filter ) {
489 return $services;
490 }
491
492 $connected_services = array();
493 foreach ( $services as $service_name => $empty ) {
494 $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
495 if ( $connections ) {
496 $connected_services[ $service_name ] = $connections;
497 }
498 }
499 return $connected_services;
500 }
501
502 /**
503 * Get a specific connection. Stub.
504 *
505 * @param string $service_name 'facebook', 'twitter', etc.
506 * @param string $connection_id Connection ID.
507 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
508 * @param false|int $_user_id The user ID. Use false (default) for the current user.
509 * @return void
510 */
511 public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
512 // Stub.
513 }
514
515 /**
516 * Flag a post for Publicize after publishing.
517 *
518 * @param string $new_status New status of the post.
519 * @param string $old_status Old status of the post.
520 * @param WP_Post $post Post object.
521 */
522 public function flag_post_for_publicize( $new_status, $old_status, $post ) {
523 if ( ! $post instanceof \WP_Post || ! $this->post_type_is_publicizeable( $post->post_type ) ) {
524 return;
525 }
526
527 $should_publicize = $this->should_submit_post_pre_checks( $post );
528
529 if ( 'publish' === $new_status && 'publish' !== $old_status ) {
530 /**
531 * Determines whether a post being published gets publicized.
532 *
533 * Side-note: Possibly our most alliterative filter name.
534 *
535 * @since 0.1.0 No longer defaults to true. Adds checks to not publicize based on different contexts.
536 * @since-jetpack 4.1.0
537 *
538 * @param bool $should_publicize Should the post be publicized? Default to true.
539 * @param WP_POST $post Current Post object.
540 */
541 $should_publicize = apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post );
542
543 if ( $should_publicize ) {
544 update_post_meta( $post->ID, $this->PENDING, true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
545 }
546 }
547 }
548
549 /**
550 * Test a connection.
551 *
552 * @param string $service_name Name of the service.
553 * @param array $connection Connection to be tested.
554 */
555 public function test_connection( $service_name, $connection ) {
556 $id = $this->get_connection_id( $connection );
557
558 if ( array_key_exists( $id, $this->test_connection_results ) ) {
559 return $this->test_connection_results[ $id ];
560 }
561
562 $xml = new Jetpack_IXR_Client();
563 $xml->query( 'jetpack.testPublicizeConnection', $id );
564
565 // Bail if all is well.
566 if ( ! $xml->isError() ) {
567 $this->test_connection_results[ $id ] = true;
568 return true;
569 }
570
571 $xml_response = $xml->getResponse();
572 $connection_test_message = $xml_response['faultString'];
573 $connection_error_code = ( empty( $xml_response['faultCode'] ) || ! is_int( $xml_response['faultCode'] ) )
574 ? -1
575 : $xml_response['faultCode'];
576
577 // Set up refresh if the user can.
578 $user_can_refresh = current_user_can( $this->GLOBAL_CAP ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
579 if ( $user_can_refresh ) {
580 /* translators: %s is the name of a social media service */
581 $refresh_text = sprintf( _x( 'Refresh connection with %s', 'Refresh connection with {social media service}', 'jetpack-publicize-pkg' ), $this->get_service_label( $service_name ) );
582 $refresh_url = $this->refresh_url( $service_name );
583 }
584
585 $error_data = array(
586 'user_can_refresh' => $user_can_refresh,
587 'refresh_text' => $refresh_text ?? null,
588 'refresh_url' => $refresh_url ?? null,
589 );
590
591 $this->test_connection_results[ $id ] = new WP_Error( $connection_error_code, $connection_test_message, $error_data );
592
593 return $this->test_connection_results[ $id ];
594 }
595
596 /**
597 * Checks if post has already been shared by Publicize in the past.
598 *
599 * Jetpack uses two methods:
600 * 1. A POST_DONE . 'all' postmeta flag, or
601 * 2. if the post has already been published.
602 *
603 * @since 0.1.0
604 * @since-jetpack 6.7.0
605 *
606 * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
607 *
608 * @return bool True if post has already been shared by Publicize, false otherwise.
609 */
610 public function post_is_done_sharing( $post_id = null ) {
611 // Defaults to current post if $post_id is null.
612 $post = get_post( $post_id );
613 if ( null === $post ) {
614 return false;
615 }
616
617 return 'publish' === $post->post_status || get_post_meta( $post->ID, $this->POST_DONE . 'all', true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
618 }
619
620 /**
621 * Save a flag locally to indicate that this post has already been Publicized via the selected
622 * connections.
623 *
624 * @param int $post_ID Post ID.
625 * @param WP_Post $post Post object.
626 */
627 public function save_publicized( $post_ID, $post = null ) {
628 if ( null === $post ) {
629 return;
630 }
631 // Only do this when a post transitions to being published.
632 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
633 if ( get_post_meta( $post->ID, $this->PENDING, false ) && $this->post_type_is_publicizeable( $post->post_type ) ) {
634 delete_post_meta( $post->ID, $this->PENDING );
635 update_post_meta( $post->ID, $this->POST_DONE . 'all', true );
636 }
637 // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
638 }
639
640 /**
641 * Set post flags for Publicize.
642 *
643 * @param array $flags List of flags.
644 * @param WP_Post $post Post object.
645 * @return array
646 */
647 public function set_post_flags( $flags, $post ) {
648 $flags['publicize_post'] = false;
649 if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
650 return $flags;
651 }
652
653 $should_publicize = $this->should_submit_post_pre_checks( $post );
654
655 /** This filter is already documented in modules/publicize/publicize-jetpack.php */
656 if ( ! apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post ) ) {
657 return $flags;
658 }
659
660 $connected_services = $this->get_all_connections();
661
662 if ( empty( $connected_services ) ) {
663 return $flags;
664 }
665
666 $flags['publicize_post'] = true;
667
668 return $flags;
669 }
670 }
671