Passed
Branch php-cs-fixer (b9836a)
by Fabio
15:02
created

TReCaptcha::addAttributesToRender()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * TReCaptcha class file
5
 *
6
 * @author Bérczi Gábor <[email protected]>
7
 * @link http://www.devworx.hu/
8
 * @copyright Copyright &copy; 2011 DevWorx
9
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
10
 * @package Prado\Web\UI\WebControls
11
 */
12
13
namespace Prado\Web\UI\WebControls;
14
15
use Prado\Exceptions\TConfigurationException;
16
use Prado\TPropertyValue;
17
use Prado\Web\Javascripts\TJavaScript;
18
use Prado\Web\Javascripts\TJavaScriptLiteral;
19
20
/**
21
 * TReCaptcha class.
22
 *
23
 * TReCaptcha displays a reCAPTCHA (a token displayed as an image) that can be used
24
 * to determine if the input is entered by a real user instead of some program. It can
25
 * also prevent multiple submits of the same form either by accident, or on purpose (ie. spamming).
26
 *
27
 * The reCAPTCHA to solve (a string consisting of two separate words) displayed is automatically
28
 * generated by the reCAPTCHA system at recaptcha.net. However, in order to use the services
29
 * of the site you will need to register and get a public and a private API key pair, and
30
 * supply those to the reCAPTCHA control through setting the {@link setPrivateKey PrivateKey}
31
 * and {@link setPublicKey PublicKey} properties.
32
 *
33
 * Currently the reCAPTCHA API supports only one reCAPTCHA field per page, so you MUST make sure that all
34
 * your input is protected and validated by a single reCAPTCHA control. Placing more than one reCAPTCHA
35
 * control on the page will lead to unpredictable results, and the user will most likely unable to solve
36
 * any of them successfully.
37
 *
38
 * Upon postback, user input can be validated by calling {@link validate()}.
39
 * The {@link TReCaptchaValidator} control can also be used to do validation, which provides
40
 * server-side validation. Calling (@link validate()) will invalidate the token supplied, so all consecutive
41
 * calls to the method - without solving a new captcha - will return false. Therefore if implementing a multi-stage
42
 * input process, you must make sure that you call validate() only once, either at the end of the input process, or
43
 * you store the result till the end of the processing.
44
 *
45
 * The following template shows a typical use of TReCaptcha control:
46
 * <code>
47
 * <com:TReCaptcha ID="Captcha"
48
 *                 PublicKey="..."
49
 *                 PrivateKey="..."
50
 * />
51
 * <com:TReCaptchaValidator ControlToValidate="Captcha"
52
 *                          ErrorMessage="You are challenged!" />
53
 * </code>
54
 *
55
 * @author Bérczi Gábor <[email protected]>
56
 * @package Prado\Web\UI\WebControls
57
 * @since 3.2
58
 */
