Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.90% covered (warning)
78.90%
86 / 109
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Keyring_Helper
78.90% covered (warning)
78.90%
86 / 109
50.00% covered (danger)
50.00%
4 / 8
29.41
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 api_url
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 connect_url
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 refresh_url
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 disconnect_url
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 intercept_request
70.00% covered (warning)
70.00%
42 / 60
0.00% covered (danger)
0.00%
0 / 1
17.56
 disconnect
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
1<?php
2/**
3 * Keyring helper.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize;
9
10use Automattic\Jetpack\Connection\Secrets;
11use Automattic\Jetpack\Paths;
12use Jetpack_IXR_Client;
13use Jetpack_Options;
14
15/**
16 * Starts Keyring connection requests and removes Publicize connections, both
17 * through public-api.
18 */
19class Keyring_Helper {
20    /**
21     * Class instance
22     *
23     * @var Keyring_Helper
24     */
25    private static $instance = null;
26
27    /**
28     * Initialize instance.
29     */
30    public static function init() {
31        if ( null === self::$instance ) {
32            self::$instance = new Keyring_Helper();
33        }
34
35        return self::$instance;
36    }
37
38    /**
39     * Services whose connection request starts from the site. Jetpack Social's
40     * networks start theirs from WordPress.com instead.
41     */
42    const SERVICES = array(
43        'google_site_verification' => array(
44            'for' => 'other',
45        ),
46    );
47
48    /**
49     * Constructor
50     */
51    private function __construct() {
52        add_action( 'admin_init', array( __CLASS__, 'intercept_request' ) );
53    }
54
55    /**
56     * Gets a URL to the public-api actions. Works like WP's admin_url.
57     * On WordPress.com this is/calls Keyring::admin_url.
58     *
59     * @param string $service Shortname of a specific service.
60     * @param array  $params  Parameters to append to an API connection URL.
61     *
62     * @return string URL to specific public-api process
63     */
64    private static function api_url( $service = false, $params = array() ) {
65        /**
66         * Filters the API URL used to interact with WordPress.com.
67         *
68         * @since 0.1.0
69         * @since-jetpack 2.0.0
70         *
71         * @param string https://public-api.wordpress.com/connect/?jetpack=publicize Default Publicize API URL.
72         */
73        $url = apply_filters( 'publicize_api_url', 'https://public-api.wordpress.com/connect/?jetpack=publicize' );
74
75        if ( $service ) {
76            $url = add_query_arg( array( 'service' => $service ), $url );
77        }
78
79        if ( array() !== $params ) {
80            $url = add_query_arg( $params, $url );
81        }
82
83        return $url;
84    }
85
86    /**
87     * Build a connection URL (admin URL with unique query args to create a connection).
88     *
89     * @param string $service_name Service name.
90     * @param string $for          Feature name.
91     */
92    public static function connect_url( $service_name, $for ) {
93        return add_query_arg(
94            array(
95                'action'           => 'request',
96                'service'          => $service_name,
97                'kr_nonce'         => wp_create_nonce( 'keyring-request' ),
98                'nonce'            => wp_create_nonce( "keyring-request-$service_name" ),
99                'for'              => $for,
100                'publicize_action' => 1,
101            ),
102            admin_url()
103        );
104    }
105
106    /**
107     * Build a URL to refresh a connection (admin URL with unique query args to refresh a connection).
108     * Similar to connect_url, but with a refresh parameter.
109     *
110     * @param string $service_name Service name.
111     * @param string $for          Feature name.
112     */
113    public static function refresh_url( $service_name, $for ) {
114        return add_query_arg(
115            array(
116                'action'           => 'request',
117                'service'          => $service_name,
118                'kr_nonce'         => wp_create_nonce( 'keyring-request' ),
119                'refresh'          => 1,
120                'for'              => $for,
121                'nonce'            => wp_create_nonce( "keyring-request-$service_name" ),
122                'publicize_action' => 1,
123            ),
124            admin_url()
125        );
126    }
127
128    /**
129     * Build a URL to delete a connection (admin URL with unique query args to delete a connection).
130     *
131     * @param string $service_name Service name.
132     * @param string $id           Connection ID.
133     */
134    public static function disconnect_url( $service_name, $id ) {
135        return add_query_arg(
136            array(
137                'action'           => 'delete',
138                'service'          => $service_name,
139                'id'               => $id,
140                'kr_nonce'         => wp_create_nonce( 'keyring-request' ),
141                'nonce'            => wp_create_nonce( "keyring-request-$service_name" ),
142                'publicize_action' => 1,
143            ),
144            admin_url()
145        );
146    }
147
148    /**
149     * Handle a Keyring connection request or deletion started from connect_url(), refresh_url() or disconnect_url().
150     */
151    public static function intercept_request() {
152        if ( ! empty( $_GET['publicize_action'] ) && isset( $_GET['action'] ) ) {
153            $service_name = null;
154
155            if ( isset( $_GET['service'] ) ) {
156                $service_name = filter_var( wp_unslash( $_GET['service'] ) );
157            }
158
159            switch ( $_GET['action'] ) {
160
161                case 'request':
162                    check_admin_referer( 'keyring-request', 'kr_nonce' );
163                    check_admin_referer( "keyring-request-$service_name", 'nonce' );
164
165                    $verification = ( new Secrets() )->generate( 'publicize' );
166                    if ( ! $verification ) {
167                        $url = ( new Paths() )->admin_url( 'page=jetpack#/settings' );
168                        wp_die(
169                            sprintf(
170                                wp_kses(
171                                    /* Translators: placeholder is a URL to a Settings page. */
172                                    __( "Jetpack is not connected. Please connect Jetpack by visiting <a href='%s'>Settings</a>.", 'jetpack-publicize-pkg' ),
173                                    array(
174                                        'a' => array(
175                                            'href' => array(),
176                                        ),
177                                    )
178                                ),
179                                esc_url( $url )
180                            )
181                        );
182
183                    }
184                    $stats_options = get_option( 'stats_options' );
185                    $wpcom_blog_id = Jetpack_Options::get_option( 'id' );
186                    $wpcom_blog_id = ! empty( $wpcom_blog_id ) ? $wpcom_blog_id : $stats_options['blog_id'];
187
188                    $for = isset( $_GET['for'] ) ? sanitize_text_field( wp_unslash( $_GET['for'] ) ) : 'publicize';
189
190                    $custom_inputs = array();
191
192                    // For Bluesky.
193                    if ( isset( $_GET['handle'] ) && isset( $_GET['app_password'] ) ) {
194                        $custom_inputs['handle'] = sanitize_text_field( wp_unslash( $_GET['handle'] ) );
195
196                        $custom_inputs['app_password'] = sanitize_text_field( wp_unslash( $_GET['app_password'] ) );
197                    }
198
199                    // For Mastodon.
200                    if ( isset( $_GET['instance'] ) ) {
201                        $custom_inputs['instance'] = sanitize_text_field( wp_unslash( $_GET['instance'] ) );
202                    }
203
204                    $user     = wp_get_current_user();
205                    $redirect = self::api_url(
206                        $service_name,
207                        urlencode_deep(
208                            $custom_inputs +
209                            array(
210                                'action'   => 'request',
211                                'for'      => $for,
212                                'siteurl'  => site_url(),
213                                'state'    => $user->ID,
214                                'blog_id'  => $wpcom_blog_id,
215                                'secret_1' => $verification['secret_1'],
216                                'secret_2' => $verification['secret_2'],
217                                'eol'      => $verification['exp'],
218                            )
219                        )
220                    );
221                    wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- The API URL is an external URL and is filterable.
222                    exit( 0 );
223
224                case 'delete':
225                    $id = isset( $_GET['id'] ) ? filter_var( wp_unslash( $_GET['id'] ) ) : null;
226
227                    check_admin_referer( 'keyring-request', 'kr_nonce' );
228                    check_admin_referer( "keyring-request-$service_name", 'nonce' );
229
230                    self::disconnect( $service_name, $id );
231
232                    do_action( 'connection_disconnected', $service_name );
233                    break;
234            }
235        }
236    }
237
238    /**
239     * Remove a Publicize connection
240     *
241     * @param string   $service_name  Service name.
242     * @param string   $connection_id Connection ID.
243     * @param int|bool $_blog_id      Blog ID.
244     * @param int|bool $_user_id      User ID.
245     * @param bool     $force_delete  Force delete the connection.
246     */
247    public static function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
248        $xml = new Jetpack_IXR_Client();
249        $xml->query( 'jetpack.deletePublicizeConnection', $connection_id );
250
251        if ( ! $xml->isError() ) {
252            Jetpack_Options::update_option( 'publicize_connections', $xml->getResponse() );
253        } else {
254            return false;
255        }
256    }
257}