CaptchaAction.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\captcha;
  8. use Yii;
  9. use yii\base\Action;
  10. use yii\base\InvalidConfigException;
  11. /**
  12. * CaptchaAction renders a CAPTCHA image.
  13. *
  14. * CaptchaAction is used together with [[Captcha]] and [[\yii\validators\CaptchaValidator]]
  15. * to provide the [CAPTCHA](http://en.wikipedia.org/wiki/Captcha) feature.
  16. *
  17. * By configuring the properties of CaptchaAction, you may customize the appearance of
  18. * the generated CAPTCHA images, such as the font color, the background color, etc.
  19. *
  20. * Note that CaptchaAction requires either GD2 extension or ImageMagick PHP extension.
  21. *
  22. * Using CAPTCHA involves the following steps:
  23. *
  24. * 1. Override [[\yii\web\Controller::actions()]] and register an action of class CaptchaAction with ID 'captcha'
  25. * 2. In the form model, declare an attribute to store user-entered verification code, and declare the attribute
  26. * to be validated by the 'captcha' validator.
  27. * 3. In the controller view, insert a [[Captcha]] widget in the form.
  28. *
  29. * @property string $verifyCode The verification code. This property is read-only.
  30. *
  31. * @author Qiang Xue <[email protected]>
  32. * @since 2.0
  33. */
  34. class CaptchaAction extends Action
  35. {
  36. /**
  37. * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated.
  38. */
  39. const REFRESH_GET_VAR = 'refresh';
  40. /**
  41. * @var integer how many times should the same CAPTCHA be displayed. Defaults to 3.
  42. * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2).
  43. */
  44. public $testLimit = 3;
  45. /**
  46. * @var integer the width of the generated CAPTCHA image. Defaults to 120.
  47. */
  48. public $width = 120;
  49. /**
  50. * @var integer the height of the generated CAPTCHA image. Defaults to 50.
  51. */
  52. public $height = 50;
  53. /**
  54. * @var integer padding around the text. Defaults to 2.
  55. */
  56. public $padding = 2;
  57. /**
  58. * @var integer the background color. For example, 0x55FF00.
  59. * Defaults to 0xFFFFFF, meaning white color.
  60. */
  61. public $backColor = 0xFFFFFF;
  62. /**
  63. * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color).
  64. */
  65. public $foreColor = 0x2040A0;
  66. /**
  67. * @var boolean whether to use transparent background. Defaults to false.
  68. */
  69. public $transparent = false;
  70. /**
  71. * @var integer the minimum length for randomly generated word. Defaults to 6.
  72. */
  73. public $minLength = 6;
  74. /**
  75. * @var integer the maximum length for randomly generated word. Defaults to 7.
  76. */
  77. public $maxLength = 7;
  78. /**
  79. * @var integer the offset between characters. Defaults to -2. You can adjust this property
  80. * in order to decrease or increase the readability of the captcha.
  81. **/
  82. public $offset = -2;
  83. /**
  84. * @var string the TrueType font file. This can be either a file path or path alias.
  85. */
  86. public $fontFile = '@yii/captcha/SpicyRice.ttf';
  87. /**
  88. * @var string the fixed verification code. When this property is set,
  89. * [[getVerifyCode()]] will always return the value of this property.
  90. * This is mainly used in automated tests where we want to be able to reproduce
  91. * the same verification code each time we run the tests.
  92. * If not set, it means the verification code will be randomly generated.
  93. */
  94. public $fixedVerifyCode;
  95. /**
  96. * Initializes the action.
  97. * @throws InvalidConfigException if the font file does not exist.
  98. */
  99. public function init()
  100. {
  101. $this->fontFile = Yii::getAlias($this->fontFile);
  102. if (!is_file($this->fontFile)) {
  103. throw new InvalidConfigException("The font file does not exist: {$this->fontFile}");
  104. }
  105. }
  106. /**
  107. * Runs the action.
  108. */
  109. public function run()
  110. {
  111. if (isset($_GET[self::REFRESH_GET_VAR])) {
  112. // AJAX request for regenerating code
  113. $code = $this->getVerifyCode(true);
  114. /** @var \yii\web\Controller $controller */
  115. $controller = $this->controller;
  116. return json_encode([
  117. 'hash1' => $this->generateValidationHash($code),
  118. 'hash2' => $this->generateValidationHash(strtolower($code)),
  119. // we add a random 'v' parameter so that FireFox can refresh the image
  120. // when src attribute of image tag is changed
  121. 'url' => $controller->createUrl($this->id, ['v' => uniqid()]),
  122. ]);
  123. } else {
  124. $this->setHttpHeaders();
  125. return $this->renderImage($this->getVerifyCode());
  126. }
  127. }
  128. /**
  129. * Generates a hash code that can be used for client side validation.
  130. * @param string $code the CAPTCHA code
  131. * @return string a hash code generated from the CAPTCHA code
  132. */
  133. public function generateValidationHash($code)
  134. {
  135. for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) {
  136. $h += ord($code[$i]);
  137. }
  138. return $h;
  139. }
  140. /**
  141. * Gets the verification code.
  142. * @param boolean $regenerate whether the verification code should be regenerated.
  143. * @return string the verification code.
  144. */
  145. public function getVerifyCode($regenerate = false)
  146. {
  147. if ($this->fixedVerifyCode !== null) {
  148. return $this->fixedVerifyCode;
  149. }
  150. $session = Yii::$app->getSession();
  151. $session->open();
  152. $name = $this->getSessionKey();
  153. if ($session[$name] === null || $regenerate) {
  154. $session[$name] = $this->generateVerifyCode();
  155. $session[$name . 'count'] = 1;
  156. }
  157. return $session[$name];
  158. }
  159. /**
  160. * Validates the input to see if it matches the generated code.
  161. * @param string $input user input
  162. * @param boolean $caseSensitive whether the comparison should be case-sensitive
  163. * @return boolean whether the input is valid
  164. */
  165. public function validate($input, $caseSensitive)
  166. {
  167. $code = $this->getVerifyCode();
  168. $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0;
  169. $session = Yii::$app->getSession();
  170. $session->open();
  171. $name = $this->getSessionKey() . 'count';
  172. $session[$name] = $session[$name] + 1;
  173. if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) {
  174. $this->getVerifyCode(true);
  175. }
  176. return $valid;
  177. }
  178. /**
  179. * Generates a new verification code.
  180. * @return string the generated verification code
  181. */
  182. protected function generateVerifyCode()
  183. {
  184. if ($this->minLength > $this->maxLength) {
  185. $this->maxLength = $this->minLength;
  186. }
  187. if ($this->minLength < 3) {
  188. $this->minLength = 3;
  189. }
  190. if ($this->maxLength > 20) {
  191. $this->maxLength = 20;
  192. }
  193. $length = mt_rand($this->minLength, $this->maxLength);
  194. $letters = 'bcdfghjklmnpqrstvwxyz';
  195. $vowels = 'aeiou';
  196. $code = '';
  197. for ($i = 0; $i < $length; ++$i) {
  198. if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) {
  199. $code .= $vowels[mt_rand(0, 4)];
  200. } else {
  201. $code .= $letters[mt_rand(0, 20)];
  202. }
  203. }
  204. return $code;
  205. }
  206. /**
  207. * Returns the session variable name used to store verification code.
  208. * @return string the session variable name
  209. */
  210. protected function getSessionKey()
  211. {
  212. return '__captcha/' . $this->getUniqueId();
  213. }
  214. /**
  215. * Renders the CAPTCHA image.
  216. * @param string $code the verification code
  217. * @return string image contents
  218. */
  219. protected function renderImage($code)
  220. {
  221. if (Captcha::checkRequirements() === 'gd') {
  222. return $this->renderImageByGD($code);
  223. } else {
  224. return $this->renderImageByImagick($code);
  225. }
  226. }
  227. /**
  228. * Renders the CAPTCHA image based on the code using GD library.
  229. * @param string $code the verification code
  230. * @return string image contents
  231. */
  232. protected function renderImageByGD($code)
  233. {
  234. $image = imagecreatetruecolor($this->width, $this->height);
  235. $backColor = imagecolorallocate($image,
  236. (int)($this->backColor % 0x1000000 / 0x10000),
  237. (int)($this->backColor % 0x10000 / 0x100),
  238. $this->backColor % 0x100);
  239. imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor);
  240. imagecolordeallocate($image, $backColor);
  241. if ($this->transparent) {
  242. imagecolortransparent($image, $backColor);
  243. }
  244. $foreColor = imagecolorallocate($image,
  245. (int)($this->foreColor % 0x1000000 / 0x10000),
  246. (int)($this->foreColor % 0x10000 / 0x100),
  247. $this->foreColor % 0x100);
  248. $length = strlen($code);
  249. $box = imagettfbbox(30, 0, $this->fontFile, $code);
  250. $w = $box[4] - $box[0] + $this->offset * ($length - 1);
  251. $h = $box[1] - $box[5];
  252. $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
  253. $x = 10;
  254. $y = round($this->height * 27 / 40);
  255. for ($i = 0; $i < $length; ++$i) {
  256. $fontSize = (int)(rand(26, 32) * $scale * 0.8);
  257. $angle = rand(-10, 10);
  258. $letter = $code[$i];
  259. $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter);
  260. $x = $box[2] + $this->offset;
  261. }
  262. imagecolordeallocate($image, $foreColor);
  263. ob_start();
  264. imagepng($image);
  265. imagedestroy($image);
  266. return ob_get_clean();
  267. }
  268. /**
  269. * Renders the CAPTCHA image based on the code using ImageMagick library.
  270. * @param string $code the verification code
  271. * @return \Imagick image instance. Can be used as string. In this case it will contain image contents.
  272. */
  273. protected function renderImageByImagick($code)
  274. {
  275. $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . dechex($this->backColor));
  276. $foreColor = new \ImagickPixel('#' . dechex($this->foreColor));
  277. $image = new \Imagick();
  278. $image->newImage($this->width, $this->height, $backColor);
  279. $draw = new \ImagickDraw();
  280. $draw->setFont($this->fontFile);
  281. $draw->setFontSize(30);
  282. $fontMetrics = $image->queryFontMetrics($draw, $code);
  283. $length = strlen($code);
  284. $w = (int)($fontMetrics['textWidth']) - 8 + $this->offset * ($length - 1);
  285. $h = (int)($fontMetrics['textHeight']) - 8;
  286. $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
  287. $x = 10;
  288. $y = round($this->height * 27 / 40);
  289. for ($i = 0; $i < $length; ++$i) {
  290. $draw = new \ImagickDraw();
  291. $draw->setFont($this->fontFile);
  292. $draw->setFontSize((int)(rand(26, 32) * $scale * 0.8));
  293. $draw->setFillColor($foreColor);
  294. $image->annotateImage($draw, $x, $y, rand(-10, 10), $code[$i]);
  295. $fontMetrics = $image->queryFontMetrics($draw, $code[$i]);
  296. $x += (int)($fontMetrics['textWidth']) + $this->offset;
  297. }
  298. $image->setImageFormat('png');
  299. return $image;
  300. }
  301. /**
  302. * Sets the HTTP headers needed by image response.
  303. */
  304. protected function setHttpHeaders()
  305. {
  306. Yii::$app->getResponse()->getHeaders()
  307. ->set('Pragma', 'public')
  308. ->set('Expires', '0')
  309. ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
  310. ->set('Content-Transfer-Encoding', 'binary')
  311. ->set('Content-type', 'image/png');
  312. }
  313. }