59
class TReCaptcha extends \Prado\Web\UI\WebControls\TWebControl implements \Prado\Web\UI\IValidatable
60
{
61
	private $_isValid = true;
62
63
	const ChallengeFieldName = 'recaptcha_challenge_field';
64
	const ResponseFieldName = 'recaptcha_response_field';
65
	const RECAPTCHA_API_SERVER = "http://www.google.com/recaptcha/api";
66
	const RECAPTCHA_API_SECURE_SERVER = "https://www.google.com/recaptcha/api";
67
	const RECAPTCHA_VERIFY_SERVER = "www.google.com";
68
	const RECAPTCHA_JS = 'http://www.google.com/recaptcha/api/js/recaptcha_ajax.js';
69
70
	public function getTagName()
71
	{
72
		return 'span';
73
	}
74
	
75
	/**
76
	 * Returns true if this control validated successfully.
77
	 * Defaults to true.
78
	 * @return bool wether this control validated successfully.
79
	 */
80
	public function getIsValid()
81
	{
82
		return $this->_isValid;
83
	}
84
	/**
85
	 * @param bool wether this control is valid.
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\WebControls\wether was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
86
	 */
87
	public function setIsValid($value)
88
	{
89
		$this->_isValid = TPropertyValue::ensureBoolean($value);
90
	}
91
	
92
	public function getValidationPropertyValue()
93
	{
94
		return $this->Request[$this->getChallengeFieldName()];
0 ignored issues
show
Bug Best Practice introduced by
The property Request does not exist on Prado\Web\UI\WebControls\TReCaptcha. Since you implemented __get, consider adding a @property annotation.
Loading history...
95
	}
96
97
	public function getPublicKey()
98
	{
99
		return $this->getViewState('PublicKey');
100
	}
101
102
	public function setPublicKey($value)
103
	{
104
		return $this->setViewState('PublicKey', TPropertyValue::ensureString($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('Pub...::ensureString($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
105
	}
106
107
	public function getPrivateKey()
108
	{
109
		return $this->getViewState('PrivateKey');
110
	}
111
112
	public function setPrivateKey($value)
113
	{
114
		return $this->setViewState('PrivateKey', TPropertyValue::ensureString($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('Pri...::ensureString($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
115
	}
116
	
117
	public function getThemeName()
118
	{
119
		return $this->getViewState('ThemeName');
120
	}
121
122
	public function setThemeName($value)
123
	{
124
		return $this->setViewState('ThemeName', TPropertyValue::ensureString($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('The...::ensureString($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
125
	}
126
127
	public function getCustomTranslations()
128
	{
129
		return TPropertyValue::ensureArray($this->getViewState('CustomTranslations'));
130
	}
131
132
	public function setCustomTranslations($value)
133
	{
134
		return $this->setViewState('CustomTranslations', TPropertyValue::ensureArray($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('Cus...e::ensureArray($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
135
	}
136
137
	public function getLanguage()
138
	{
139
		return $this->getViewState('Language');
140
	}
141
142
	public function setLanguage($value)
143
	{
144
		return $this->setViewState('Language', TPropertyValue::ensureString($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('Lan...::ensureString($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
145
	}
146
147
	public function getCallbackScript()
148
	{
149
		return $this->getViewState('CallbackScript');
150
	}
151
152
	public function setCallbackScript($value)
153
	{
154
		return $this->setViewState('CallbackScript', TPropertyValue::ensureString($value));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setViewState('Cal...::ensureString($value)) targeting Prado\Web\UI\TControl::setViewState() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
155
	}
156
157
	protected function getChallengeFieldName()
158
	{
159
		return /*$this->ClientID.'_'.*/self::ChallengeFieldName;
160
	}
161
	
162
	public function getResponseFieldName()
163
	{
164
		return /*$this->ClientID.'_'.*/self::ResponseFieldName;
165
	}
166
	
167
	public function getClientSideOptions()
168
	{
169
		$options = [];
170
		if ($theme = $this->getThemeName())
171
			$options['theme'] = $theme;
172
		if ($lang = $this->getLanguage())
173
			$options['lang'] = $lang;
174
		if ($trans = $this->getCustomTranslations())
175
			$options['custom_translations'] = $trans;
176
		return $options;
177
	}
178
179
	public function validate()
180
	{
181
		if (!
182
			  (
183
			($challenge = @$_POST[$this->getChallengeFieldName()])
184
			and
185
			($response = @$_POST[$this->getResponseFieldName()])
186
			  )
187
				   )
188
		   return false;
189
190
		return $this->recaptcha_check_answer(
191
			$this->getPrivateKey(),
192
			$_SERVER["REMOTE_ADDR"],
193
			$challenge,
194
			$response
195
		);
196
	}
197
198
	/**
199
	 * Checks for API keys
200
	 * @param mixed event parameter
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\WebControls\event was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
201
	 */
202
	public function onPreRender($param)
203
	{
204
		parent::onPreRender($param);
205
206
		if("" == $this->getPublicKey())
207
			throw new TConfigurationException('recaptcha_publickey_unknown');
208
		if("" == $this->getPrivateKey())
209
			throw new TConfigurationException('recaptcha_privatekey_unknown');
210
211
		// need to register captcha fields so they will be sent back also in callbacks
212
		$page = $this->getPage();
213
		$page->registerRequiresPostData($this->getChallengeFieldName());
214
		$page->registerRequiresPostData($this->getResponseFieldName());
215
	}
216
217
	protected function addAttributesToRender($writer)
218
	{
219
		parent::addAttributesToRender($writer);
220
		$writer->addAttribute('id', $this->getClientID());
221
	}
222
223
	public function regenerateToken()
224
	{
225
		// if we're in a callback, then schedule re-rendering of the control
226
		// if not, don't do anything, because a new challenge will be rendered anyway
227
		if ($this->Page->IsCallback)
0 ignored issues
show
Bug Best Practice introduced by
The property Page does not exist on Prado\Web\UI\WebControls\TReCaptcha. Since you implemented __get, consider adding a @property annotation.
Loading history...
228
			$this->Page->CallbackClient->jQuery($this->getClientID() . ' #recaptcha_reload', 'click');
229
	}
230
231
	public function renderContents($writer)
232
	{
233
		$readyscript = 'jQuery(document).trigger(' . TJavaScript::quoteString('captchaready:' . $this->getClientID()) . ')';
234
		$cs = $this->Page->ClientScript;
0 ignored issues
show
Bug Best Practice introduced by
The property Page does not exist on Prado\Web\UI\WebControls\TReCaptcha. Since you implemented __get, consider adding a @property annotation.
Loading history...
235
		$id = $this->getClientID();
236
		$divid = $id . '_1_recaptchadiv';
237
		$writer->write('<div id="' . htmlspecialchars($divid) . '">');
238
	
239
		if (!$this->Page->IsCallback)
240
			{
241
				$writer->write(TJavaScript::renderScriptBlock(
242
					'var RecaptchaOptions = ' . TJavaScript::jsonEncode($this->getClientSideOptions()) . ';'
243
				));
244
	
245
				$html = $this->recaptcha_get_html($this->getPublicKey());
246
				/*
247
				reCAPTCHA currently does not support multiple validations per page
248
				$html = str_replace(
249
					array(self::ChallengeFieldName,self::ResponseFieldName),
250
					array($this->getChallengeFieldName(),$this->getResponseFieldName()),
251
					$html
252
				);
253
				*/
254
				$writer->write($html);
255
256
				$cs->registerEndScript('ReCaptcha::EventScript', 'jQuery(document).ready(function() { ' . $readyscript . '; } );');
257
			}
258
		else
259
			{
260
				$options = $this->getClientSideOptions();
261
				$options['callback'] = new TJavaScriptLiteral('function() { ' . $readyscript . '; ' . $this->getCallbackScript() . '; }');
262
				$cs->registerScriptFile('ReCaptcha::AjaxScript', self::RECAPTCHA_JS);
263
				$cs->registerEndScript('ReCaptcha::CreateScript::' . $id, implode(' ', [
264
					'if (!jQuery(' . TJavaScript::quoteString('#' . $this->getResponseFieldName()) . '))',
265
					'{',
266
					'Recaptcha.destroy();',
267
					'Recaptcha.create(',
268
						TJavaScript::quoteString($this->getPublicKey()) . ', ',
269
						TJavaScript::quoteString($divid) . ', ',
270
						TJavaScript::encode($options),
271
					');',
272
					'}',
273
				]));
274
			}
275
			
276
		$writer->write('</div>');
277
	}
278
279
280
	/**
281
	 * Gets the challenge HTML (javascript and non-javascript version).
282
	 * This is called from the browser, and the resulting reCAPTCHA HTML widget
283
	 * is embedded within the HTML form it was called from.
284
	 * @param string $pubkey A public key for reCAPTCHA
285
	 * @param string $error The error given by reCAPTCHA (optional, default is null)
286
	 * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false)
287
	 * @return string - The HTML to be embedded in the user's form.
288
	 */
289
	private function recaptcha_get_html($pubkey, $error = null, $use_ssl = false)
290
	{
291
		$server = $use_ssl ? self::RECAPTCHA_API_SECURE_SERVER : $server = self::RECAPTCHA_API_SERVER;
0 ignored issues
show
Unused Code introduced by
The assignment to $server is dead and can be removed.
Loading history...
292
		$errorpart = '';
293
		if ($error)
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
294
			$errorpart = "&amp;error=" . $error;
295
296
		return '<script type="text/javascript" src="' . $server . '/challenge?k=' . $pubkey . $errorpart . '"></script>
297
		<noscript>
298
		<iframe src="' . $server . '/noscript?k=' . $pubkey . $errorpart . '" height="300" width="500" frameborder="0"></iframe><br/>
299
		<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
300
		<input type="hidden" name="recaptcha_response_field" value="manual_challenge"/>
301
		</noscript>';
302
	}
303
304
	/**
305
	 * Encodes the given data into a query string format
306
	 * @param $data - array of string elements to be encoded
307
	 * @return string - encoded request
308
	 */
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
309
	private function recaptcha_qsencode ($data) {
310
		$req = "";
311
		foreach ($data as $key => $value)
312
			$req .= $key . '=' . urlencode(stripslashes($value)) . '&';
313
314
		// Cut the last '&'
315
		$req = substr($req, 0, strlen($req) - 1);
316
		return $req;
317
	}
318
319
	/**
320
	 * Submits an HTTP POST to a reCAPTCHA server
321
	 * @param string $host
322
	 * @param string $path
323
	 * @param array $data
324
	 * @param int port
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\WebControls\port was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
325
	 * @return array response
326
	 */
327
	private function recaptcha_http_post($host, $path, $data, $port = 80)
328
	{
329
		$req = $this->recaptcha_qsencode($data);
330
331
		$http_request = "POST $path HTTP/1.0\r\n";
332
		$http_request .= "Host: $host\r\n";
333
		$http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n";
334
		$http_request .= "Content-Length: " . strlen($req) . "\r\n";
335
		$http_request .= "User-Agent: reCAPTCHA/PHP\r\n";
336
		$http_request .= "\r\n";
337
		$http_request .= $req;
338
339
		$response = '';
340
		if(false == ($fs = @fsockopen($host, $port, $errno, $errstr, 10)))
0 ignored issues
show
introduced by
The condition false == $fs = @fsockope...t, $errno, $errstr, 10) can never be true.
Loading history...
341
			die('Could not open socket');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
342
343
		fwrite($fs, $http_request);
344
345
		while (!feof($fs))
346
			$response .= fgets($fs, 1160); // One TCP-IP packet
347
		fclose($fs);
348
		$response = explode("\r\n\r\n", $response, 2);
349
350
		return $response;
351
	}
352
353
	/**
354
	 * Calls an HTTP POST function to verify if the user's guess was correct
355
	 * @param string $privkey
356
	 * @param string $remoteip
357
	 * @param string $challenge
358
	 * @param string $response
359
	 * @param array $extra_params an array of extra variables to post to the server
360
	 * @return bool
361
	 */
362
	private function recaptcha_check_answer($privkey, $remoteip, $challenge, $response, $extra_params = [])
363
	{
364
		//discard spam submissions
365
		if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0)
366
			return false;
367
368
		$response = $this->recaptcha_http_post(self::RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify",
369
		[
370
			'privatekey' => $privkey,
371
			'remoteip' => $remoteip,
372
			'challenge' => $challenge,
373
			'response' => $response
374
			] + $extra_params
375
		);
376
377
		$answers = explode("\n", $response [1]);
378
379
		if (trim($answers [0]) == 'true')
380
			return true;
381
		else
382
			return false;
383
	}
384
}
385