Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.12% covered (danger)
26.12%
70 / 268
11.76% covered (danger)
11.76%
4 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Publicize
26.12% covered (danger)
26.12%
70 / 268
11.76% covered (danger)
11.76%
4 / 34
4900.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 add_disconnect_notice
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 force_user_connection
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 admin_page_warning
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 disconnect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 receive_updated_publicize_connections
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
2.06
 filter_usable_connections
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 register_update_publicize_connections_xmlrpc_method
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_all_connections
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 get_connections
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
 get_all_connections_for_user
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
110
 add_connection_test_results
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 get_connection_for_user
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_connection_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_connection_unique_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_connection_meta
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 admin_page_load
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 display_connection_error
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
132
 display_disconnected
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 globalization
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 globalize_connection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 unglobalize_connection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 refresh_connections
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 clear_connections_transient
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 connect_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 refresh_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disconnect_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_services
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 get_connection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flag_post_for_publicize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 test_connection
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 post_is_done_sharing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 save_publicized
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 set_post_flags
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Publicize class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Connection\Tokens;
12use Jetpack_IXR_Client;
13use Jetpack_Options;
14use WP_Error;
15use WP_Post;
16
17/**
18 * Extend the base class with Jetpack-specific functionality.
19 */
20class Publicize extends Publicize_Base {
21
22    const JETPACK_SOCIAL_CONNECTIONS_TRANSIENT = 'jetpack_social_connections';
23
24    /**
25     * Transitory storage of connection testing results.
26     *
27     * @var array
28     */
29    private $test_connection_results = array();
30
31    /**
32     * Property to store the results of fetching the connections.
33     *
34     * @var null|array
35     */
36    private $current_connections = null;
37
38    /**
39     * Add hooks.
40     */
41    public function __construct() {
42        parent::__construct();
43
44        add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( $this, 'register_update_publicize_connections_xmlrpc_method' ) );
45
46        add_action( 'load-settings_page_sharing', array( $this, 'admin_page_load' ), 9 );
47
48        add_action( 'load-settings_page_sharing', array( $this, 'force_user_connection' ) );
49
50        add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
51
52        add_action( 'wp_insert_post', array( $this, 'save_publicized' ), 11, 2 );
53
54        add_action( 'connection_disconnected', array( $this, 'add_disconnect_notice' ) );
55    }
56
57    /**
58     * Add a notice when a connection has been disconnected.
59     */
60    public function add_disconnect_notice() {
61        add_action( 'admin_notices', array( $this, 'display_disconnected' ) );
62    }
63
64    /**
65     * Force user connection before showing the Publicize UI.
66     */
67    public function force_user_connection() {
68        global $current_user;
69
70        $user_token        = ( new Tokens() )->get_access_token( $current_user->ID );
71        $is_user_connected = $user_token && ! is_wp_error( $user_token );
72
73        // If the user is already connected via Jetpack, then we're good.
74        if ( $is_user_connected ) {
75            return;
76        }
77
78        // If they're not connected, then remove the Publicize UI and tell them they need to connect first.
79        global $publicize_ui;
80        remove_action( 'pre_admin_screen_sharing', array( $publicize_ui, 'admin_page' ) );
81
82        // Do we really need `admin_styles`? With the new admin UI, it's breaking some bits.
83        // Jetpack::init()->admin_styles();.
84        add_action( 'pre_admin_screen_sharing', array( $this, 'admin_page_warning' ), 1 );
85    }
86
87    /**
88     * Show a warning when Publicize does not have a connection.
89     */
90    public function admin_page_warning() {
91        $jetpack   = \Jetpack::init();
92        $blog_name = get_bloginfo( 'blogname' );
93        if ( empty( $blog_name ) ) {
94            $blog_name = home_url( '/' );
95        }
96
97        ?>
98        <div id="message" class="updated jetpack-message jp-connect">
99            <div class="jetpack-wrap-container">
100                <div class="jetpack-text-container">
101                    <p>
102                        <?php
103                            printf(
104                                /* translators: %s is the name of the blog */
105                                esc_html( wptexturize( __( "To use Jetpack Social, you'll need to link your %s account to your WordPress.com account using the link below.", 'jetpack-publicize-pkg' ) ) ),
106                                '<strong>' . esc_html( $blog_name ) . '</strong>'
107                            );
108                        ?>
109                    </p>
110                    <p><?php echo esc_html( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack-publicize-pkg' ) ) ); ?></p>
111                </div>
112                <div class="jetpack-install-container">
113                    <p class="submit"><a
114                            href="<?php echo esc_url( $jetpack->build_connect_url( false, menu_page_url( 'sharing', false ) ) ); ?>"
115                            class="button-connector"
116                            id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack-publicize-pkg' ); ?></a>
117                    </p>
118                    <p class="jetpack-install-blurb">
119                        <?php jetpack_render_tos_blurb(); ?>
120                    </p>
121                </div>
122            </div>
123        </div>
124        <?php
125    }
126
127    /**
128     * Remove a Publicize Connection.
129     *
130     * @param string    $service_name 'facebook', 'twitter', etc.
131     * @param string    $connection_id Connection ID.
132     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
133     * @param false|int $_user_id The user ID. Use false (default) for the current user.
134     * @param bool      $force_delete Whether to skip permissions checks.
135     * @return false|void False on failure. Void on success.
136     */
137    public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) {
138        return Keyring_Helper::disconnect( $service_name, $connection_id, $_blog_id, $_user_id, $force_delete );
139    }
140
141    /**
142     * Set updated Publicize connections in a transient.
143     *
144     * @param mixed $publicize_connections Updated connections.
145     * @return true
146     */
147    public function receive_updated_publicize_connections( $publicize_connections ) {
148
149        $publicize_connections = self::filter_usable_connections( $publicize_connections );
150
151        // Populate the cache with the new data.
152        Connections::get_all( array( 'ignore_cache' => true ) );
153
154        $expiry = 3600 * 4;
155        if ( ! set_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT, $publicize_connections, $expiry ) ) {
156            // If the transient has beeen set in another request, the call to set_transient can fail. If so,
157            // we can delete the transient and try again.
158            $this->clear_connections_transient();
159            set_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT, $publicize_connections, $expiry );
160        }
161        // Regardless of whether the transient is set ok, let's set and use the local property for this request.
162        $this->current_connections = $publicize_connections;
163        return true;
164    }
165
166    /**
167     * Drop connections we cannot attribute to a user.
168     *
169     * A missing user ID reads as "shared with everyone" in is_global_connection(), so such a
170     * connection would be exposed to every user on the site. User ID 0 is a genuine shared
171     * connection and is kept; unknown fields pass through, as WPCOM adds to this payload.
172     *
173     * @param mixed $publicize_connections Connections, keyed by service name.
174     * @return array
175     */
176    private static function filter_usable_connections( $publicize_connections ) {
177        // An empty payload is valid - it means the site has no connections left.
178        if ( ! is_array( $publicize_connections ) ) {
179            return array();
180        }
181
182        $usable_connections = array();
183
184        foreach ( $publicize_connections as $service_name => $connections_for_service ) {
185            if ( ! is_array( $connections_for_service ) ) {
186                continue;
187            }
188
189            foreach ( $connections_for_service as $id => $connection ) {
190                if ( ! isset( $connection['connection_data']['user_id'] ) || ! is_numeric( $connection['connection_data']['user_id'] ) ) {
191                    continue;
192                }
193
194                $usable_connections[ $service_name ][ $id ] = $connection;
195            }
196        }
197
198        return $usable_connections;
199    }
200
201    /**
202     * Add method to update Publicize connections.
203     *
204     * @todo Kept as a fallback for the jetpack/v4/publicize/connections/sync REST endpoint that replaced it; remove after a few releases (CONNECT-446).
205     *
206     * @param array $methods Array of registered methods.
207     * @return array
208     */
209    public function register_update_publicize_connections_xmlrpc_method( $methods ) {
210        return array_merge(
211            $methods,
212            array(
213                'jetpack.updatePublicizeConnections' => array( $this, 'receive_updated_publicize_connections' ),
214            )
215        );
216    }
217
218    /**
219     * Get a list of all connections.
220     *
221     * Google Plus is no longer a functional service, so we remove it from the list.
222     *
223     * @return array
224     */
225    public function get_all_connections() {
226        $this->refresh_connections();
227
228        $connections = $this->current_connections;
229
230        if ( empty( $connections ) ) {
231            $connections = array();
232        }
233
234        if ( isset( $connections['google_plus'] ) ) {
235            unset( $connections['google_plus'] );
236        }
237        return $connections;
238    }
239
240    /**
241     * Get connections for a specific service.
242     *
243     * @param string    $service_name 'facebook', 'twitter', etc.
244     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
245     * @param false|int $_user_id The user ID. Use false (default) for the current user.
246     * @return false|object[]|array[]
247     */
248    public function get_connections( $service_name, $_blog_id = false, $_user_id = false ) {
249        if ( false === $_user_id ) {
250            $_user_id = $this->user_id();
251        }
252
253        $connections           = $this->get_all_connections();
254        $connections_to_return = array();
255
256        if ( ! empty( $connections ) && is_array( $connections ) ) {
257            if ( ! empty( $connections[ $service_name ] ) ) {
258                foreach ( $connections[ $service_name ] as $id => $connection ) {
259                    if ( $this->is_global_connection( $connection ) || $_user_id === (int) $connection['connection_data']['user_id'] ) {
260                        $connections_to_return[ $id ] = $connection;
261                    }
262                }
263            }
264
265            return $connections_to_return;
266        }
267
268        return false;
269    }
270
271    /**
272     * Get all connections for a specific user.
273     *
274     * @param array $args Arguments to run operations such as force refresh and connection test results.
275     * @return array
276     */
277    public function get_all_connections_for_user( $args = array() ) {
278        if ( ( isset( $args['clear_cache'] ) && $args['clear_cache'] )
279        || ( isset( $args['test_connections'] ) && $args['test_connections'] ) ) {
280            $this->clear_connections_transient();
281        }
282        $connections = $this->get_all_connections();
283
284        $connections_to_return = array();
285        if ( ! empty( $connections ) ) {
286            foreach ( (array) $connections as $service_name => $connections_for_service ) {
287                foreach ( $connections_for_service as $id => $connection ) {
288                    $user_id = (int) $connection['connection_data']['user_id'];
289                    if ( $user_id === 0 || $this->user_id() === $user_id ) {
290                        $connections_to_return[ $service_name ][ $id ] = $connection;
291                    }
292                }
293            }
294        }
295
296        return $connections_to_return;
297    }
298
299    /**
300     * To add the connection test results to the connections.
301     *
302     * @param array $connections The Jetpack Social connections.
303
304     * @return array
305     */
306    public function add_connection_test_results( $connections ) {
307        $path                   = sprintf( '/sites/%d/publicize/connection-test-results', absint( Jetpack_Options::get_option( 'id' ) ) );
308        $response               = Client::wpcom_json_api_request_as_user( $path, '2', array(), null, 'wpcom' );
309        $connection_results     = json_decode( wp_remote_retrieve_body( $response ), true );
310        $connection_results_map = array();
311
312        foreach ( $connection_results as $connection_result ) {
313            $connection_results_map[ $connection_result['connection_id'] ] = $connection_result['test_success'] ? 'ok' : 'broken';
314        }
315        foreach ( $connections as $key => $connection ) {
316            if ( isset( $connection_results_map[ $connection['connection_id'] ] ) ) {
317                $connections[ $key ]['status'] = $connection_results_map[ $connection['connection_id'] ];
318            }
319        }
320
321        return $connections;
322    }
323
324    /**
325     * Get a connections for a user.
326     *
327     * @param int $connection_id The connection_id.
328
329     * @return array
330     */
331    public function get_connection_for_user( $connection_id ) {
332        foreach ( $this->get_all_connections_for_user() as $connection ) {
333            if ( (int) $connection['connection_id'] === (int) $connection_id ) {
334                return $connection;
335            }
336        }
337        return array();
338    }
339
340    /**
341     * Get the ID of a connection.
342     *
343     * @param array $connection The connection.
344     * @return string
345     */
346    public function get_connection_id( $connection ) {
347        return $connection['connection_data']['id'];
348    }
349
350    /**
351     * Get the unique ID of a connection.
352     *
353     * @param array $connection The connection.
354     * @return string
355     */
356    public function get_connection_unique_id( $connection ) {
357        return $connection['connection_data']['token_id'];
358    }
359
360    /**
361     * Get the meta of a connection.
362     *
363     * @param array $connection The connection.
364     * @return array
365     */
366    public function get_connection_meta( $connection ) {
367        $connection['user_id'] = $connection['connection_data']['user_id']; // Allows for shared connections.
368        return $connection;
369    }
370
371    /**
372     * Show error on settings page if applicable.
373     */
374    public function admin_page_load() {
375        $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
376
377        if ( 'error' === $action ) {
378            add_action( 'pre_admin_screen_sharing', array( $this, 'display_connection_error' ), 9 );
379        }
380    }
381
382    /**
383     * Display an error message.
384     */
385    public function display_connection_error() {
386        $code = false;
387        // phpcs:disable WordPress.Security.NonceVerification.Recommended
388        $service         = isset( $_GET['service'] ) ? sanitize_text_field( wp_unslash( $_GET['service'] ) ) : null;
389        $publicize_error = isset( $_GET['publicize_error'] ) ? sanitize_text_field( wp_unslash( $_GET['publicize_error'] ) ) : null;
390        // phpcs:enable WordPress.Security.NonceVerification.Recommended
391
392        if ( $service ) {
393            /* translators: %s is the name of the Jetpack Social service (e.g. Facebook, Twitter) */
394            $error = sprintf( __( 'There was a problem connecting to %s to create an authorized connection. Please try again in a moment.', 'jetpack-publicize-pkg' ), self::get_service_label( $service ) );
395        } elseif ( $publicize_error ) {
396            $code = strtolower( $publicize_error );
397            switch ( $code ) {
398                case '400':
399                    $error = __( 'An invalid request was made. This normally means that something intercepted or corrupted the request from your server to the Jetpack Server. Try again and see if it works this time.', 'jetpack-publicize-pkg' );
400                    break;
401                case 'secret_mismatch':
402                    $error = __( 'We could not verify that your server is making an authorized request. Please try again, and make sure there is nothing interfering with requests from your server to the Jetpack Server.', 'jetpack-publicize-pkg' );
403                    break;
404                case 'empty_blog_id':
405                    $error = __( 'No blog_id was included in your request. Please try disconnecting Jetpack from WordPress.com and then reconnecting it. Once you have done that, try connecting Jetpack Social again.', 'jetpack-publicize-pkg' );
406                    break;
407                case 'empty_state':
408                    /* translators: %s is the URL of the Jetpack admin page */
409                    $error = sprintf( __( 'No user information was included in your request. Please make sure that your user account has connected to Jetpack. Connect your user account by going to the <a href="%s">Jetpack page</a> within wp-admin.', 'jetpack-publicize-pkg' ), \Jetpack::admin_url() );
410                    break;
411                default:
412                    $error = __( 'Something which should never happen, happened. Sorry about that. If you try again, maybe it will work.', 'jetpack-publicize-pkg' );
413                    break;
414            }
415        } else {
416            $error = __( 'There was a problem connecting with Jetpack Social. Please try again in a moment.', 'jetpack-publicize-pkg' );
417        }
418        // Using the same formatting/style as Jetpack::admin_notices() error.
419        ?>
420        <div id="message" class="jetpack-message jetpack-err">
421            <div class="squeezer">
422                <h2>
423                    <?php
424                        echo wp_kses(
425                            $error,
426                            array(
427                                'a'      => array(
428                                    'href' => true,
429                                ),
430                                'code'   => true,
431                                'strong' => true,
432                                'br'     => true,
433                                'b'      => true,
434                            )
435                        );
436                    ?>
437                </h2>
438                <?php if ( $code ) : ?>
439                    <p>
440                    <?php
441                    printf(
442                        /* translators: %s is the name of the error */
443                        esc_html__( 'Error code: %s', 'jetpack-publicize-pkg' ),
444                        esc_html( stripslashes( $code ) )
445                    );
446                    ?>
447                    </p>
448                <?php endif; ?>
449            </div>
450        </div>
451        <?php
452    }
453
454    /**
455     * Show a message that the connection has been removed.
456     */
457    public function display_disconnected() {
458        echo "<div class='updated'>\n";
459        echo '<p>' . esc_html( __( 'That connection has been removed.', 'jetpack-publicize-pkg' ) ) . "</p>\n";
460        echo "</div>\n\n";
461    }
462
463    /**
464     * If applicable, globalize a connection.
465     *
466     * @param string $connection_id Connection ID.
467     */
468    public function globalization( $connection_id ) {
469        if ( isset( $_REQUEST['global'] ) && 'on' === $_REQUEST['global'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce check happens earlier in the process before we get here
470            if ( ! current_user_can( $this->GLOBAL_CAP ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
471                return;
472            }
473
474            $this->globalize_connection( $connection_id );
475        }
476    }
477
478    /**
479     * Globalize a connection.
480     *
481     * @param string $connection_id Connection ID.
482     */
483    public function globalize_connection( $connection_id ) {
484        $xml = new Jetpack_IXR_Client();
485        $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'globalize' );
486
487        if ( ! $xml->isError() ) {
488            $response = $xml->getResponse();
489            $this->receive_updated_publicize_connections( $response );
490        }
491    }
492
493    /**
494     * Unglobalize a connection.
495     *
496     * @param string $connection_id Connection ID.
497     */
498    public function unglobalize_connection( $connection_id ) {
499        $xml = new Jetpack_IXR_Client();
500        $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'unglobalize' );
501
502        if ( ! $xml->isError() ) {
503            $response = $xml->getResponse();
504            $this->receive_updated_publicize_connections( $response );
505        }
506    }
507
508    /**
509     * Grabs a fresh copy of the publicize connections data, if the cache is busted.
510     */
511    public function refresh_connections() {
512        if ( null !== $this->current_connections ) {
513            return;
514        }
515
516        $connections = get_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT );
517        if ( false === $connections ) {
518            $xml = new Jetpack_IXR_Client();
519            $xml->query( 'jetpack.fetchPublicizeConnections' );
520            if ( ! $xml->isError() ) {
521                $response = $xml->getResponse();
522                $this->receive_updated_publicize_connections( $response );
523            }
524            return;
525        }
526        $this->current_connections = $connections;
527    }
528
529    /**
530     * Delete the transient.
531     */
532    public function clear_connections_transient() {
533        delete_transient( self::JETPACK_SOCIAL_CONNECTIONS_TRANSIENT );
534    }
535
536    /**
537     * Get the Publicize connect URL from Keyring.
538     *
539     * @param string $service_name Name of the service to get connect URL for.
540     * @param string $for What the URL is for. Default 'publicize'.
541     * @return string
542     */
543    public function connect_url( $service_name, $for = 'publicize' ) {
544        return Keyring_Helper::connect_url( $service_name, $for );
545    }
546
547    /**
548     * Get the Publicize refresh URL from Keyring.
549     *
550     * @param string $service_name Name of the service to get refresh URL for.
551     * @param string $for What the URL is for. Default 'publicize'.
552     * @return string
553     */
554    public function refresh_url( $service_name, $for = 'publicize' ) {
555        return Keyring_Helper::refresh_url( $service_name, $for );
556    }
557
558    /**
559     * Get the Publicize disconnect URL from Keyring.
560     *
561     * @param string $service_name Name of the service to get disconnect URL for.
562     * @param mixed  $id ID of the conenction to disconnect.
563     * @return string
564     */
565    public function disconnect_url( $service_name, $id ) {
566        return Keyring_Helper::disconnect_url( $service_name, $id );
567    }
568
569    /**
570     * Get social networks, either all available or only those that the site is connected to.
571     *
572     * @since 0.1.0
573     * @since-jetpack 2.0.0
574     *
575     * @since-jetpack 6.6.0 Removed Path. Service closed October 2018.
576     *
577     * @param string    $filter Select the list of services that will be returned. Defaults to 'all', accepts 'connected'.
578     * @param false|int $_blog_id Get services for a specific blog by ID, or set to false for current blog. Default false.
579     * @param false|int $_user_id Get services for a specific user by ID, or set to false for current user. Default false.
580     * @return array List of social networks.
581     */
582    public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false ) {
583        $services = array(
584            'facebook'           => array(),
585            'twitter'            => array(),
586            'linkedin'           => array(),
587            'tumblr'             => array(),
588            'mastodon'           => array(),
589            'instagram-business' => array(),
590            'nextdoor'           => array(),
591            'threads'            => array(),
592            'bluesky'            => array(),
593        );
594
595        if ( 'all' === $filter ) {
596            return $services;
597        }
598
599        $connected_services = array();
600        foreach ( $services as $service_name => $empty ) {
601            $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
602            if ( $connections ) {
603                $connected_services[ $service_name ] = $connections;
604            }
605        }
606        return $connected_services;
607    }
608
609    /**
610     * Get a specific connection. Stub.
611     *
612     * @param string    $service_name 'facebook', 'twitter', etc.
613     * @param string    $connection_id Connection ID.
614     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
615     * @param false|int $_user_id The user ID. Use false (default) for the current user.
616     * @return void
617     */
618    public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
619        // Stub.
620    }
621
622    /**
623     * Flag a post for Publicize after publishing.
624     *
625     * @param string  $new_status New status of the post.
626     * @param string  $old_status Old status of the post.
627     * @param WP_Post $post Post object.
628     */
629    public function flag_post_for_publicize( $new_status, $old_status, $post ) {
630        if ( ! $post instanceof \WP_Post || ! $this->post_type_is_publicizeable( $post->post_type ) ) {
631            return;
632        }
633
634        $should_publicize = $this->should_submit_post_pre_checks( $post );
635
636        if ( 'publish' === $new_status && 'publish' !== $old_status ) {
637            /**
638             * Determines whether a post being published gets publicized.
639             *
640             * Side-note: Possibly our most alliterative filter name.
641             *
642             * @since 0.1.0 No longer defaults to true. Adds checks to not publicize based on different contexts.
643             * @since-jetpack 4.1.0
644             *
645             * @param bool $should_publicize Should the post be publicized? Default to true.
646             * @param WP_POST $post Current Post object.
647             */
648            $should_publicize = apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post );
649
650            if ( $should_publicize ) {
651                update_post_meta( $post->ID, $this->PENDING, true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
652            }
653        }
654    }
655
656    /**
657     * Test a connection.
658     *
659     * @param string $service_name Name of the service.
660     * @param array  $connection Connection to be tested.
661     */
662    public function test_connection( $service_name, $connection ) {
663        $id = $this->get_connection_id( $connection );
664
665        if ( array_key_exists( $id, $this->test_connection_results ) ) {
666            return $this->test_connection_results[ $id ];
667        }
668
669        $xml = new Jetpack_IXR_Client();
670        $xml->query( 'jetpack.testPublicizeConnection', $id );
671
672        // Bail if all is well.
673        if ( ! $xml->isError() ) {
674            $this->test_connection_results[ $id ] = true;
675            return true;
676        }
677
678        $xml_response            = $xml->getResponse();
679        $connection_test_message = $xml_response['faultString'];
680        $connection_error_code   = ( empty( $xml_response['faultCode'] ) || ! is_int( $xml_response['faultCode'] ) )
681            ? -1
682            : $xml_response['faultCode'];
683
684        // Set up refresh if the user can.
685        $user_can_refresh = current_user_can( $this->GLOBAL_CAP ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
686        if ( $user_can_refresh ) {
687            /* translators: %s is the name of a social media service */
688            $refresh_text = sprintf( _x( 'Refresh connection with %s', 'Refresh connection with {social media service}', 'jetpack-publicize-pkg' ), $this->get_service_label( $service_name ) );
689            $refresh_url  = $this->refresh_url( $service_name );
690        }
691
692        $error_data = array(
693            'user_can_refresh' => $user_can_refresh,
694            'refresh_text'     => $refresh_text ?? null,
695            'refresh_url'      => $refresh_url ?? null,
696        );
697
698        $this->test_connection_results[ $id ] = new WP_Error( $connection_error_code, $connection_test_message, $error_data );
699
700        return $this->test_connection_results[ $id ];
701    }
702
703    /**
704     * Checks if post has already been shared by Publicize in the past.
705     *
706     * Jetpack uses two methods:
707     * 1. A POST_DONE . 'all' postmeta flag, or
708     * 2. if the post has already been published.
709     *
710     * @since 0.1.0
711     * @since-jetpack 6.7.0
712     *
713     * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
714     *
715     * @return bool True if post has already been shared by Publicize, false otherwise.
716     */
717    public function post_is_done_sharing( $post_id = null ) {
718        // Defaults to current post if $post_id is null.
719        $post = get_post( $post_id );
720        if ( null === $post ) {
721            return false;
722        }
723
724        return 'publish' === $post->post_status || get_post_meta( $post->ID, $this->POST_DONE . 'all', true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
725    }
726
727    /**
728     * Save a flag locally to indicate that this post has already been Publicized via the selected
729     * connections.
730     *
731     * @param int     $post_ID Post ID.
732     * @param WP_Post $post Post object.
733     */
734    public function save_publicized( $post_ID, $post = null ) {
735        if ( null === $post ) {
736            return;
737        }
738        // Only do this when a post transitions to being published.
739        // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
740        if ( get_post_meta( $post->ID, $this->PENDING, false ) && $this->post_type_is_publicizeable( $post->post_type ) ) {
741            delete_post_meta( $post->ID, $this->PENDING );
742            update_post_meta( $post->ID, $this->POST_DONE . 'all', true );
743        }
744        // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
745    }
746
747    /**
748     * Set post flags for Publicize.
749     *
750     * @param array   $flags List of flags.
751     * @param WP_Post $post Post object.
752     * @return array
753     */
754    public function set_post_flags( $flags, $post ) {
755        $flags['publicize_post'] = false;
756        if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
757            return $flags;
758        }
759
760        $should_publicize = $this->should_submit_post_pre_checks( $post );
761
762        /** This filter is already documented in modules/publicize/publicize-jetpack.php */
763        if ( ! apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post ) ) {
764            return $flags;
765        }
766
767        $connected_services = $this->get_all_connections();
768
769        if ( empty( $connected_services ) ) {
770            return $flags;
771        }
772
773        $flags['publicize_post'] = true;
774
775        return $flags;
776    }
777}