Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 2
n/a
0 / 0
CRAP
n/a
0 / 0
Jetpack_ReCaptcha
n/a
0 / 0
n/a
0 / 0
21
n/a
0 / 0
 __construct
n/a
0 / 0
n/a
0 / 0
1
 get_default_config
n/a
0 / 0
n/a
0 / 0
1
 verify
n/a
0 / 0
n/a
0 / 0
12
 get_verify_request_params
n/a
0 / 0
n/a
0 / 0
1
 get_recaptcha_html
n/a
0 / 0
n/a
0 / 0
6
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Google reCAPTCHA utilities, for use in the sharing feature.
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12/**
13 * Class that handles reCAPTCHA.
14 *
15 * @deprecated 11.0
16 */
17class Jetpack_ReCaptcha {
18
19    /**
20     * URL to which requests are POSTed.
21     *
22     * @const string
23     */
24    const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
25
26    /**
27     * Site key to use in HTML code.
28     *
29     * @var string
30     */
31    private $site_key;
32
33    /**
34     * Shared secret for the site.
35     *
36     * @var string
37     */
38    private $secret_key;
39
40    /**
41     * Config for reCAPTCHA instance.
42     *
43     * @var array
44     */
45    private $config;
46
47    /**
48     * Error codes returned from reCAPTCHA API.
49     *
50     * @see https://developers.google.com/recaptcha/docs/verify
51     *
52     * @var array
53     */
54    private $error_codes;
55
56    /**
57     * Create a configured instance to use the reCAPTCHA service.
58     *
59     * @param string $site_key   Site key to use in HTML code.
60     * @param string $secret_key Shared secret between site and reCAPTCHA server.
61     * @param array  $config     Config array to optionally configure reCAPTCHA instance.
62     */
63    public function __construct( $site_key, $secret_key, $config = array() ) {
64        $this->site_key   = $site_key;
65        $this->secret_key = $secret_key;
66        $this->config     = wp_parse_args( $config, $this->get_default_config() );
67
68        $this->error_codes = array(
69            'missing-input-secret'   => __( 'The secret parameter is missing', 'jetpack' ),
70            'invalid-input-secret'   => __( 'The secret parameter is invalid or malformed', 'jetpack' ),
71            'missing-input-response' => __( 'The response parameter is missing', 'jetpack' ),
72            'invalid-input-response' => __( 'The response parameter is invalid or malformed', 'jetpack' ),
73            'invalid-json'           => __( 'Invalid JSON', 'jetpack' ),
74            'unexpected-response'    => __( 'Unexpected response', 'jetpack' ),
75            'unexpected-hostname'    => __( 'Unexpected hostname', 'jetpack' ),
76        );
77    }
78
79    /**
80     * Get default config for this reCAPTCHA instance.
81     *
82     * @return array Default config
83     */
84    public function get_default_config() {
85        return array(
86            'language'       => get_locale(),
87            'script_async'   => false,
88            'script_defer'   => true,
89            'script_lazy'    => false,
90            'tag_class'      => 'g-recaptcha',
91            'tag_attributes' => array(
92                'theme'    => 'light',
93                'type'     => 'image',
94                'tabindex' => 0,
95            ),
96        );
97    }
98
99    /**
100     * Calls the reCAPTCHA siteverify API to verify whether the user passes
101     * CAPTCHA test.
102     *
103     * @param string $response  The value of 'g-recaptcha-response' in the submitted
104     *                          form.
105     * @param string $remote_ip The end user's IP address.
106     *
107     * @return bool|WP_Error Returns true if verified. Otherwise WP_Error is returned.
108     */
109    public function verify( $response, $remote_ip ) {
110        // No need make a request if response is empty.
111        if ( empty( $response ) ) {
112            return new WP_Error( 'missing-input-response', $this->error_codes['missing-input-response'], 400 );
113        }
114
115        $resp = wp_remote_post( self::VERIFY_URL, $this->get_verify_request_params( $response, $remote_ip ) );
116        if ( is_wp_error( $resp ) ) {
117            return $resp;
118        }
119
120        $resp_decoded = json_decode( wp_remote_retrieve_body( $resp ), true );
121        if ( ! $resp_decoded ) {
122            return new WP_Error( 'invalid-json', $this->error_codes['invalid-json'], 400 );
123        }
124
125        // Default error code and message.
126        $error_code    = 'unexpected-response';
127        $error_message = $this->error_codes['unexpected-response'];
128
129        // Use the first error code if exists.
130        if ( isset( $resp_decoded['error-codes'] ) && is_array( $resp_decoded['error-codes'] ) ) {
131            if ( isset( $resp_decoded['error-codes'][0] ) && isset( $this->error_codes[ $resp_decoded['error-codes'][0] ] ) ) {
132                $error_message = $this->error_codes[ $resp_decoded['error-codes'][0] ];
133                $error_code    = $resp_decoded['error-codes'][0];
134            }
135        }
136
137        if ( ! isset( $resp_decoded['success'] ) ) {
138            return new WP_Error( $error_code, $error_message );
139        }
140
141        if ( true !== $resp_decoded['success'] ) {
142            return new WP_Error( $error_code, $error_message );
143        }
144        // Validate the hostname matches expected source
145        if ( isset( $resp_decoded['hostname'] ) ) {
146            $url = wp_parse_url( get_home_url() );
147
148            /**
149             * Allow other valid hostnames.
150             *
151             * This can be useful in cases where the token hostname is expected to be
152             * different from the get_home_url (ex. AMP recaptcha token contains a different hostname)
153             *
154             * @module sharedaddy
155             *
156             * @since 9.1.0
157             *
158             * @param array [ $url['host'] ] List of the valid hostnames to check against.
159             */
160            $valid_hostnames = apply_filters( 'jetpack_recaptcha_valid_hostnames', array( $url['host'] ) );
161
162            if ( ! in_array( $resp_decoded['hostname'], $valid_hostnames, true ) ) {
163                return new WP_Error( 'unexpected-host', $this->error_codes['unexpected-hostname'] );
164            }
165        }
166
167        return true;
168    }
169
170    /**
171     * Get siteverify request parameters.
172     *
173     * @param string $response  The value of 'g-recaptcha-response' in the submitted
174     *                          form.
175     * @param string $remote_ip The end user's IP address.
176     *
177     * @return array
178     */
179    public function get_verify_request_params( $response, $remote_ip ) {
180        return array(
181            'body'      => array(
182                'secret'   => $this->secret_key,
183                'response' => $response,
184                'remoteip' => $remote_ip,
185            ),
186            'sslverify' => true,
187        );
188    }
189
190    /**
191     * Get reCAPTCHA HTML to render.
192     *
193     * @return string
194     */
195    public function get_recaptcha_html() {
196        $url = sprintf(
197            'https://www.google.com/recaptcha/api.js?hl=%s',
198            rawurlencode( $this->config['language'] )
199        );
200
201        $html = sprintf(
202            '
203            <div
204                class="%s"
205                data-sitekey="%s"
206                data-theme="%s"
207                data-type="%s"
208                data-tabindex="%s"
209                data-lazy="%s"
210                data-url="%s"></div>
211            ',
212            esc_attr( $this->config['tag_class'] ),
213            esc_attr( $this->site_key ),
214            esc_attr( $this->config['tag_attributes']['theme'] ),
215            esc_attr( $this->config['tag_attributes']['type'] ),
216            esc_attr( $this->config['tag_attributes']['tabindex'] ),
217            $this->config['script_lazy'] ? 'true' : 'false',
218            esc_attr( $url )
219        );
220
221        if ( ! $this->config['script_lazy'] ) {
222            $html = $html . sprintf(
223                // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
224                '<script src="%s"%s%s></script>
225                ',
226                $url,
227                $this->config['script_async'] && ! $this->config['script_defer'] ? ' async' : '',
228                $this->config['script_defer'] ? ' defer' : ''
229            );
230        }
231
232        return $html;
233    }
234}