summaryrefslogtreecommitdiff
path: root/framework/Web/UI/WebControls/TCaptcha.php
blob: 17e9ad34a26073687d926ff64b26eee194e8518f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<?php
/**
 * TCaptcha class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2007 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Web.UI.WebControls
 */

Prado::using('System.Web.UI.WebControls.TImage');

/**
 * TCaptcha class.
 *
 * TCaptcha displays a CAPTCHA (a token displayed as an image) that can be used
 * to determine if the input is entered by a real user instead of some program.
 *
 * The token (a string consisting of alphanumeric characters) displayed is automatically
 * generated and can be configured in several ways. To specify the length of characters
 * in the token, set {@link setMinTokenLength MinTokenLength} and {@link setMaxTokenLength MaxTokenLength}.
 * To use case-insensitive comparison and generate upper-case-only token, set {@link setCaseSensitive CaseSensitive}
 * to false.
 *
 * Upon postback, user input can be validated by calling {@link validate()}.
 * The {@link TCaptchaValidator} control can also be used to do validation, which provides
 * client-side validation besides the server-side validation.  By default, the token will
 * remain the same during multiple postbacks. A new one can be generated by calling
 * {@link regenerateToken()} manually.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Web.UI.WebControls
 * @since 3.1.1
 */
class TCaptcha extends TImage
{
	const MIN_TOKEN_LENGTH=4;
	const MAX_TOKEN_LENGTH=40;
	private $_privateKey;

	public function onInit($param)
	{
		parent::onInit($param);
		$this->checkRequirements();
	}

	/**
	 * @return integer the minimum length of the token. Defaults to 5.
	 */
	public function getMinTokenLength()
	{
		return $this->getViewState('MinTokenLength',5);
	}

	/**
	 * @param integer the minimum length of the token. It must be between 2 and 40.
	 */
	public function setMinTokenLength($value)
	{
		$length=TPropertyValue::ensureInteger($value);
		if($length>=self::MIN_TOKEN_LENGTH && $length<=self::MAX_TOKEN_LENGTH)
			$this->setViewState('MinTokenLength',$length,5);
		else
			throw new TConfigurationException('captcha_mintokenlength_invalid',self::MIN_TOKEN_LENGTH,self::MAX_TOKEN_LENGTH);
	}

	/**
	 * @return integer the maximum length of the token. Defaults to 8.
	 */
	public function getMaxTokenLength()
	{
		return $this->getViewState('MaxTokenLength',8);
	}

	/**
	 * @param integer the maximum length of the token. It must be between 2 and 40.
	 */
	public function setMaxTokenLength($value)
	{
		$length=TPropertyValue::ensureInteger($value);
		if($length>=self::MIN_TOKEN_LENGTH && $length<=self::MAX_TOKEN_LENGTH)
			$this->setViewState('MaxTokenLength',$length,8);
		else
			throw new TConfigurationException('captcha_maxtokenlength_invalid',self::MIN_TOKEN_LENGTH,self::MAX_TOKEN_LENGTH);
	}

	/**
	 * @return boolean whether the token should be treated as case-sensitive. Defaults to true.
	 */
	public function getCaseSensitive()
	{
		return $this->getViewState('CaseSensitive',true);
	}

	/**
	 * @param boolean whether the token should be treated as case-sensitive. If false, only upper-case letters will appear in the token.
	 */
	public function setCaseSensitive($value)
	{
		$this->setViewState('CaseSensitive',TPropertyValue::ensureBoolean($value),true);
	}

	/**
	 * @return string the characters that may appear in the token. Defaults to '234578adefhijmnrtABDEFGHJLMNQRT'.
	 */
	public function getTokenAlphabet()
	{
		return $this->getViewState('TokenAlphabet','234578adefhijmnrtABDEFGHJLMNQRT');
	}

	/**
	 * @param string the characters that may appear in the token. At least 2 characters must be specified.
	 */
	public function setTokenAlphabet($value)
	{
		if(strlen($value)<2)
			throw new TConfigurationException('captcha_tokenalphabet_invalid');
		$this->setViewState('TokenAlphabet',$value,'234578adefhijmnrtABDEFGHJLMNQRT');
	}

	/**
	 * @return string the public key used for generating the token. A random one will be generated and returned if this is not set.
	 */
	public function getPublicKey()
	{
		if(($publicKey=$this->getViewState('PublicKey',''))==='')
		{
			$publicKey=$this->generateRandomKey();
			$this->setPublicKey($publicKey);
		}
		return $publicKey;
	}

	/**
	 * @param string the public key used for generating the token. A random one will be generated if this is not set.
	 */
	public function setPublicKey($value)
	{
		$this->setViewState('PublicKey',$value,'');
	}

	/**
	 * @return string the token that will be displayed
	 */
	public function getToken()
	{
		return $this->generateToken($this->getPublicKey(),$this->getPrivateKey(),$this->getTokenAlphabet(),$this->getTokenLength(),$this->getCaseSensitive());
	}

	/**
	 * @return integer the length of the token to be generated.
	 */
	protected function getTokenLength()
	{
		if(($tokenLength=$this->getViewState('TokenLength'))===null)
		{
			$minLength=$this->getMinTokenLength();
			$maxLength=$this->getMaxTokenLength();
			if($minLength>$maxLength)
				$tokenLength=rand($maxLength,$minLength);
			else if($minLength<$maxLength)
				$tokenLength=rand($minLength,$maxLength);
			else
				$tokenLength=$minLength;
			$this->setViewState('TokenLength',$tokenLength);
		}
		return $tokenLength;
	}

