Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_Smart_Dictation
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 8
1332
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
 is_proxied
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_block_editor
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 determine_iso_639_locale
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 has_paid_plan
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 current_user_is_in_paid_rollout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_scripts
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
210
 get_assets_json
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * WordPress.com Smart Dictation
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8namespace A8C\FSE;
9
10use Automattic\Jetpack\Status\Host;
11
12/**
13 * Class WPCOM_Smart_Dictation
14 */
15class WPCOM_Smart_Dictation {
16    /**
17     * Percentage of paid users eligible for Smart Dictation.
18     */
19    private const PAID_USER_ROLLOUT_PERCENTAGE = 100;
20
21    /**
22     * WPCOM_Smart_Dictation constructor.
23     */
24    public static function init() {
25        add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ), 100 );
26    }
27
28    /**
29     * Returns whether the current request is coming from the a8c proxy.
30     */
31    private static function is_proxied() {
32        return isset( $_SERVER['A8C_PROXIED_REQUEST'] )
33            ? sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) )
34            : defined( 'A8C_PROXIED_REQUEST' ) && A8C_PROXIED_REQUEST;
35    }
36
37    /**
38     * Returns true if the current screen if the block editor.
39     */
40    public static function is_block_editor() {
41        global $current_screen;
42
43        if ( ! $current_screen ) {
44            return false;
45        }
46
47        return $current_screen->is_block_editor() && $current_screen->id !== 'widgets';
48    }
49
50    /**
51     * Returns ISO 639 conforming locale string of the current user.
52     *
53     * @return string ISO 639 locale string e.g. "en"
54     */
55    private static function determine_iso_639_locale() {
56        $language = get_user_locale();
57        $language = strtolower( $language );
58
59        if ( in_array( $language, array( 'pt_br', 'pt-br', 'zh_tw', 'zh-tw', 'zh_cn', 'zh-cn' ), true ) ) {
60            $language = str_replace( '_', '-', $language );
61        } else {
62            $language = preg_replace( '/([-_].*)$/i', '', $language );
63        }
64
65        if ( empty( $language ) ) {
66            return 'en';
67        }
68
69        return $language;
70    }
71
72    /**
73     * Returns whether the current simple site has a paid plan.
74     */
75    private static function has_paid_plan() {
76        if ( class_exists( '\WPCOM_Store_API' ) ) {
77            $current_plan = \WPCOM_Store_API::get_current_plan( get_current_blog_id() );
78
79            if ( is_array( $current_plan ) ) {
80                return ! ( $current_plan['is_free'] ?? true );
81            }
82        }
83
84        if ( function_exists( '\wpcom_get_site_purchases' ) ) {
85            $site_purchases = \wpcom_get_site_purchases();
86
87            if ( ! is_array( $site_purchases ) ) {
88                return false;
89            }
90
91            $bundle_purchases = wp_list_filter( $site_purchases, array( 'product_type' => 'bundle' ) );
92
93            return ! empty( $bundle_purchases );
94        }
95
96        return false;
97    }
98
99    /**
100     * Returns whether the current user is in the paid users rollout.
101     */
102    private static function current_user_is_in_paid_rollout() {
103        $user_id = get_current_user_id();
104
105        return 0 !== $user_id
106            && self::has_paid_plan()
107            && $user_id % 100 < self::PAID_USER_ROLLOUT_PERCENTAGE;
108    }
109
110    /**
111     * Enqueue the Smart Dictation assets.
112     */
113    public static function enqueue_scripts() {
114        if ( ! ( new Host() )->is_wpcom_simple() ) {
115            return;
116        }
117
118        if ( self::is_block_editor() && 'en' === self::determine_iso_639_locale() && ( self::is_proxied() || self::current_user_is_in_paid_rollout() ) ) {
119            $asset_file = self::get_assets_json( 'widgets.wp.com/wpcom-smart-dictation/wpcom-smart-dictation.asset.json' );
120            $is_a11n    = function_exists( '\is_automattician' ) && \is_automattician();
121            $version    = ( is_array( $asset_file ) && isset( $asset_file['version'] ) )
122                ? $asset_file['version']
123                : false;
124            wp_enqueue_script(
125                'wpcom-smart-dictation',
126                'https://widgets.wp.com/wpcom-smart-dictation/wpcom-smart-dictation.min.js',
127                array(),
128                $version,
129                true
130            );
131
132            $user_id      = get_current_user_id();
133            $user_data    = get_userdata( $user_id );
134            $username     = $user_data ? $user_data->user_login : null;
135            $user_email   = $user_data ? $user_data->user_email : null;
136            $display_name = $user_data ? $user_data->display_name : null;
137            $avatar_url   = $user_data ? ( function_exists( 'wpcom_get_avatar_url' ) ? wpcom_get_avatar_url( $user_email, 64, '', true )[0] : get_avatar_url( $user_id ) ) : null;
138
139            wp_add_inline_script(
140                'wpcom-smart-dictation',
141                'if ( typeof wpcomSmartDictationData === "undefined" ) { var wpcomSmartDictationData = ' . wp_json_encode(
142                    array(
143                        'isProxied'   => boolval( self::is_proxied() ),
144                        'currentUser' => array(
145                            'ID'           => $user_id,
146                            'username'     => $username,
147                            'display_name' => $display_name,
148                            'avatar_URL'   => $avatar_url,
149                            'email'        => $user_email,
150                            'is_a11n'      => $is_a11n,
151                        ),
152                        'locale'      => self::determine_iso_639_locale(),
153                    ),
154                    JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
155                ) . '; }',
156                'before'
157            );
158
159            wp_enqueue_style(
160                'wpcom-smart-dictation-style',
161                'https://widgets.wp.com/wpcom-smart-dictation/wpcom-smart-dictation.css',
162                array(),
163                $version
164            );
165        }
166    }
167
168    /**
169     * Get the asset via file-system on wpcom and via network on Atomic sites.
170     *
171     * Checks a transient cache of successful remote fetches before the filesystem or a new HTTP request.
172     * Failures are not cached.
173     *
174     * @param string $filepath The path to the asset file.
175     * @return array|null Decoded manifest, or null when JSON is invalid or empty.
176     */
177    private static function get_assets_json( $filepath ) {
178        $local_path = ABSPATH . $filepath;
179
180        if ( file_exists( $local_path ) ) {
181            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local file, not a remote URL.
182            return json_decode( file_get_contents( $local_path ), true );
183        }
184
185        $cache_key = 'jetpack_mu_wpcom_sd_asset_' . md5( $filepath );
186        $cached    = get_transient( $cache_key );
187
188        if ( is_array( $cached ) ) {
189            return $cached;
190        }
191
192        $request = wp_remote_get(
193            'https://' . $filepath,
194            array( 'timeout' => 10 )
195        );
196
197        $decoded = json_decode( wp_remote_retrieve_body( $request ), true );
198        if ( is_array( $decoded ) ) {
199            set_transient( $cache_key, $decoded, 12 * HOUR_IN_SECONDS );
200        }
201        return $decoded;
202    }
203}
204
205add_action( 'init', array( __NAMESPACE__ . '\WPCOM_Smart_Dictation', 'init' ) );