Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReCaptchaNoCaptcha
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 15
650
0.00% covered (danger)
0.00%
0 / 1
 getFormInformation
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 getCSPUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logCheckError
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getCaptchaParamsFromRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 passCaptcha
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 addCaptchaAPI
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 describeCaptchaType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getMessage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiGetAllowedParams
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 storeCaptcha
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveCaptcha
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCaptcha
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createAuthenticationRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAuthChangeFormFields
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\ConfirmEdit\ReCaptchaNoCaptcha;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Auth\AuthenticationRequest;
7use MediaWiki\Extension\ConfirmEdit\Auth\CaptchaAuthenticationRequest;
8use MediaWiki\Extension\ConfirmEdit\Hooks;
9use MediaWiki\Extension\ConfirmEdit\SimpleCaptcha\SimpleCaptcha;
10use MediaWiki\Html\Html;
11use MediaWiki\Json\FormatJson;
12use MediaWiki\Language\RawMessage;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Message\Message;
15use MediaWiki\Output\OutputPage;
16use MediaWiki\Request\WebRequest;
17use MediaWiki\Status\Status;
18use MediaWiki\User\UserIdentity;
19
20class ReCaptchaNoCaptcha extends SimpleCaptcha {
21    /**
22     * @var string used for renocaptcha-edit, renocaptcha-addurl, renocaptcha-badlogin, renocaptcha-createaccount,
23     * renocaptcha-create, renocaptcha-sendemail via getMessage()
24     */
25    protected static $messagePrefix = 'renocaptcha';
26
27    /** @var string|null */
28    private $error = null;
29
30    /** @inheritDoc */
31    public function getFormInformation( $tabIndex = 1, ?OutputPage $out = null ) {
32        global $wgReCaptchaSiteKey, $wgLang;
33        $lang = htmlspecialchars( urlencode( $wgLang->getCode() ) );
34
35        $output = Html::element( 'div', [
36            'class' => [
37                'g-recaptcha',
38                'mw-confirmedit-captcha-fail' => (bool)$this->error,
39            ],
40            'data-sitekey' => $wgReCaptchaSiteKey
41        ] );
42        $htmlUrlencoded = htmlspecialchars( urlencode( $wgReCaptchaSiteKey ) );
43        $output .= <<<HTML
44<noscript>
45  <div>
46    <div style="width: 302px; height: 422px; position: relative;">
47      <div style="width: 302px; height: 422px; position: absolute;">
48        <iframe
49            src="https://www.recaptcha.net/recaptcha/api/fallback?k={$htmlUrlencoded}&hl={$lang}"
50            frameborder="0" scrolling="no"
51            style="width: 302px; height:422px; border-style: none;">
52        </iframe>
53      </div>
54    </div>
55    <div style="width: 300px; height: 60px; border-style: none;
56                bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
57                background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
58      <textarea id="g-recaptcha-response" name="g-recaptcha-response"
59                class="g-recaptcha-response"
60                style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
61                       margin: 10px 25px; padding: 0px; resize: none;" >
62      </textarea>
63    </div>
64  </div>
65</noscript>
66HTML;
67        return [
68            'html' => $output,
69            'headitems' => [
70                // Insert reCAPTCHA script, in display language, if available.
71                // Language falls back to the browser's display language.
72                // See https://developers.google.com/recaptcha/docs/faq
73                "<script src=\"https://www.recaptcha.net/recaptcha/api.js?hl={$lang}\" async defer></script>"
74            ]
75        ];
76    }
77
78    /** @inheritDoc */
79    public static function getCSPUrls() {
80        return [ 'https://www.recaptcha.net/recaptcha/api.js' ];
81    }
82
83    /**
84     * @param Status|array|string $info
85     */
86    protected function logCheckError( $info ) {
87        if ( $info instanceof Status ) {
88            $errors = $info->getErrorsArray();
89            $error = $errors[0][0];
90        } elseif ( is_array( $info ) ) {
91            $error = implode( ',', $info );
92        } else {
93            $error = $info;
94        }
95
96        wfDebugLog( 'captcha', 'Unable to validate response: ' . $error );
97    }
98
99    /**
100     * @param WebRequest $request
101     * @return array
102     */
103    protected function getCaptchaParamsFromRequest( WebRequest $request ) {
104        // ReCaptchaNoCaptcha combines captcha ID + solution into a single value
105        // API is hardwired to return captchaWord, so use that if the standard isempty
106        // "captchaWord" is sent as "captchaword" by visual editor
107        $index = 'not used';
108        $response = $request->getVal(
109            'g-recaptcha-response',
110            $request->getVal( 'captchaWord', $request->getVal( 'captchaword' ) )
111        );
112        return [ $index, $response ];
113    }
114
115    /**
116     * Check, if the user solved the captcha.
117     *
118     * Based on reference implementation:
119     * https://github.com/google/recaptcha#php
120     *
121     * @param mixed $_ Not used (ReCaptcha v2 puts index and solution in a single string)
122     * @param string $word captcha solution
123     * @param UserIdentity $user
124     * @return bool
125     */
126    protected function passCaptcha( $_, $word, $user ) {
127        global $wgRequest, $wgReCaptchaSecretKey, $wgReCaptchaSendRemoteIP;
128
129        $url = 'https://www.recaptcha.net/recaptcha/api/siteverify';
130        // Build data to append to request
131        $data = [
132            'secret' => $wgReCaptchaSecretKey,
133            'response' => $word,
134        ];
135        if ( $wgReCaptchaSendRemoteIP ) {
136            $data['remoteip'] = $wgRequest->getIP();
137        }
138        $url = wfAppendQuery( $url, $data );
139        $request = MediaWikiServices::getInstance()->getHttpRequestFactory()
140            ->create( $url, [ 'method' => 'POST' ], __METHOD__ );
141        $status = $request->execute();
142        if ( !$status->isOK() ) {
143            $this->error = 'http';
144            $this->logCheckError( $status );
145            return false;
146        }
147        $response = FormatJson::decode( $request->getContent(), true );
148        if ( !$response ) {
149            $this->error = 'json';
150            $this->logCheckError( $this->error );
151            return false;
152        }
153        if ( isset( $response['error-codes'] ) ) {
154            $this->error = 'recaptcha-api';
155            $this->logCheckError( $response['error-codes'] );
156            return false;
157        }
158
159        return $response['success'];
160    }
161
162    /**
163     * @param array &$resultArr
164     */
165    protected function addCaptchaAPI( &$resultArr ) {
166        $resultArr['captcha'] = $this->describeCaptchaType( $this->action );
167        $resultArr['captcha']['error'] = $this->error;
168    }
169
170    /**
171     * @return array
172     */
173    public function describeCaptchaType( ?string $action = null ) {
174        global $wgReCaptchaSiteKey;
175        return [
176            'type' => 'recaptchanocaptcha',
177            'mime' => 'application/javascript',
178            'key' => $wgReCaptchaSiteKey,
179        ];
180    }
181
182    /**
183     * Show a message asking the user to enter a captcha on edit
184     * The result will be treated as wiki text
185     *
186     * @param string $action Action being performed
187     * @return Message
188     */
189    public function getMessage( $action ) {
190        $msg = parent::getMessage( $action );
191        if ( $this->error ) {
192            $msg = new RawMessage( '<div class="error">$1</div>', [ $msg ] );
193        }
194        return $msg;
195    }
196
197    /**
198     * @param ApiBase $module
199     * @param array &$params
200     * @param int $flags
201     * @return bool
202     */
203    public function apiGetAllowedParams( ApiBase $module, &$params, $flags ) {
204        if ( $flags && $this->isAPICaptchaModule( $module ) ) {
205            $params['g-recaptcha-response'] = [
206                ApiBase::PARAM_HELP_MSG => 'renocaptcha-apihelp-param-g-recaptcha-response',
207            ];
208        }
209
210        return true;
211    }
212
213    /** @inheritDoc */
214    public function getError() {
215        return $this->error;
216    }
217
218    /** @inheritDoc */
219    public function storeCaptcha( $info ) {
220        // ReCaptcha is stored by Google; the ID will be generated at that time as well, and
221        // the one returned here won't be used. Just pretend this worked.
222        return 'not used';
223    }
224
225    /** @inheritDoc */
226    public function retrieveCaptcha( $index ) {
227        // just pretend it worked
228        return [ 'index' => $index ];
229    }
230
231    /** @inheritDoc */
232    public function getCaptcha() {
233        // ReCaptcha is handled by frontend code + an external provider; nothing to do here.
234        return [];
235    }
236
237    /**
238     * @return ReCaptchaNoCaptchaAuthenticationRequest
239     */
240    public function createAuthenticationRequest() {
241        return new ReCaptchaNoCaptchaAuthenticationRequest();
242    }
243
244    /**
245     * @param array $requests
246     * @param array $fieldInfo
247     * @param array &$formDescriptor
248     * @param string $action
249     */
250    public function onAuthChangeFormFields(
251        array $requests, array $fieldInfo, array &$formDescriptor, $action
252    ) {
253        global $wgReCaptchaSiteKey;
254
255        /** @var CaptchaAuthenticationRequest $req */
256        $req = AuthenticationRequest::getRequestByClass(
257            $requests,
258            CaptchaAuthenticationRequest::class,
259            true
260        );
261        if ( !$req ) {
262            return;
263        }
264
265        // ugly way to retrieve error information
266        $captcha = Hooks::getInstance( $req->getAction() );
267
268        $formDescriptor['captchaWord'] = [
269            'class' => HTMLReCaptchaNoCaptchaField::class,
270            'key' => $wgReCaptchaSiteKey,
271            'error' => $captcha->getError(),
272        ] + $formDescriptor['captchaWord'];
273    }
274}