PageRenderTime 40ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/lms_debug/protected/extensions/captchaExtended/CaptchaExtendedAction.php

https://gitlab.com/badelal143/lms_debug
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
  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_DEFAULT;
  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. // Font Duality supplied with the framework does not support UTF-8, only ISO-8859-1 - missing accented 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->generateValidationHash(mb_convert_case($result, MB_CASE_LOWER, 'utf-8')),
  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. * Generates a hash code that can be used for client side validation.
  138. * @param string $code Displayed captcha code in generated image
  139. * @return int Sum of ASCII codes for all $code characters. Used for client-side validation
  140. */
  141. public function generateValidationHash($code){
  142. switch($this->mode){
  143. case CaptchaExtendedAction::MODE_DEFAULT:
  144. return parent::generateValidationHash($code);
  145. break;
  146. default:
  147. /**
  148. * We must encode result stored in session
  149. * @var CHttpSession
  150. */
  151. $session = Yii::app()->session;
  152. $key = $this->getSessionKey();
  153. if($this->mode != CaptchaExtendedAction::MODE_WORDS){
  154. // only in mode WORDS we must calculate ORD values of plain string
  155. // but we cannot use framework implementation since we need multibyte support
  156. $code = $session[$key.'result'];
  157. }
  158. $code = preg_replace('/\s/', '', $code);
  159. $code = urlencode($code);
  160. for($h=0,$i=mb_strlen($code)-1;$i>=0;--$i){
  161. $chr=mb_substr($code, $i, 1, 'utf-8');
  162. if(trim($chr)!=''){
  163. $h+=ord($chr);
  164. }
  165. }
  166. return $h;
  167. }
  168. }
  169. /**
  170. * Generates a new verification code.
  171. * @return string the generated verification code
  172. */
  173. protected function generateVerifyCode(){
  174. switch (strtolower($this->mode)){
  175. case self::MODE_MATH:
  176. return $this->getCodeMath();
  177. case self::MODE_MATHVERBAL:
  178. return $this->getCodeMathVerbal();
  179. case self::MODE_LOGICAL:
  180. return $this->getCodeLogical();
  181. case self::MODE_WORDS:
  182. return $this->getCodeWords();
  183. case self::MODE_DEFAULT:
  184. default:
  185. $code = parent::generateVerifyCode();
  186. return array('code' => $code, 'result' => $code);
  187. }
  188. }
  189. /**
  190. * Return code for random words from text file.
  191. * First we'll try to load file for current language, like [words.de.txt]
  192. * If not found, we will try to load generic file like [words.txt]
  193. */
  194. protected function getCodeWords(){
  195. if($this->fileWords === null){
  196. // guess words file upon current language like [words.de.txt], [words.ru.txt]
  197. $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.'.Yii::app()->language.'.txt';
  198. if(!is_file($this->fileWords)){
  199. // take fallback file without language specification
  200. $this->fileWords = dirname(__FILE__).DIRECTORY_SEPARATOR.'words.txt';
  201. }
  202. }
  203. if(!file_exists($this->fileWords)){
  204. throw new CHttpException(500, Yii::t('main','File not found in "{path}"', array('{path}' => $this->fileWords)));
  205. }
  206. $words = file_get_contents($this->fileWords);
  207. $words = explode(' ', $words);
  208. $found = array();
  209. for($i=0;$i<count($words);++$i){
  210. // select random word
  211. $w = array_splice($words, mt_rand(0,count($words)),1);
  212. if(empty($w[0])){
  213. continue;
  214. }
  215. // purify word
  216. $w = $this->purifyWord($w[0]);
  217. if(strlen($w)>3){
  218. // accept only word with at least 3 characters
  219. $found[] = $w;
  220. if(mb_strlen(implode('', $found), 'utf-8') > 10){
  221. // words must have at least 10 characters together
  222. break;
  223. }
  224. }
  225. }
  226. $code = implode(' ', $found); // without whitespaces
  227. $result = preg_replace('/\s/', '', $code);
  228. return array('code' => $code, 'result' => $result);
  229. }
  230. /**
  231. * Return captcha word without dirty characters like *,/,{,},.. Retain diacritics if unicode supported.
  232. * @param string $w The word to be purified
  233. */
  234. protected function purifyWord($w){
  235. if(@preg_match('/\pL/u', 'a')){
  236. // unicode supported, we remove everything except for accented characters
  237. $w = preg_replace('/[^\p{L}]/u', '', (string) $w);
  238. }else{
  239. // Unicode is not supported. Cannot validate utf-8 characters, we keep only latin1
  240. $w = preg_replace('/[^a-zA-Z0-9]/','',$w);
  241. }
  242. return $w;
  243. }
  244. /**
  245. * Return code for math mode like 9+1= or 95-5=
  246. */
  247. protected function getCodeMath(){
  248. $n2 = mt_rand(1,9);
  249. if(mt_rand(1,100) > 50){
  250. $n1 = mt_rand(1,9)*10+$n2;
  251. $code = $n1.'-'.$n2.'=';
  252. $r = $n1-$n2;
  253. }else{
  254. $n1 = mt_rand(1,10)*10-$n2;
  255. $code = $n1.'+'.$n2.'=';
  256. $r = $n1+$n2;
  257. }
  258. return array('code' => $code, 'result' => $r);
  259. }
  260. /**
  261. * Return numbers 0..9 translated into word
  262. */
  263. protected static function getNumbers(){
  264. return array(
  265. '0' => Yii::t('main','zero'),
  266. '1' => Yii::t('main','one'),
  267. '2' => Yii::t('main','two'),
  268. '3' => Yii::t('main','three'),
  269. '4' => Yii::t('main','four'),
  270. '5' => Yii::t('main','five'),
  271. '6' => Yii::t('main','six'),
  272. '7' => Yii::t('main','seven'),
  273. '8' => Yii::t('main','eight'),
  274. '9' => Yii::t('main','nine'),
  275. );
  276. }
  277. /**
  278. * Return verbal representation for supplied number, like 1 => one
  279. * @param int $n The number to be translated
  280. */
  281. protected static function getNumber($n){
  282. static $nums;
  283. if(empty($nums)){
  284. $nums = self::getNumbers();
  285. }
  286. return array_key_exists($n, $nums) ? $nums[$n] : '';
  287. }
  288. /**
  289. * Return code for logical formula like min(one,7,four)
  290. */
  291. protected function getCodeLogical(){
  292. $t = mt_rand(2,4);
  293. $a = array();
  294. for($i=0;$i<$t;++$i){
  295. // we dont use zero
  296. $a[] = mt_rand(1,9);
  297. }
  298. if(mt_rand(0,1)){
  299. $r = max($a);
  300. $code = array();
  301. for($i=0;$i<count($a);++$i){
  302. $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
  303. }
  304. $code = Yii::t('main','max').' ( '.implode(', ',$code).' )';
  305. }else{
  306. $r = min($a);
  307. $code = array();
  308. for($i=0;$i<count($a);++$i){
  309. $code[] = mt_rand(1,100)>30 ? self::getNumber($a[$i]) : $a[$i];
  310. }
  311. $code = Yii::t('main','min').' ( '.implode(', ',$code).' )';
  312. }
  313. return array('code' => $code, 'result' => $r);
  314. }
  315. /**
  316. * Return code for verbal math mode like "How much is 1 plus 1 ?"
  317. */
  318. protected function getCodeMathVerbal(){
  319. $n2 = mt_rand(1,9);
  320. if(mt_rand(1,100) > 50){
  321. switch(mt_rand(0,2)){
  322. case 0:
  323. $op = Yii::t('main','minus');
  324. break;
  325. case 1:
  326. $op = Yii::t('main','deducted by');
  327. break;
  328. case 2:
  329. $op = '-';
  330. break;
  331. }
  332. $n1 = mt_rand(1,9)*10+$n2;
  333. $code = $n1.' '.$op.' '. ( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
  334. $r = $n1-$n2;
  335. }else{
  336. switch(mt_rand(0,2)){
  337. case 0:
  338. $op = Yii::t('main','plus');
  339. break;
  340. case 1:
  341. $op = Yii::t('main','and');
  342. break;
  343. case 2:
  344. $op = '+';
  345. break;
  346. }
  347. $n1 = mt_rand(1,10)*10-$n2;
  348. $code = $n1.' '.$op.' '.( mt_rand(1,10)>3 ? self::getNumber($n2) : $n2);
  349. $r = $n1+$n2;
  350. }
  351. switch (mt_rand(0,2)){
  352. case 0:
  353. $question = Yii::t('main','How much is');
  354. break;
  355. case 1:
  356. $question = Yii::t('main','What\'s result for');
  357. break;
  358. case 2:
  359. $question = Yii::t('main','Give result for');
  360. break;
  361. }
  362. switch (mt_rand(0,2)){
  363. case 0:
  364. $equal = '?';
  365. break;
  366. case 1:
  367. $equal = '=';
  368. break;
  369. case 2:
  370. $equal = str_repeat('.', mt_rand(2,5));
  371. break;
  372. }
  373. $code = $question.' '.$code.' '.$equal;
  374. return array('code' => $code, 'result' => $r);
  375. }
  376. /**
  377. * Validates the input to see if it matches the generated code.
  378. * @param string $input user input
  379. * @param boolean $caseSensitive whether the comparison should be case-sensitive
  380. * @return whether the input is valid
  381. */
  382. public function validate($input,$caseSensitive){
  383. // open session, if necessary generate new code
  384. $this->getVerifyCode();
  385. // read result
  386. $session = Yii::app()->session;
  387. $name = $this->getSessionKey();
  388. $result = $session[$name . 'result'];
  389. // input always taken without whitespaces
  390. $input = preg_replace('/\s/','',$input);
  391. $valid = $caseSensitive ? strcmp($input, $result)===0 : strcasecmp($input, $result)===0;
  392. // increase attempts counter
  393. $name = $this->getSessionKey() . 'count';
  394. $session[$name] = $session[$name] + 1;
  395. if($valid || $session[$name] > $this->testLimit && $this->testLimit > 0){
  396. // generate new code also each time correctly entered
  397. $this->getVerifyCode(true);
  398. }
  399. return $valid;
  400. }
  401. /**
  402. * Gets the verification code.
  403. * @param boolean $regenerate whether the verification code should be regenerated.
  404. * @return string the verification code.
  405. */
  406. public function getVerifyCode($regenerate=false){
  407. if($this->fixedVerifyCode !== null){
  408. return $this->fixedVerifyCode;
  409. }
  410. $session = Yii::app()->session;
  411. $session->open();
  412. $name = $this->getSessionKey();
  413. if(empty($session[$name]) || $regenerate){
  414. $code = $this->generateVerifyCode();
  415. $session[$name] = $code['code'];
  416. $session[$name . 'result'] = $code['result'];
  417. $session[$name . 'count'] = 1;
  418. }
  419. return $session[$name];
  420. }
  421. /**
  422. * Return verification result expected by user
  423. * @param bool $regenerate
  424. */
  425. public function getVerifyResult($regenerate=false){
  426. if($this->fixedVerifyCode !== null){
  427. return $this->fixedVerifyCode;
  428. }
  429. $session = Yii::app()->session;
  430. $session->open();
  431. $name = $this->getSessionKey();
  432. if(empty($session[$name . 'result']) || $regenerate){
  433. $code = $this->generateVerifyCode();
  434. $session[$name] = $code['code'];
  435. $session[$name . 'result'] = $code['result'];
  436. $session[$name . 'count'] = 1;
  437. }
  438. return $session[$name . 'result'];
  439. }
  440. /**
  441. * Renders the CAPTCHA image based on the code.
  442. * @param string $code the verification code
  443. * @return string image content
  444. */
  445. protected function renderImage($code){
  446. $image = imagecreatetruecolor($this->width,$this->height);
  447. $backColor = imagecolorallocate($image,
  448. (int)($this->backColor % 0x1000000 / 0x10000),
  449. (int)($this->backColor % 0x10000 / 0x100),
  450. $this->backColor % 0x100);
  451. imagefilledrectangle($image,0,0,$this->width,$this->height,$backColor);
  452. imagecolordeallocate($image,$backColor);
  453. if($this->transparent){
  454. imagecolortransparent($image,$backColor);
  455. }
  456. if($this->fontFile === null){
  457. $this->fontFile = dirname(__FILE__) . '/Duality.ttf';
  458. }
  459. $length = strlen($code);
  460. $box = imagettfbbox(25,0,$this->fontFile,$code);
  461. $w = $box[4] - $box[0] + $this->offset * ($length - 1);
  462. $h = $box[1] - $box[5];
  463. $scale = min(($this->width - $this->padding * 2) / $w,($this->height - $this->padding * 2) / $h);
  464. $x = 10;
  465. $y = round($this->height * 27 / 40);
  466. $r = (int)($this->foreColor % 0x1000000 / 0x10000);
  467. $g = (int)($this->foreColor % 0x10000 / 0x100);
  468. $b = $this->foreColor % 0x100;
  469. $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
  470. for($i = 0; $i < $length; ++$i){
  471. $fontSize = (int)(rand(26,32) * $scale * 0.8);
  472. $angle = rand(-10,10);
  473. $letter = $code[$i];
  474. // UTF-8 characters above > 127 are stored in two bytes
  475. if(ord($letter)>127){
  476. ++$i;
  477. $letter .= $code[$i];
  478. }
  479. // randomize font color
  480. if(mt_rand(0,10)>7){
  481. $foreColor = imagecolorallocate($image, mt_rand($r-50,$r+50), mt_rand($g-50,$g+50),mt_rand($b-50,$b+50));
  482. }
  483. $box = imagettftext($image,$fontSize,$angle,$x,$y,$foreColor,$this->fontFile,$letter);
  484. $x = $box[2] + $this->offset;
  485. }
  486. // add density dots
  487. $this->density = intval($this->density);
  488. if($this->density > 0){
  489. $length = intval($this->width*$this->height/100*$this->density);
  490. $c = imagecolorallocate($image, mt_rand(0,255), mt_rand(0,255), mt_rand(0,255));
  491. for($i=0;$i<$length;++$i){
  492. $x = mt_rand(0,$this->width);
  493. $y = mt_rand(0,$this->height);
  494. imagesetpixel($image, $x, $y, $c);
  495. }
  496. }
  497. // add lines
  498. $this->lines = intval($this->lines);
  499. if($this->lines > 0){
  500. for($i=0; $i<$this->lines; ++$i){
  501. imagesetthickness($image, mt_rand(1,2));
  502. // gray lines only to save human eyes:-)
  503. $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
  504. $x = mt_rand(0, $this->width);
  505. $y = mt_rand(0, $this->width);
  506. imageline($image, $x, 0, $y, $this->height, $c);
  507. }
  508. }
  509. // filled flood section
  510. $this->fillSections = intval($this->fillSections);
  511. if($this->fillSections > 0){
  512. for($i = 0; $i < $this->fillSections; ++$i){
  513. $c = imagecolorallocate($image, mt_rand(200,255), mt_rand(200,255), mt_rand(200,255));
  514. $x = mt_rand(0, $this->width);
  515. $y = mt_rand(0, $this->width);
  516. imagefill($image, $x, $y, $c);
  517. }
  518. }
  519. imagecolordeallocate($image,$foreColor);
  520. header('Pragma: public');
  521. header('Expires: 0');
  522. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  523. header('Content-Transfer-Encoding: binary');
  524. header("Content-type: image/png");
  525. imagepng($image);
  526. imagedestroy($image);
  527. }
  528. }