Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TCaptcha class file
4
 *
5
 * @author Qiang Xue <qiang.xue@gmail.com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TCaptcha.php 2541 2008-10-21 15:05:13Z qiang.xue $
10
 * @package System.Web.UI.WebControls
11
 */
12
 
13
Prado::using('System.Web.UI.WebControls.TImage');
14
 
15
/**
16
 * TCaptcha class.
17
 *
18
 * TCaptcha displays a CAPTCHA (a token displayed as an image) that can be used
19
 * to determine if the input is entered by a real user instead of some program.
20
 *
21
 * Unlike other CAPTCHA scripts, TCaptcha does not need session or cookie.
22
 *
23
 * The token (a string consisting of alphanumeric characters) displayed is automatically
24
 * generated and can be configured in several ways. To specify the length of characters
25
 * in the token, set {@link setMinTokenLength MinTokenLength} and {@link setMaxTokenLength MaxTokenLength}.
26
 * To use case-insensitive comparison and generate upper-case-only token, set {@link setCaseSensitive CaseSensitive}
27
 * to false. Advanced users can try to set {@link setTokenAlphabet TokenAlphabet}, which
28
 * specifies what characters can appear in tokens.
29
 *
30
 * The validation of the token is related with two properties: {@link setTestLimit TestLimit}
31
 * and {@link setTokenExpiry TokenExpiry}. The former specifies how many times a token can
32
 * be tested with on the server side, and the latter says when a generated token will expire.
33
 *
34
 * To specify the appearance of the generated token image, set {@link setTokenImageTheme TokenImageTheme}
35
 * to be an integer between 0 and 63. And to adjust the generated image size, set {@link setTokenFontSize TokenFontSize}
36
 * (you may also set {@link TWebControl::setWidth Width}, but the scaled image may not look good.)
37
 * By setting {@link setChangingTokenBackground ChangingTokenBackground} to true, the image background
38
 * of the token will be variating even though the token is the same during postbacks.
39
 *
40
 * Upon postback, user input can be validated by calling {@link validate()}.
41
 * The {@link TCaptchaValidator} control can also be used to do validation, which provides
42
 * client-side validation besides the server-side validation.  By default, the token will
43
 * remain the same during multiple postbacks. A new one can be generated by calling
44
 * {@link regenerateToken()} manually.
45
 *
46
 * The following template shows a typical use of TCaptcha control:
47
 * <code>
48
 * <com:TCaptcha ID="Captcha" />
49
 * <com:TTextBox ID="Input" />
50
 * <com:TCaptchaValidator CaptchaControl="Captcha"
51
 *                        ControlToValidate="Input"
52
 *                        ErrorMessage="You are challenged!" />
53
 * </code>
54
 *
55
 * @author Qiang Xue <qiang.xue@gmail.com>
56
 * @version $Id: TCaptcha.php 2541 2008-10-21 15:05:13Z qiang.xue $
57
 * @package System.Web.UI.WebControls
58
 * @since 3.1.1
59
 */
