/lms_debug/protected/extensions/captchaExtended/CaptchaExtendedAction.php
PHP | 573 lines | 365 code | 45 blank | 163 comment | 47 complexity | efdcb834f8c658c1a739dd4489847361 MD5 | raw file
Possible License(s): LGPL-2.1, LGPL-3.0, LGPL-2.0
- <?php
- /**
- * Yii extended captcha supporting more complex formulas and masking techniques.
- *
- * Features:
- * - supported modes: logical, words, mathverbal, math, default
- * - supported extended characters latin1, latin2 (utf-8) including middle- east- european and cyrillyc characters
- * - implements masking elements: dots density, through lines, fillSections, font color varying
- *
- * Installation:
- * =============
- *
- * 1) unzip CaptchaExtended.zip files into ../protected/extensions/captchaExtended/*.*
- *
- * 2) Register class paths to [CaptchaExtendedAction] and [CaptchaExtendedValidator], e.g. in components/controller.php:
- *
- * public function init(){
- * // import class paths for captcha extended
- * Yii::$classMap = array_merge( Yii::$classMap, array(
- * 'CaptchaExtendedAction' => Yii::getPathOfAlias('ext.captchaExtended').DIRECTORY_SEPARATOR.'CaptchaExtendedAction.php',
- * 'CaptchaExtendedValidator' => Yii::getPathOfAlias('ext.captchaExtended').DIRECTORY_SEPARATOR.'CaptchaExtendedValidator.php'
- * ));
- * }
- *
- * 3) Define action in controller, e.g. SiteController:
- *
- * public function actions(){
- * return array(
- * 'captcha'=>array(
- * 'class'=>'CaptchaExtendedAction',
- * ),
- * );
- * }
- *
- * 4) Define client validation in model::rules():
- *
- * public function rules(){
- * return array(
- * array('verifyCode', 'CaptchaExtendedValidator', 'allowEmpty'=>!CCaptcha::checkRequirements()),
- * );
- * }
- *
- * 5) If needed, collect localized strings via CLI command "yiic message messages/config.php" and translate captcha related strings.
- *
- * 6) If needed, you can tune captcha modes and visibility options:
- * - In "words" mode, you can place your own file [words.txt] or [words.yourlanguage.txt]
- * - If needed, set the dots density [0-100], the number of through lines [0-] or fillSections [0-], font and background colors
- *
- * 7) Test & enjoy!
- */
- class CaptchaExtendedAction extends CCaptchaAction{
-
- const
- MODE_MATH = 'math',
- MODE_MATHVERBAL = 'mathverbal',
- MODE_DEFAULT = 'default',
- MODE_LOGICAL = 'logical',
- MODE_WORDS = 'words';
-
- /**
- * @var integer padding around the text. Defaults to 2.
- */
- public $offset = 2;
-
- /**
- * Captcha mode, supported values are [logical, words, mathverbal, math, default].
- * Default value is [default], which uses native frameworks implementation.
- * Captcha mode examples:
- * - logical e.g. min(5, one, 9) or max (two, three, 3)
- * - words e.g. [localized random words] (supports translated strings in UTF-8 including latin2 and cyrillic)
- * - mathverbal e.g. How much is 12 plus 8 ?
- * - math e.g. 93 - 3 =
- * - default e.g. random latin1 characters
- */
- public $mode = self::MODE_DEFAULT;
-
- /**
- * Path to the file to be used for generating random words in "words" mode
- */
- public $fileWords;
-
- /**
- * Dots density around characters 0 - 100 [%], defaults 5.
- */
- public $density = 5; // dots density 0 - 100%
-
- /**
- * The number of lines drawn through the generated captcha picture, default 3.
- */
- public $lines = 3;
-
- /**
- * The number of sections to be filled with random flood color, default 10.
- */
- public $fillSections = 10;
-
- /**
- * Run action
- */
- public function run(){
-
- if(!extension_loaded('mbstring')){
- throw new CHttpException(500, Yii::t('main','Missing extension "{ext}"', array('{ext}' => 'mbstring')));
- }
-
- // set font file with all extended UTF-8 characters
- // Font Duality supplied with the framework does not support UTF-8, only ISO-8859-1 - missing accented characters like ščťžôäě...
- $this->fontFile = dirname(__FILE__).'/fonts/nimbus.ttf';
-
- // set captcha mode
- $this->mode = strtolower($this->mode);
-
- // set image size
- switch ($this->mode){
- case self::MODE_LOGICAL:
- case self::MODE_WORDS:
- $this->width = 300;
- $this->height = 50;
- break;
- case self::MODE_MATHVERBAL:
- $this->width = 400;
- $this->height = 50;
- break;
- case self::MODE_MATH:
- case self::MODE_DEFAULT:
- default:
- $this->width = 120;
- $this->height = 50;
- }
-
- if($this->mode == self::MODE_DEFAULT){
- // default framework implementation
- parent::run();
- }else{
- // we hash result value rather than the displayed code
- if(isset($_GET[self::REFRESH_GET_VAR])){
- $result=$this->getVerifyResult(true);
- echo CJSON::encode(array(
- 'hash1'=>$this->generateValidationHash($result),
- 'hash2'=>$this->generateValidationHash(mb_convert_case($result, MB_CASE_LOWER, 'utf-8')),
- 'url'=>$this->getController()->createUrl($this->getId(),array('v' => uniqid())),
- ));
- }else{
- $this->renderImage($this->getVerifyCode());
- }
- }
- Yii::app()->end();
- }
-
- /**
- * Generates a hash code that can be used for client side validation.
- * @param string $code Displayed captcha code in generated image
- * @return int Sum of ASCII codes for all $code characters. Used for client-side validation
- */
- public function generateValidationHash($code){
- switch($this->mode){
- case CaptchaExtendedAction::MODE_DEFAULT:
- return parent::generateValidationHash($code);
- break;
- default:
- /**
- * We must encode result stored in session
- * @var CHttpSession
- */
- $session = Yii::app()->session;
- $key = $this->getSessionKey();
- if($this->mode != CaptchaExtendedAction::MODE_WORDS){
- // only in mode WORDS we must calculate ORD values of plain string
- // but we cannot use framework implementation since we need multibyte support
- $code = $session[$key.'result'];
- }
- $code = preg_replace('/\s/', '', $code);
- $code = urlencode($code);
- for($h=0,$i=mb_strlen($code)-1;$i>=0;--$i){
- $chr=mb_substr($code, $i, 1, 'utf-8');
- if(trim($chr)!=''){
- $h+=ord($chr);
- }
- }
- return $h;
- }
- }
-
- /**
- * Generates a new verification code.
- * @return string the generated verification code
- */
- protected function generateVerifyCode(){
- switch (strtolower($this->mode)){
- case self::MODE_MATH:
- return $this->getCodeMath();
- case self::MODE_MATHVERBAL:
- return $this->getCodeMathVerbal();
- case self::MODE_LOGICAL:
- return $this->getCodeLogical();
- case self::MODE_WORDS:
- return $this->getCodeWords();
- case self::MODE_DEFAULT:
- default:
- $code = parent::generateVerifyCode();
- return array('code' => $code, 'result' => $code);
- }
- }
-
- /**
- * Return code for random words from text file.
- * First we'll try to load file for current language, like [words.de.txt]
- * If not found, we will try to load generic file like [words.txt]
- */
- protected function getCodeWords(){
- if($this->fileWords === null){
- // guess words file upon current language like [words.de.txt], [words.ru.txt]
- $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.'.Yii::app()->language.'.txt';
- if(!is_file($this->fileWords)){
- // take fallback file without language specification
- $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.txt';
- }
- }
- if(!file_exists($this->fileWords)){
- throw new CHttpException(500, Yii::t('main','File not found in "{path}"', array('{path}' => $this->fileWords)));
- }
- $words = file_get_contents($this->fileWords);
- $words = explode(' ', $words);
- $found = array();
- for($i=0;$i<count($words);++$i){
- // select random word
- $w = array_splice($words, mt_rand(0,count($words)),1);
- if(empty($w[0])){
- continue;
- }
- // purify word
- $w = $this->purifyWord($w[0]);
- if(strlen($w)>3){
- // accept only word with at least 3 characters
- $found[] = $w;
- if(mb_strlen(implode('', $found), 'utf-8') > 10){
- // words must have at least 10 characters together
- break;
- }
- }
- }
- $code = implode(' ', $found); // without whitespaces
- $result = preg_replace('/\s/', '', $code);
- return array('code' => $code, 'result' => $result);
- }
-
- /**
- * Return captcha word without dirty characters like *,/,{,},.. Retain diacritics if unicode supported.
- * @param string $w The word to be purified
- */
- protected function purifyWord($w){
- if(@preg_match('/\pL/u', 'a')){
- // unicode supported, we remove everything except for accented characters
- $w = preg_replace('/[^\p{L}]/u', '', (string) $w);
- }else{
- // Unicode is not supported. Cannot validate utf-8 characters, we keep only latin1
- $w = preg_replace('/[^a-zA-Z0-9]/','',$w);
- }
- return $w;
- }
-
- /**
- * Return code for math mode like 9+1= or 95-5=
- */
- protected function getCodeMath(){
- $n2 = mt_rand(1,9);
- if(mt_rand(1,100) > 50){
- $n1 = mt_rand(1,9)*10+$n2;
- $code = $n1.'-'.$n2.'=';
- $r = $n1-$n2;
- }else{
- $n1 = mt_rand(1,10)*10-$n2;
- $code = $n1.'+'.$n2.'=';
- $r = $n1+$n2;
- }
- return array('code' => $code, 'result' => $r);
- }
-
- /**
- * Return numbers 0..9 translated into word
- */
- protected static function getNumbers(){
- return array(
- '0' => Yii::t('main','zero'),
- '1' => Yii::t('main','one'),
- '2' => Yii::t('main','two'),
- '3' => Yii::t('main','three'),
- '4' => Yii::t('main','four'),
- '5' => Yii::t('main','five'),
- '6' => Yii::t('main','six'),
- '7' => Yii::t('main','seven'),
- '8' => Yii::t('main','eight'),
- '9' => Yii::t('main','nine'),
- );
- }
-
- /**
- * Return verbal representation for supplied number, like 1 => one
- * @param int $n The number to be translated
- */
- protected static function getNumber($n){
- static $nums;
- if(empty($nums)){
- $nums = self::getNumbers();
- }
- return array_key_exists($n, $nums) ? $nums[$n] : '';
- }
-
- /**
- * Return code for logical formula like min(one,7,four)
- */
- protected function getCodeLogical(){
- $t = mt_rand(2,4);
- $a = array();
- for($i=0;$i<$t;++$i){
- // we dont use zero
- $a[] = mt_rand(1,9);
- }
- if(mt_rand(0,1)){
- $r = max($a);
- $code = array();
- for($i=0;$i<count($a);++$i){
- $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
- }
- $code = Yii::t('main','max').' ( '.implode(', ',$code).' )';
- }else{
- $r = min($a);
- $code = array();
- for($i=0;$i<count($a);++$i){
- $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
- }
- $code = Yii::t('main','min').' ( '.implode(', ',$code).' )';
- }
- return array('code' => $code, 'result' => $r);
- }
-
- /**
- * Return code for verbal math mode like "How much is 1 plus 1 ?"
- */
- protected function getCodeMathVerbal(){
- $n2 = mt_rand(1,9);
- if(mt_rand(1,100) > 50){
- switch(mt_rand(0,2)){
- case 0:
- $op = Yii::t('main','minus');
- break;
- case 1:
- $op = Yii::t('main','deducted by');
- break;
- case 2:
- $op = '-';
- break;
- }
- $n1 = mt_rand(1,9)*10+$n2;
- $code = $n1.' '.$op.' '. ( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
- $r = $n1-$n2;
- }else{
- switch(mt_rand(0,2)){
- case 0:
- $op = Yii::t('main','plus');
- break;
- case 1:
- $op = Yii::t('main','and');
- break;
- case 2:
- $op = '+';
- break;
- }
- $n1 = mt_rand(1,10)*10-$n2;
- $code = $n1.' '.$op.' '.( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
- $r = $n1+$n2;
- }
- switch (mt_rand(0,2)){
- case 0:
- $question = Yii::t('main','How much is');
- break;
- case 1:
- $question = Yii::t('main','What\'s result for');
- break;
- case 2:
- $question = Yii::t('main','Give result for');
- break;
- }
-
- switch (mt_rand(0,2)){
- case 0:
- $equal = '?';
- break;
- case 1:
- $equal = '=';
- break;
- case 2:
- $equal = str_repeat('.', mt_rand(2,5));
- break;
- }
-
- $code = $question.' '.$code.' '.$equal;
- return array('code' => $code, 'result' => $r);
- }
-
-
- /**
- * Validates the input to see if it matches the generated code.
- * @param string $input user input
- * @param boolean $caseSensitive whether the comparison should be case-sensitive
- * @return whether the input is valid
- */
- public function validate($input,$caseSensitive){
- // open session, if necessary generate new code
- $this->getVerifyCode();
- // read result
- $session = Yii::app()->session;
- $name = $this->getSessionKey();
- $result = $session[$name . 'result'];
- // input always taken without whitespaces
- $input = preg_replace('/\s/','',$input);
- $valid = $caseSensitive ? strcmp($input, $result)===0 : strcasecmp($input, $result)===0;
- // increase attempts counter
- $name = $this->getSessionKey() . 'count';
- $session[$name] = $session[$name] + 1;
- if($valid || $session[$name] > $this->testLimit && $this->testLimit > 0){
- // generate new code also each time correctly entered
- $this->getVerifyCode(true);
- }
- return $valid;
- }
-
- /**
- * Gets the verification code.
- * @param boolean $regenerate whether the verification code should be regenerated.
- * @return string the verification code.
- */
- public function getVerifyCode($regenerate=false){
- if($this->fixedVerifyCode !== null){
- return $this->fixedVerifyCode;
- }
- $session = Yii::app()->session;
- $session->open();
- $name = $this->getSessionKey();
- if(empty($session[$name]) || $regenerate){
- $code = $this->generateVerifyCode();
- $session[$name] = $code['code'];
- $session[$name . 'result'] = $code['result'];
- $session[$name . 'count'] = 1;
- }
- return $session[$name];
- }
-
- /**
- * Return verification result expected by user
- * @param bool $regenerate
- */
- public function getVerifyResult($regenerate=false){
- if($this->fixedVerifyCode !== null){
- return $this->fixedVerifyCode;
- }
- $session = Yii::app()->session;
- $session->open();
- $name = $this->getSessionKey();
- if(empty($session[$name . 'result']) || $regenerate){
- $code = $this->generateVerifyCode();
- $session[$name] = $code['code'];
- $session[$name . 'result'] = $code['result'];
- $session[$name . 'count'] = 1;
- }
- return $session[$name . 'result'];
- }
-
- /**
- * Renders the CAPTCHA image based on the code.
- * @param string $code the verification code
- * @return string image content
- */
- protected function renderImage($code){
-
- $image = imagecreatetruecolor($this->width,$this->height);
-
- $backColor = imagecolorallocate($image,
- (int)($this->backColor % 0x1000000 / 0x10000),
- (int)($this->backColor % 0x10000 / 0x100),
- $this->backColor % 0x100);
- imagefilledrectangle($image,0,0,$this->width,$this->height,$backColor);
- imagecolordeallocate($image,$backColor);
-
- if($this->transparent){
- imagecolortransparent($image,$backColor);
- }
-
- if($this->fontFile === null){
- $this->fontFile = dirname(__FILE__) . '/Duality.ttf';
- }
-
- $length = strlen($code);
- $box = imagettfbbox(25,0,$this->fontFile,$code);
- $w = $box[4] - $box[0] + $this->offset * ($length - 1);
- $h = $box[1] - $box[5];
- $scale = min(($this->width - $this->padding * 2) / $w,($this->height - $this->padding * 2) / $h);
- $x = 10;
- $y = round($this->height * 27 / 40);
-
- $r = (int)($this->foreColor % 0x1000000 / 0x10000);
- $g = (int)($this->foreColor % 0x10000 / 0x100);
- $b = $this->foreColor % 0x100;
- $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
-
- for($i = 0; $i < $length; ++$i){
- $fontSize = (int)(rand(26,32) * $scale * 0.8);
- $angle = rand(-10,10);
- $letter = $code[$i];
-
- // UTF-8 characters above > 127 are stored in two bytes
- if(ord($letter)>127){
- ++$i;
- $letter .= $code[$i];
- }
-
- // randomize font color
- if(mt_rand(0,10)>7){
- $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
- }
-
- $box = imagettftext($image,$fontSize,$angle,$x,$y,$foreColor,$this->fontFile,$letter);
- $x = $box[2] + $this->offset;
- }
-
- // add density dots
- $this->density = intval($this->density);
- if($this->density > 0){
- $length = intval($this->width*$this->height/100*$this->density);
- $c = imagecolorallocate($image, mt_rand(0,255), mt_rand(0,255), mt_rand(0,255));
- for($i=0;$i<$length;++$i){
- $x = mt_rand(0,$this->width);
- $y = mt_rand(0,$this->height);
- imagesetpixel($image, $x, $y, $c);
- }
- }
-
- // add lines
- $this->lines = intval($this->lines);
- if($this->lines > 0){
- for($i=0; $i<$this->lines; ++$i){
- imagesetthickness($image, mt_rand(1,2));
- // gray lines only to save human eyes:-)
- $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
- $x = mt_rand(0, $this->width);
- $y = mt_rand(0, $this->width);
- imageline($image, $x, 0, $y, $this->height, $c);
- }
- }
-
- // filled flood section
- $this->fillSections = intval($this->fillSections);
- if($this->fillSections > 0){
- for($i = 0; $i < $this->fillSections; ++$i){
- $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
- $x = mt_rand(0, $this->width);
- $y = mt_rand(0, $this->width);
- imagefill($image, $x, $y, $c);
- }
- }
-
- imagecolordeallocate($image,$foreColor);
-
- header('Pragma: public');
- header('Expires: 0');
- header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
- header('Content-Transfer-Encoding: binary');
- header("Content-type: image/png");
- imagepng($image);
- imagedestroy($image);
- }
-
- }