	/**
	 * @return string the private key used for generating the token. This is randomly generated and kept in a file for persistency.
	 */
	public function getPrivateKey()
	{
		if($this->_privateKey===null)
		{
			$fileName=$this->generatePrivateKeyFile();
			$content=file_get_contents($fileName);
			$matches=array();
			if(preg_match("/privateKey='(.*?)'/ms",$content,$matches)>0)
				$this->_privateKey=$matches[1];
			else
				throw new TConfigurationException('captcha_privatekey_unknown');
		}
		return $this->_privateKey;
	}

	/**
	 * Validates a user input with the token.
	 * @param string user input
	 * @return boolean if the user input is not the same as the token.
	 */
	public function validate($input)
	{
		return $this->getToken()===($this->getCaseSensitive()?$input:strtoupper($input));
	}

	/**
	 * Regenerates the token to be displayed.
	 * By default, a token, once generated, will remain the same during the following page postbacks.
	 * Calling this method will generate a new token.
	 */
	public function regenerateToken()
	{
		$this->clearViewState('TokenLength');
		$this->setPublicKey('');
		$this->clearViewState('TokenGenerated');
	}

	/**
	 * Configures the image URL that shows the token.
	 * @param mixed event parameter
	 */
	public function onPreRender($param)
	{
		parent::onPreRender($param);
		if(!$this->getViewState('TokenGenerated',false))
		{
			$manager=$this->getApplication()->getAssetManager();
			$manager->publishFilePath($this->getFontFile());
			$url=$manager->publishFilePath($this->getCaptchaScriptFile());
			$url.='?options='.urlencode($this->getTokenImageOptions());
			$this->setImageUrl($url);

			$this->setViewState('TokenGenerated',true);
		}
	}

	/**
	 * @return string the options to be passed to the token image generator
	 */
	protected function getTokenImageOptions()
	{
		$privateKey=$this->getPrivateKey();  // call this method to ensure private key is generated
		$token=$this->getToken();
		$options=array();
		$options['publicKey']=$this->getPublicKey();
		$options['tokenLength']=strlen($token);
		$options['caseSensitive']=$this->getCaseSensitive();
		$options['alphabet']=$this->getTokenAlphabet();
		$str=serialize($options);
		return base64_encode(md5($privateKey.$str).$str);
	}

	/**
	 * @return string the file path of the PHP script generating the token image
	 */
	protected function getCaptchaScriptFile()
	{
		return dirname(__FILE__).DIRECTORY_SEPARATOR.'assets'.DIRECTORY_SEPARATOR.'captcha.php';
	}

	protected function getFontFile()
	{
		return dirname(__FILE__).DIRECTORY_SEPARATOR.'assets'.DIRECTORY_SEPARATOR.'verase.ttf';
	}

	/**
	 * Generates a file with a randomly generated private key.
	 * @return string the path of the file keeping the private key
	 */
	protected function generatePrivateKeyFile()
	{
		$captchaScript=$this->getCaptchaScriptFile();
		$path=dirname($this->getApplication()->getAssetManager()->getPublishedPath($captchaScript));
		$fileName=$path.DIRECTORY_SEPARATOR.'captcha_key.php';
		if(!is_file($fileName))
		{
			@mkdir($path);
			$key=$this->generateRandomKey();
			$content="<?php
\$privateKey='$key';
?>";
			file_put_contents($fileName,$content);
		}
		return $fileName;
	}

	/**
	 * @return string a randomly generated key
	 */
	protected function generateRandomKey()
	{
		return md5(rand().rand().rand().rand());
	}

	/**
	 * Generates the token.
	 * @param string public key
	 * @param string private key
	 * @param integer the length of the token
	 * @param boolean whether the token is case sensitive
	 * @return string the token generated.
	 */
	protected function generateToken($publicKey,$privateKey,$alphabet,$tokenLength,$caseSensitive)
	{
		$token=substr($this->hash2string(md5($publicKey.$privateKey),$alphabet).$this->hash2string(md5($privateKey.$publicKey),$alphabet),0,$tokenLength);
		return $caseSensitive?$token:strtoupper($token);
	}

	/**
	 * Converts a hash string into a string with characters consisting of alphanumeric characters.
	 * @param string the hexadecimal representation of the hash string
	 * @param string the alphabet used to represent the converted string. If empty, it means '234578adefhijmnrtwyABDEFGHIJLMNQRTWY', which excludes those confusing characters.
	 * @return string the converted string
	 */
	protected function hash2string($hex,$alphabet='')
	{
		if(strlen($alphabet)<2)
			$alphabet='234578adefhijmnrtABDEFGHJLMNQRT';
		$hexLength=strlen($hex);
		$base=strlen($alphabet);
		$result='';
		for($i=0;$i<$hexLength;$i+=6)
		{
			$number=hexdec(substr($hex,$i,6));
			while($number)
			{
				$result.=$alphabet[$number%$base];
				$number=floor($number/$base);
			}
		}
		return $result;
	}

	/**
	 * Checks the requirements needed for generating CAPTCHA images.
	 */
	protected function checkRequirements()
	{
		if(!extension_loaded('gd'))
			throw new TConfigurationException('captcha_gd2_required');
		if(!function_exists('imagettftext'))
			throw new TConfigurationException('captcha_imagettftext_required');
		if(!function_exists('imagepng'))
			throw new TConfigurationException('captcha_imagepng_required');
	}
}

?>