Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Keyring_Result_Handler
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 2
20
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Same-origin completion handler for the publicize auth_flow=v2 connect flow.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize;
9
10/**
11 * Handles the popup landing page that public-api redirects back to after a connection is
12 * verified (auth_flow=v2).
13 *
14 * Because Meta/Threads sever `window.opener` via COOP, the popup cannot post its result back
15 * to the opener tab. Instead public-api redirects the popup to this same-origin admin-post
16 * endpoint, which broadcasts the request_id over a BroadcastChannel that the opener is
17 * listening on, then closes itself. The opener then fetches the verified result once.
18 */
19class Keyring_Result_Handler {
20
21    /**
22     * The admin-post action that the connect popup is redirected back to.
23     */
24    const ACTION = 'jetpack_social_keyring_done';
25
26    /**
27     * The BroadcastChannel name shared with the client.
28     *
29     * Must match KEYRING_BROADCAST_CHANNEL in _inc/utils/request-external-access.js.
30     */
31    const CHANNEL = 'jetpack-social-keyring';
32
33    /**
34     * Register the handler.
35     */
36    public static function init() {
37        add_action( 'admin_post_' . self::ACTION, array( __CLASS__, 'handle' ) );
38    }
39
40    /**
41     * Output a minimal page that broadcasts the request_id to the opener and closes the popup.
42     *
43     * @return never
44     */
45    public static function handle() {
46        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only: the request_id is an opaque token reflected back to a same-origin BroadcastChannel; nothing is mutated.
47        $request_id = isset( $_GET['request_id'] ) ? sanitize_key( wp_unslash( $_GET['request_id'] ) ) : '';
48
49        nocache_headers();
50
51        if ( ! headers_sent() ) {
52            header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
53        }
54
55        $close_warning = esc_html__( 'You can close this window now.', 'jetpack-publicize-pkg' );
56        ?>
57<!DOCTYPE html>
58<html <?php language_attributes(); ?>>
59<head>
60    <meta charset="<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
61    <title></title>
62</head>
63<body>
64    <p><?php echo $close_warning; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped above. ?></p>
65    <script>
66        ( function () {
67            try {
68                var channel = new BroadcastChannel( <?php echo wp_json_encode( self::CHANNEL, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?> );
69                channel.postMessage( {
70                    type: 'keyring-result',
71                    requestId: <?php echo wp_json_encode( $request_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
72                } );
73                channel.close();
74            } catch ( e ) {}
75
76            window.setTimeout( function () {
77                window.close();
78            }, 50 );
79        } )();
80    </script>
81</body>
82</html>
83        <?php
84        exit;
85    }
86}