PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/protected/extensions/captchaExtended/CaptchaExtendedAction.php

https://bitbucket.org/trujka/msleague
PHP | 549 lines | 348 code | 45 blank | 156 comment | 43 complexity | 1f98e747838d862cb08207eecd75f3b2 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * Yii extended captcha supporting more complex formulas and masking techniques.
  4. *
  5. * Features:
  6. * - supported modes: logical, words, mathverbal, math, default
  7. * - supported extended characters latin1, latin2 (utf-8) including middle- east- european and cyrillyc characters
  8. * - implements masking elements: dots density, through lines, fillSections, font color varying
  9. *
  10. * Installation:
  11. * =============
  12. *
  13. * 1) unzip CaptchaExtended.zip files into ../protected/extensions/captchaExtended/*.*
  14. *
  15. * 2) Register class paths to [CaptchaExtendedAction] and [CaptchaExtendedValidator], e.g. in components/controller.php:
  16. *
  17. * public function init(){
  18. * // import class paths for captcha extended
  19. * Yii::$classMap = array_merge( Yii::$classMap, array(
  20. * 'CaptchaExtendedAction' => Yii::getPathOfAlias('ext.captchaExtended').DIRECTORY_SEPARATOR.'CaptchaExtendedAction.php',
  21. * 'CaptchaExtendedValidator' => Yii::getPathOfAlias('ext.captchaExtended').DIRECTORY_SEPARATOR.'CaptchaExtendedValidator.php'
  22. * ));
  23. * }
  24. *
  25. * 3) Define action in controller, e.g. SiteController:
  26. *
  27. * public function actions(){
  28. * return array(
  29. * 'captcha'=>array(
  30. * 'class'=>'CaptchaExtendedAction',
  31. * ),
  32. * );
  33. * }
  34. *
  35. * 4) Define client validation in model::rules():
  36. *
  37. * public function rules(){
  38. * return array(
  39. * array('verifyCode', 'CaptchaExtendedValidator', 'allowEmpty'=>!CCaptcha::checkRequirements()),
  40. * );
  41. * }
  42. *
  43. * 5) If needed, collect localized strings via CLI command "yiic message messages/config.php" and translate captcha related strings.
  44. *
  45. * 6) If needed, you can tune captcha modes and visibility options:
  46. * - In "words" mode, you can place your own file [words.txt] or [words.yourlanguage.txt]
  47. * - If needed, set the dots density [0-100], the number of through lines [0-] or fillSections [0-], font and background colors
  48. *
  49. * 7) Test & enjoy!
  50. */
  51. class CaptchaExtendedAction extends CCaptchaAction{
  52. const
  53. MODE_MATH = 'math',
  54. MODE_MATHVERBAL = 'mathverbal',
  55. MODE_DEFAULT = 'default',
  56. MODE_LOGICAL = 'logical',
  57. MODE_WORDS = 'words';
  58. /**
  59. * @var integer padding around the text. Defaults to 2.
  60. */
  61. public $offset = 2;
  62. /**
  63. * Captcha mode, supported values are [logical, words, mathverbal, math, default].
  64. * Default value is [default], which uses native frameworks implementation.
  65. * Captcha mode examples:
  66. * - logical e.g. min(5, one, 9) or max (two, three, 3)
  67. * - words e.g. [localized random words] (supports translated strings in UTF-8 including latin2 and cyrillic)
  68. * - mathverbal e.g. How much is 12 plus 8 ?
  69. * - math e.g. 93 - 3 =
  70. * - default e.g. random latin1 characters
  71. */
  72. public $mode = self::MODE_MATH;
  73. /**
  74. * Path to the file to be used for generating random words in "words" mode
  75. */
  76. public $fileWords;
  77. /**
  78. * Dots density around characters 0 - 100 [%], defaults 5.
  79. */
  80. public $density = 5; // dots density 0 - 100%
  81. /**
  82. * The number of lines drawn through the generated captcha picture, default 3.
  83. */
  84. public $lines = 3;
  85. /**
  86. * The number of sections to be filled with random flood color, default 10.
  87. */
  88. public $fillSections = 10;
  89. /**
  90. * Run action
  91. */
  92. public function run(){
  93. if(!extension_loaded('mbstring')){
  94. throw new CHttpException(500, Yii::t('main','Missing extension "{ext}"', array('{ext}' => 'mbstring')));
  95. }
  96. // set font file with all extended UTF-8 characters
  97. // Duality supplied with the framework does not support some extended characters like ščťžôäě...
  98. $this->fontFile = dirname(__FILE__).'/fonts/nimbus.ttf';
  99. // set captcha mode
  100. $this->mode = strtolower($this->mode);
  101. // set image size
  102. switch ($this->mode){
  103. case self::MODE_LOGICAL:
  104. case self::MODE_WORDS:
  105. $this->width = 300;
  106. $this->height = 50;
  107. break;
  108. case self::MODE_MATHVERBAL:
  109. $this->width = 400;
  110. $this->height = 50;
  111. break;
  112. case self::MODE_MATH:
  113. case self::MODE_DEFAULT:
  114. default:
  115. $this->width = 120;
  116. $this->height = 50;
  117. }
  118. if($this->mode == self::MODE_DEFAULT){
  119. // default framework implementation
  120. parent::run();
  121. }else{
  122. // we hash result value rather than the displayed code
  123. if(isset($_GET[self::REFRESH_GET_VAR])){
  124. $result=$this->getVerifyResult(true);
  125. echo CJSON::encode(array(
  126. 'hash1'=>$this->generateValidationHash($result),
  127. 'hash2'=>$this->generateValidationHashCI($result),
  128. 'url'=>$this->getController()->createUrl($this->getId(),array('v' => uniqid())),
  129. ));
  130. }else{
  131. $this->renderImage($this->getVerifyCode());
  132. }
  133. }
  134. Yii::app()->end();
  135. }
  136. /**
  137. * Return hash for case insensitive result (converted to lowercase)
  138. * @param string $result
  139. */
  140. protected function generateValidationHashCI($result){
  141. $result = preg_replace('/\s/', '', $result);
  142. $result = mb_convert_case($result, MB_CASE_LOWER, 'utf-8');
  143. $result = urlencode($result);
  144. return $this->generateValidationHash($result);
  145. }
  146. /**
  147. * Generates a new verification code.
  148. * @return string the generated verification code
  149. */
  150. protected function generateVerifyCode(){
  151. switch (strtolower($this->mode)){
  152. case self::MODE_MATH:
  153. return $this->getCodeMath();
  154. case self::MODE_MATHVERBAL:
  155. return $this->getCodeMathVerbal();
  156. case self::MODE_LOGICAL:
  157. return $this->getCodeLogical();
  158. case self::MODE_WORDS:
  159. return $this->getCodeWords();
  160. case self::MODE_DEFAULT:
  161. default:
  162. $code = parent::generateVerifyCode();
  163. return array('code' => $code, 'result' => $code);
  164. }
  165. }
  166. /**
  167. * Return code for random words from text file.
  168. * First we'll try to load file for current language, like [words.de.txt]
  169. * If not found, we will try to load generic file like [words.txt]
  170. */
  171. protected function getCodeWords(){
  172. if($this->fileWords === null){
  173. // guess words file upon current language like [words.de.txt], [words.ru.txt]
  174. $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.'.Yii::app()->language.'.txt';
  175. if(!is_file($this->fileWords)){
  176. // take fallback file without language specification
  177. $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.txt';
  178. }
  179. }
  180. if(!file_exists($this->fileWords)){
  181. throw new CHttpException(500, Yii::t('main','FIle not found in "{path}"', array('{path}' => $this->fileWords)));
  182. }
  183. $words = file_get_contents($this->fileWords);
  184. $words = explode(' ', $words);
  185. $found = array();
  186. for($i=0;$i<count($words);++$i){
  187. // select random word
  188. $w = array_splice($words, mt_rand(0,count($words)),1);
  189. if(!isset($w[0])){
  190. continue;
  191. }
  192. // purify word
  193. $w = $this->purifyWord($w[0]);
  194. if(strlen($w)>3){
  195. // accept only word with at least 3 characters
  196. $found[] = $w;
  197. if(strlen(implode('', $found))>10){
  198. // words must have at least 10 characters together
  199. break;
  200. }
  201. }
  202. }
  203. $code = implode('', $found); // without whitespaces
  204. return array('code' => $code, 'result' => $code);
  205. }
  206. /**
  207. * Return captcha word without dirty characters like *,/,{,},.. Retain diacritics if unicode supported.
  208. * @param string $w The word to be purified
  209. */
  210. protected function purifyWord($w){
  211. if(@preg_match('/\pL/u', 'a')){
  212. // unicode supported, we remove everything except for accented characters
  213. $w = preg_replace('/[^\p{L}]/u', '', (string) $w);
  214. }else{
  215. // Unicode is not supported. Cannot validate utf-8 characters, we keep only latin1
  216. $w = preg_replace('/[^a-zA-Z0-9]/','',$w);
  217. }
  218. return $w;
  219. }
  220. /**
  221. * Return code for math mode like 9+1= or 95-5=
  222. */
  223. protected function getCodeMath(){
  224. $n2 = mt_rand(1,9);
  225. if(mt_rand(1,100) > 50){
  226. $n1 = mt_rand(1,9)*10+$n2;
  227. $code = $n1.'-'.$n2.'=';
  228. $r = $n1-$n2;
  229. }else{
  230. $n1 = mt_rand(1,10)*10-$n2;
  231. $code = $n1.'+'.$n2.'=';
  232. $r = $n1+$n2;
  233. }
  234. return array('code' => $code, 'result' => $r);
  235. }
  236. /**
  237. * Return numbers 0..9 translated into word
  238. */
  239. protected static function getNumbers(){
  240. return array(
  241. '0' => Yii::t('main','zero'),
  242. '1' => Yii::t('main','one'),
  243. '2' => Yii::t('main','two'),
  244. '3' => Yii::t('main','three'),
  245. '4' => Yii::t('main','four'),
  246. '5' => Yii::t('main','five'),
  247. '6' => Yii::t('main','six'),
  248. '7' => Yii::t('main','seven'),
  249. '8' => Yii::t('main','eight'),
  250. '9' => Yii::t('main','nine'),
  251. );
  252. }
  253. /**
  254. * Return verbal representation for supplied number, like 1 => one
  255. * @param int $n The number to be translated
  256. */
  257. protected static function getNumber($n){
  258. static $nums;
  259. if(empty($nums)){
  260. $nums = self::getNumbers();
  261. }
  262. return array_key_exists($n, $nums) ? $nums[$n] : '';
  263. }
  264. /**
  265. * Return code for logical formula like min(one,7,four)
  266. */
  267. protected function getCodeLogical(){
  268. $t = mt_rand(2,4);
  269. $a = array();
  270. for($i=0;$i<$t;++$i){
  271. // we dont use zero
  272. $a[] = mt_rand(1,9);
  273. }
  274. if(mt_rand(0,1)){
  275. $r = max($a);
  276. $code = array();
  277. for($i=0;$i<count($a);++$i){
  278. $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
  279. }
  280. $code = Yii::t('main','max').' ( '.implode(', ',$code).' )';
  281. }else{
  282. $r = min($a);
  283. $code = array();
  284. for($i=0;$i<count($a);++$i){
  285. $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
  286. }
  287. $code = Yii::t('main','min').' ( '.implode(', ',$code).' )';
  288. }
  289. return array('code' => $code, 'result' => $r);
  290. }
  291. /**
  292. * Return code for verbal math mode like "How much is 1 plus 1 ?"
  293. */
  294. protected function getCodeMathVerbal(){
  295. $n2 = mt_rand(1,9);
  296. if(mt_rand(1,100) > 50){
  297. switch(mt_rand(0,2)){
  298. case 0:
  299. $op = Yii::t('main','minus');
  300. break;
  301. case 1:
  302. $op = Yii::t('main','deducted by');
  303. break;
  304. case 2:
  305. $op = '-';
  306. break;
  307. }
  308. $n1 = mt_rand(1,9)*10+$n2;
  309. $code = $n1.' '.$op.' '. ( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
  310. $r = $n1-$n2;
  311. }else{
  312. switch(mt_rand(0,2)){
  313. case 0:
  314. $op = Yii::t('main','plus');
  315. break;
  316. case 1:
  317. $op = Yii::t('main','and');
  318. break;
  319. case 2:
  320. $op = '+';
  321. break;
  322. }
  323. $n1 = mt_rand(1,10)*10-$n2;
  324. $code = $n1.' '.$op.' '.( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
  325. $r = $n1+$n2;
  326. }
  327. switch (mt_rand(0,2)){
  328. case 0:
  329. $question = Yii::t('main','How much is');
  330. break;
  331. case 1:
  332. $question = Yii::t('main','What\'s result for');
  333. break;
  334. case 2:
  335. $question = Yii::t('main','Give result for');
  336. break;
  337. }
  338. switch (mt_rand(0,2)){
  339. case 0:
  340. $equal = '?';
  341. break;
  342. case 1:
  343. $equal = '=';
  344. break;
  345. case 2:
  346. $equal = str_repeat('.', mt_rand(2,5));
  347. break;
  348. }
  349. $code = $question.' '.$code.' '.$equal;
  350. return array('code' => $code, 'result' => $r);
  351. }
  352. /**
  353. * Validates the input to see if it matches the generated code.
  354. * @param string $input user input
  355. * @param boolean $caseSensitive whether the comparison should be case-sensitive
  356. * @return whether the input is valid
  357. */
  358. public function validate($input,$caseSensitive){
  359. // open session, if necessary generate new code
  360. $this->getVerifyCode();
  361. // read result
  362. $session = Yii::app()->session;
  363. $name = $this->getSessionKey();
  364. $result = $session[$name . 'result'];
  365. // input always taken without whitespaces
  366. $input = preg_replace('/\s/','',$input);
  367. $valid = $caseSensitive ? strcmp($input, $result)===0 : strcasecmp($input, $result)===0;
  368. // increase attempts counter
  369. $name = $this->getSessionKey() . 'count';
  370. $session[$name] = $session[$name] + 1;
  371. if($valid || $session[$name] > $this->testLimit && $this->testLimit > 0){
  372. // generate new code also each time correctly entered
  373. $this->getVerifyCode(true);
  374. }
  375. return $valid;
  376. }
  377. /**
  378. * Gets the verification code.
  379. * @param boolean $regenerate whether the verification code should be regenerated.
  380. * @return string the verification code.
  381. */
  382. public function getVerifyCode($regenerate=false){
  383. if($this->fixedVerifyCode !== null){
  384. return $this->fixedVerifyCode;
  385. }
  386. $session = Yii::app()->session;
  387. $session->open();
  388. $name = $this->getSessionKey();
  389. if(empty($session[$name]) || $regenerate){
  390. $code = $this->generateVerifyCode();
  391. $session[$name] = $code['code'];
  392. $session[$name . 'result'] = $code['result'];
  393. $session[$name . 'count'] = 1;
  394. }
  395. return $session[$name];
  396. }
  397. /**
  398. * Return verification result expected by user
  399. * @param bool $regenerate
  400. */
  401. public function getVerifyResult($regenerate=false){
  402. if($this->fixedVerifyCode !== null){
  403. return $this->fixedVerifyCode;
  404. }
  405. $session = Yii::app()->session;
  406. $session->open();
  407. $name = $this->getSessionKey();
  408. if(empty($session[$name . 'result']) || $regenerate){
  409. $code = $this->generateVerifyCode();
  410. $session[$name] = $code['code'];
  411. $session[$name . 'result'] = $code['result'];
  412. $session[$name . 'count'] = 1;
  413. }
  414. return $session[$name . 'result'];
  415. }
  416. /**
  417. * Renders the CAPTCHA image based on the code.
  418. * @param string $code the verification code
  419. * @return string image content
  420. */
  421. protected function renderImage($code){
  422. $image = imagecreatetruecolor($this->width,$this->height);
  423. $backColor = imagecolorallocate($image,
  424. (int)($this->backColor % 0x1000000 / 0x10000),
  425. (int)($this->backColor % 0x10000 / 0x100),
  426. $this->backColor % 0x100);
  427. imagefilledrectangle($image,0,0,$this->width,$this->height,$backColor);
  428. imagecolordeallocate($image,$backColor);
  429. if($this->transparent){
  430. imagecolortransparent($image,$backColor);
  431. }
  432. if($this->fontFile === null){
  433. $this->fontFile = dirname(__FILE__) . '/Duality.ttf';
  434. }
  435. $length = strlen($code);
  436. $box = imagettfbbox(25,0,$this->fontFile,$code);
  437. $w = $box[4] - $box[0] + $this->offset * ($length - 1);
  438. $h = $box[1] - $box[5];
  439. $scale = min(($this->width - $this->padding * 2) / $w,($this->height - $this->padding * 2) / $h);
  440. $x = 10;
  441. $y = round($this->height * 27 / 40);
  442. $r = (int)($this->foreColor % 0x1000000 / 0x10000);
  443. $g = (int)($this->foreColor % 0x10000 / 0x100);
  444. $b = $this->foreColor % 0x100;
  445. $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
  446. for($i = 0; $i < $length; ++$i){
  447. $fontSize = (int)(rand(26,32) * $scale * 0.8);
  448. $angle = rand(-10,10);
  449. $letter = $code[$i];
  450. // UTF-8 characters above > 127 are stored in two bytes
  451. if(ord($letter)>127){
  452. ++$i;
  453. $letter .= $code[$i];
  454. }
  455. // randomize font color
  456. if(mt_rand(0,10)>7){
  457. $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
  458. }
  459. $box = imagettftext($image,$fontSize,$angle,$x,$y,$foreColor,$this->fontFile,$letter);
  460. $x = $box[2] + $this->offset;
  461. }
  462. // add density dots
  463. $this->density = intval($this->density);
  464. if($this->density > 0){
  465. $length = intval($this->width*$this->height/100*$this->density);
  466. $c = imagecolorallocate($image, mt_rand(0,255), mt_rand(0,255), mt_rand(0,255));
  467. for($i=0;$i<$length;++$i){
  468. $x = mt_rand(0,$this->width);
  469. $y = mt_rand(0,$this->height);
  470. imagesetpixel($image, $x, $y, $c);
  471. }
  472. }
  473. // add lines
  474. $this->lines = intval($this->lines);
  475. if($this->lines > 0){
  476. for($i=0; $i<$this->lines; ++$i){
  477. imagesetthickness($image, mt_rand(1,2));
  478. // gray lines only to save human eyes:-)
  479. $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
  480. $x = mt_rand(0, $this->width);
  481. $y = mt_rand(0, $this->width);
  482. imageline($image, $x, 0, $y, $this->height, $c);
  483. }
  484. }
  485. // filled flood section
  486. $this->fillSections = intval($this->fillSections);
  487. if($this->fillSections > 0){
  488. for($i = 0; $i < $this->fillSections; ++$i){
  489. $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
  490. $x = mt_rand(0, $this->width);
  491. $y = mt_rand(0, $this->width);
  492. imagefill($image, $x, $y, $c);
  493. }
  494. }
  495. imagecolordeallocate($image,$foreColor);
  496. header('Pragma: public');
  497. header('Expires: 0');
  498. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  499. header('Content-Transfer-Encoding: binary');
  500. header("Content-type: image/png");
  501. imagepng($image);
  502. imagedestroy($image);
  503. }
  504. }