60
class TCaptcha extends TImage
61
{
62
	const MIN_TOKEN_LENGTH=2;
63
	const MAX_TOKEN_LENGTH=40;
64
	private $_privateKey;
65
	private $_validated=false;
66
 
67
	/**
68
	 * @return integer the theme of the token image. Defaults to 0.
69
	 */
70
	public function getTokenImageTheme()
71
	{
72
		return $this->getViewState('TokenImageTheme',0);
73
	}
74
 
75
	/**
76
	 * Sets the theme of the token image.
77
	 * You may test each theme to find out the one you like the most.
78
	 * Below is the explanation of the theme value:
79
	 * It is treated as a 5-bit integer. Each bit toggles a specific feature of the image.
80
	 * Bit 0 (the least significant): whether the image is opaque (1) or transparent (0).
81
	 * Bit 1: whether we should add white noise to the image (1) or not (0).
82
	 * Bit 2: whether we should add a grid to  the image (1) or not (0).
83
	 * Bit 3: whether we should add some scribbles to the image (1) or not (0).
84
	 * Bit 4: whether the image background should be morphed (1) or not (0).
85
	 * Bit 5: whether the token text should cast a shadow (1) or not (0).
86
	 * @param integer the theme of the token image. It must be an integer between 0 and 63.
87
	 */
88
	public function setTokenImageTheme($value)
89
	{
90
		$value=TPropertyValue::ensureInteger($value);
91
		if($value>=0 && $value<=63)
92
			$this->setViewState('TokenImageTheme',$value,0);
93
		else
94
			throw new TConfigurationException('captcha_tokenimagetheme_invalid',0,63);
95
	}
96
 
97
	/**
98
	 * @return integer the font size used for displaying the token in an image. Defaults to 30.
99
	 */
100
	public function getTokenFontSize()
101
	{
102
		return $this->getViewState('TokenFontSize',30);
103
	}
104
 
105
	/**
106
	 * Sets the font size used for displaying the token in an image.
107
	 * This property affects the generated token image size.
108
	 * The image width is proportional to this font size.
109
	 * @param integer the font size used for displaying the token in an image. It must be an integer between 20 and 100.
110
	 */
111
	public function setTokenFontSize($value)
112
	{
113
		$value=TPropertyValue::ensureInteger($value);
114
		if($value>=20 && $value<=100)
115
			$this->setViewState('TokenFontSize',$value,30);
116
		else
117
			throw new TConfigurationException('captcha_tokenfontsize_invalid',20,100);
118
	}
119
 
120
	/**
121
	 * @return integer the minimum length of the token. Defaults to 4.
122
	 */
123
	public function getMinTokenLength()
124
	{
125
		return $this->getViewState('MinTokenLength',4);
126
	}
127
 
128
	/**
129
	 * @param integer the minimum length of the token. It must be between 2 and 40.
130
	 */
131
	public function setMinTokenLength($value)
132
	{
133
		$length=TPropertyValue::ensureInteger($value);
134
		if($length>=self::MIN_TOKEN_LENGTH && $length<=self::MAX_TOKEN_LENGTH)
135
			$this->setViewState('MinTokenLength',$length,4);
136
		else
137
			throw new TConfigurationException('captcha_mintokenlength_invalid',self::MIN_TOKEN_LENGTH,self::MAX_TOKEN_LENGTH);
138
	}
139
 
140
	/**
141
	 * @return integer the maximum length of the token. Defaults to 6.
142
	 */
143
	public function getMaxTokenLength()
144
	{
145
		return $this->getViewState('MaxTokenLength',6);
146
	}
147
 
148
	/**
149
	 * @param integer the maximum length of the token. It must be between 2 and 40.
150
	 */
151
	public function setMaxTokenLength($value)
152
	{
153
		$length=TPropertyValue::ensureInteger($value);
154
		if($length>=self::MIN_TOKEN_LENGTH && $length<=self::MAX_TOKEN_LENGTH)
155
			$this->setViewState('MaxTokenLength',$length,6);
156
		else
157
			throw new TConfigurationException('captcha_maxtokenlength_invalid',self::MIN_TOKEN_LENGTH,self::MAX_TOKEN_LENGTH);
158
	}
159
 
160
	/**
161
	 * @return boolean whether the token should be treated as case-sensitive. Defaults to true.
162
	 */
163
	public function getCaseSensitive()
164
	{
165
		return $this->getViewState('CaseSensitive',true);
166
	}
167
 
168
	/**
169
	 * @param boolean whether the token should be treated as case-sensitive. If false, only upper-case letters will appear in the token.
170
	 */
171
	public function setCaseSensitive($value)
172
	{
173
		$this->setViewState('CaseSensitive',TPropertyValue::ensureBoolean($value),true);
174
	}
175
 
176
	/**
177
	 * @return string the characters that may appear in the token. Defaults to '234578adefhijmnrtABDEFGHJLMNRT'.
178
	 */
179
	public function getTokenAlphabet()
180
	{
181
		return $this->getViewState('TokenAlphabet','234578adefhijmnrtABDEFGHJLMNRT');
182
	}
183
 
184
	/**
185
	 * @param string the characters that may appear in the token. At least 2 characters must be specified.
186
	 */
187
	public function setTokenAlphabet($value)
188
	{
189
		if(strlen($value)<2)
190
			throw new TConfigurationException('captcha_tokenalphabet_invalid');
191
		$this->setViewState('TokenAlphabet',$value,'234578adefhijmnrtABDEFGHJLMNRT');
192
	}
193
 
194
	/**
195
	 * @return integer the number of seconds that a generated token will remain valid. Defaults to 600 seconds (10 minutes).
196
	 */
197
	public function getTokenExpiry()
198
	{
199
		return $this->getViewState('TokenExpiry',600);
200
	}
201
 
202
	/**
203
	 * @param integer the number of seconds that a generated token will remain valid. A value smaller than 1 means the token will not expire.
204
	 */
205
	public function setTokenExpiry($value)
206
	{
207
		$this->setViewState('TokenExpiry',TPropertyValue::ensureInteger($value),600);
208
	}
209
 
210
	/**
211
	 * @return boolean whether the background of the token image should be variated during postbacks. Defaults to false.
212
	 */
213
	public function getChangingTokenBackground()
214
	{
215
		return $this->getViewState('ChangingTokenBackground',false);
216
	}
217
 
218
	/**
219
	 * @param boolean whether the background of the token image should be variated during postbacks.
220
	 */
221
	public function setChangingTokenBackground($value)
222
	{
223
		$this->setViewState('ChangingTokenBackground',TPropertyValue::ensureBoolean($value),false);
224
	}
225
 
226
	/**
227
	 * @return integer how many times a generated token can be tested. Defaults to 5.
228
	 */
229
	public function getTestLimit()
230
	{
231
		return $this->getViewState('TestLimit',5);
232
	}
233
 
234
	/**
235
	 * @param integer how many times a generated token can be tested. For unlimited tests, set it to 0.
236
	 */
237
	public function setTestLimit($value)
238
	{
239
		$this->setViewState('TestLimit',TPropertyValue::ensureInteger($value),5);
240
	}
241
 
242
	/**
243
	 * @return boolean whether the currently generated token has expired.
244
	 */
245
	public function getIsTokenExpired()
246
	{
247
		if(($expiry=$this->getTokenExpiry())>0 && ($start=$this->getViewState('TokenGenerated',0))>0)
248
			return $expiry+$start<time();
249
		else
250
			return false;
251
	}
252
 
253
	/**
254
	 * @return string the public key used for generating the token. A random one will be generated and returned if this is not set.
255
	 */
256
	public function getPublicKey()
257
	{
258
		if(($publicKey=$this->getViewState('PublicKey',''))==='')
259
		{
260
			$publicKey=$this->generateRandomKey();
261
			$this->setPublicKey($publicKey);
262
		}
263
		return $publicKey;
264
	}
265
 
266
	/**
267
	 * @param string the public key used for generating the token. A random one will be generated if this is not set.
268
	 */
269
	public function setPublicKey($value)
270
	{
271
		$this->setViewState('PublicKey',$value,'');
272
	}
273
 
274
	/**
275
	 * @return string the token that will be displayed
276
	 */
277
	public function getToken()
278
	{
279
		return $this->generateToken($this->getPublicKey(),$this->getPrivateKey(),$this->getTokenAlphabet(),$this->getTokenLength(),$this->getCaseSensitive());
280
	}
281
 
282
	/**
283
	 * @return integer the length of the token to be generated.
284
	 */
285
	protected function getTokenLength()
286
	{
287
		if(($tokenLength=$this->getViewState('TokenLength'))===null)
288
		{
289
			$minLength=$this->getMinTokenLength();
290
			$maxLength=$this->getMaxTokenLength();
291
			if($minLength>$maxLength)
292
				$tokenLength=rand($maxLength,$minLength);
293
			else if($minLength<$maxLength)
294
				$tokenLength=rand($minLength,$maxLength);
295
			else
296
				$tokenLength=$minLength;
297
			$this->setViewState('TokenLength',$tokenLength);
298
		}
299
		return $tokenLength;
300
	}
301
 
302
	/**
303
	 * @return string the private key used for generating the token. This is randomly generated and kept in a file for persistency.
304
	 */
305
	public function getPrivateKey()
306
	{
307
		if($this->_privateKey===null)
308
		{
309
			$fileName=$this->generatePrivateKeyFile();
310
			$content=file_get_contents($fileName);
311
			$matches=array();
312
			if(preg_match("/privateKey='(.*?)'/ms",$content,$matches)>0)
313
				$this->_privateKey=$matches[1];
314
			else
315
				throw new TConfigurationException('captcha_privatekey_unknown');
316
		}
317
		return $this->_privateKey;
318
	}
319
 
320
	/**
321
	 * Validates a user input with the token.
322
	 * @param string user input
323
	 * @return boolean if the user input is not the same as the token.
324
	 */
325
	public function validate($input)
326
	{
327
		$number=$this->getViewState('TestNumber',0);
328
		if(!$this->_validated)
329
		{
330
			$this->setViewState('TestNumber',++$number);
331
			$this->_validated=true;
332
		}
333
		if($this->getIsTokenExpired() || (($limit=$this->getTestLimit())>0 && $number>$limit))
334
		{
335
			$this->regenerateToken();
336
			return false;
337
		}
338
		return ($this->getToken()===($this->getCaseSensitive()?$input:strtoupper($input)));
339
	}
340
 
341
	/**
342
	 * Regenerates the token to be displayed.
343
	 * By default, a token, once generated, will remain the same during the following page postbacks.
344
	 * Calling this method will generate a new token.
345
	 */
346
	public function regenerateToken()
347
	{
348
		$this->clearViewState('TokenLength');
349
		$this->setPublicKey('');
350
		$this->clearViewState('TokenGenerated');
351
		$this->clearViewState('RandomSeed');
352
		$this->clearViewState('TestNumber',0);
353
	}
354
 
355
	/**
356
	 * Configures the image URL that shows the token.
357
	 * @param mixed event parameter
358
	 */
359
	public function onPreRender($param)
360
	{
361
		parent::onPreRender($param);
362
		if(!self::checkRequirements())
363
			throw new TConfigurationException('captcha_imagettftext_required');
364
		if(!$this->getViewState('TokenGenerated',0))
365
		{
366
			$manager=$this->getApplication()->getAssetManager();
367
			$manager->publishFilePath($this->getFontFile());
368
			$url=$manager->publishFilePath($this->getCaptchaScriptFile());
369
			$url.='?options='.urlencode($this->getTokenImageOptions());
370
			$this->setImageUrl($url);
371
 
372
			$this->setViewState('TokenGenerated',time());
373
		}
374
	}
375
 
376
	/**
377
	 * @return string the options to be passed to the token image generator
378
	 */
379
	protected function getTokenImageOptions()
380
	{
381
		$privateKey=$this->getPrivateKey();  // call this method to ensure private key is generated
382
		$token=$this->getToken();
383
		$options=array();
384
		$options['publicKey']=$this->getPublicKey();
385
		$options['tokenLength']=strlen($token);
386
		$options['caseSensitive']=$this->getCaseSensitive();
387
		$options['alphabet']=$this->getTokenAlphabet();
388
		$options['fontSize']=$this->getTokenFontSize();
389
		$options['theme']=$this->getTokenImageTheme();
390
		if(($randomSeed=$this->getViewState('RandomSeed',0))===0)
391
		{
392
			$randomSeed=(int)(microtime()*1000000);
393
			$this->setViewState('RandomSeed',$randomSeed);
394
		}
395
		$options['randomSeed']=$this->getChangingTokenBackground()?0:$randomSeed;
396
		$str=serialize($options);
397
		return base64_encode(md5($privateKey.$str).$str);
398
	}
399
 
400
	/**
401
	 * @return string the file path of the PHP script generating the token image
402
	 */
403
	protected function getCaptchaScriptFile()
404
	{
405
		return dirname(__FILE__).DIRECTORY_SEPARATOR.'assets'.DIRECTORY_SEPARATOR.'captcha.php';
406
	}
407
 
408
	protected function getFontFile()
409
	{
410
		return dirname(__FILE__).DIRECTORY_SEPARATOR.'assets'.DIRECTORY_SEPARATOR.'verase.ttf';
411
	}
412
 
413
	/**
414
	 * Generates a file with a randomly generated private key.
415
	 * @return string the path of the file keeping the private key
416
	 */
417
	protected function generatePrivateKeyFile()
418
	{
419
		$captchaScript=$this->getCaptchaScriptFile();
420
		$path=dirname($this->getApplication()->getAssetManager()->getPublishedPath($captchaScript));
421
		$fileName=$path.DIRECTORY_SEPARATOR.'captcha_key.php';
422
		if(!is_file($fileName))
423
		{
424
			@mkdir($path);
425
			$key=$this->generateRandomKey();
426
			$content="<?php
427
\$privateKey='$key';
428
?>";
429
			file_put_contents($fileName,$content);
430
		}
431
		return $fileName;
432
	}
433
 
434
	/**
435
	 * @return string a randomly generated key
436
	 */
437
	protected function generateRandomKey()
438
	{
439
		return md5(rand().rand().rand().rand());
440
	}
441
 
442
	/**
443
	 * Generates the token.
444
	 * @param string public key
445
	 * @param string private key
446
	 * @param integer the length of the token
447
	 * @param boolean whether the token is case sensitive
448
	 * @return string the token generated.
449
	 */
450
	protected function generateToken($publicKey,$privateKey,$alphabet,$tokenLength,$caseSensitive)
451
	{
452
		$token=substr($this->hash2string(md5($publicKey.$privateKey),$alphabet).$this->hash2string(md5($privateKey.$publicKey),$alphabet),0,$tokenLength);
453
		return $caseSensitive?$token:strtoupper($token);
454
	}
455
 
456
	/**
457
	 * Converts a hash string into a string with characters consisting of alphanumeric characters.
458
	 * @param string the hexadecimal representation of the hash string
459
	 * @param string the alphabet used to represent the converted string. If empty, it means '234578adefhijmnrtwyABDEFGHIJLMNQRTWY', which excludes those confusing characters.
460
	 * @return string the converted string
461
	 */
462
	protected function hash2string($hex,$alphabet='')
463
	{
464
		if(strlen($alphabet)<2)
465
			$alphabet='234578adefhijmnrtABDEFGHJLMNQRT';
466
		$hexLength=strlen($hex);
467
		$base=strlen($alphabet);
468
		$result='';
469
		for($i=0;$i<$hexLength;$i+=6)
470
		{
471
			$number=hexdec(substr($hex,$i,6));
472
			while($number)
473
			{
474
				$result.=$alphabet[$number%$base];
475
				$number=floor($number/$base);
476
			}
477
		}
478
		return $result;
479
	}
480
 
481
	/**
482
	 * Checks the requirements needed for generating CAPTCHA images.
483
	 * TCaptach requires GD2 with TrueType font support and PNG image support.
484
	 * @return boolean whether the requirements are satisfied.
485
	 */
486
	public static function checkRequirements()
487
	{
488
		return extension_loaded('gd') && function_exists('imagettftext') && function_exists('imagepng');
489
	}
490
}
491