PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/phpmyfaq/inc/Captcha.php

https://github.com/cyrke/phpMyFAQ
PHP | 565 lines | 289 code | 51 blank | 225 comment | 49 complexity | c4275f43f444cb98ce10b8a712ad36b8 MD5 | raw file
Possible License(s): LGPL-2.1, LGPL-3.0, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * The phpMyFAQ Captcha class
  4. *
  5. * PHP Version 5.3
  6. *
  7. * This Source Code Form is subject to the terms of the Mozilla Public License,
  8. * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  9. * obtain one at http://mozilla.org/MPL/2.0/.
  10. *
  11. * @category phpMyFAQ
  12. * @package PMF_Captcha
  13. * @author Thomas Zeithaml <seo@annatom.de>
  14. * @author Thorsten Rinne <thorsten@phpmyfaq.de>
  15. * @author Matteo Scaramuccia <matteo@scaramuccia.com>
  16. * @copyright 2006-2012 phpMyFAQ Team
  17. * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
  18. * @link http://www.phpmyfaq.de
  19. * @since 2006-02-04
  20. */
  21. if (!defined('IS_VALID_PHPMYFAQ')) {
  22. exit();
  23. }
  24. /**
  25. * PMF_Captcha
  26. *
  27. * @category phpMyFAQ
  28. * @package PMF_Captcha
  29. * @author Thomas Zeithaml <seo@annatom.de>
  30. * @author Thorsten Rinne <thorsten@phpmyfaq.de>
  31. * @author Matteo Scaramuccia <matteo@scaramuccia.com>
  32. * @copyright 2006-2012 phpMyFAQ Team
  33. * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
  34. * @link http://www.phpmyfaq.de
  35. * @since 2006-02-04
  36. */
  37. class PMF_Captcha
  38. {
  39. /**
  40. * @var PMF_Configuration
  41. */
  42. private $_config = null;
  43. /**
  44. * The phpMyFAQ session id
  45. *
  46. * @var string
  47. */
  48. private $sids;
  49. /**
  50. * Array of fonts
  51. *
  52. * @var array
  53. */
  54. private $fonts = array();
  55. /**
  56. * The captcha code
  57. *
  58. * @var string
  59. */
  60. private $code = '';
  61. /**
  62. * Array of characters
  63. *
  64. * @var array
  65. */
  66. private $letters = array(
  67. '1', '2', '3', '4', '5', '6', '7', '8', '9',
  68. 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
  69. 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
  70. 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
  71. );
  72. /**
  73. * Length of the captcha code
  74. *
  75. * @var integer
  76. */
  77. public $caplength = 6;
  78. /**
  79. * Width of the image
  80. *
  81. * @var integer
  82. */
  83. private $width = 165;
  84. /**
  85. * Height of the image
  86. *
  87. * @var integer
  88. */
  89. private $height = 40;
  90. /**
  91. * JPEG quality in percents
  92. *
  93. * @var integer
  94. */
  95. private $quality = 60;
  96. /**
  97. * Random background color RGB components
  98. *
  99. * @var array
  100. */
  101. private $_backgroundColor;
  102. /**
  103. * Generated image
  104. *
  105. * @var resource
  106. */
  107. private $img;
  108. /**
  109. * The user agent string
  110. *
  111. * @var string
  112. */
  113. private $userAgent;
  114. /**
  115. * Timestamp
  116. *
  117. * @var integer
  118. */
  119. private $timestamp;
  120. /**
  121. * Constructor
  122. *
  123. * @param PMF_Configuration $config
  124. *
  125. * @return PMF_Captcha
  126. */
  127. public function __construct(PMF_Configuration $config)
  128. {
  129. $this->_config = $config;
  130. $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
  131. $this->ip = $_SERVER['REMOTE_ADDR'];
  132. $this->fonts = $this->getFonts();
  133. $this->timestamp = $_SERVER['REQUEST_TIME'];
  134. }
  135. //
  136. // public functions
  137. //
  138. /**
  139. * Setter for session id
  140. *
  141. * @param integer $sid session id
  142. *
  143. * @return void
  144. */
  145. public function setSessionId($sid)
  146. {
  147. $this->sids = $sid;
  148. }
  149. /**
  150. * Setter for the captcha code length
  151. *
  152. * @param integer $caplength Length of captch code
  153. *
  154. * @return void
  155. */
  156. public function setCodeLength($length = 6)
  157. {
  158. $this->caplength = $length;
  159. }
  160. /**
  161. * Gives the HTML output code for the Captcha
  162. *
  163. * @param string $action The action parameter
  164. * @return string
  165. */
  166. public function printCaptcha($action)
  167. {
  168. $output = sprintf(
  169. '<img id="captchaImage" src="%s?%saction=%s&amp;gen=img&amp;ck=%s" height="%d" width="%d" border="0" alt="%s" title="%s" />',
  170. $_SERVER['SCRIPT_NAME'],
  171. $this->sids,
  172. $action,
  173. $_SERVER['REQUEST_TIME'],
  174. $this->height,
  175. $this->width,
  176. 'Chuck Norris has counted to infinity. Twice.',
  177. 'click to refresh');
  178. return $output;
  179. }
  180. /**
  181. * Draw the Captcha
  182. *
  183. * @return void
  184. */
  185. public function showCaptchaImg()
  186. {
  187. $this->createBackground();
  188. $this->drawlines();
  189. $this->generateCaptchaCode($this->caplength);
  190. $this->drawText();
  191. if (function_exists('imagepng')) {
  192. header('Content-Type: image/png');
  193. imagepng($this->img);
  194. } elseif (function_exists('imagejpeg')) {
  195. header('Content-Type: image/jpeg');
  196. imagejpeg($this->img, '', ( int )$this->quality);
  197. } elseif (function_exists('imagegif')) {
  198. header('Content-Type: image/gif');
  199. imagegif($this->img);
  200. }
  201. imagedestroy($this->img);
  202. }
  203. /**
  204. * Gets the Captcha from the DB
  205. *
  206. * @return string
  207. */
  208. public function getCaptchaCode()
  209. {
  210. $query = sprintf('SELECT id FROM %sfaqcaptcha', PMF_Db::getTablePrefix());
  211. $result = $this->_config->getDb()->query($query);
  212. while ($row = $this->_config->fetchArray($result)) {
  213. $this->code = $row['id'];
  214. }
  215. return $this->code;
  216. }
  217. /**
  218. * Validate the Captcha
  219. *
  220. * @param string $captchaCode Captcha code
  221. * @return boolean
  222. */
  223. public function validateCaptchaCode($captchaCode)
  224. {
  225. // Sanity check
  226. if (0 == PMF_String::strlen($captchaCode)) {
  227. return false;
  228. }
  229. $captchaCode = PMF_String::strtoupper($captchaCode);
  230. // Help the user: treat "0" (ASCII 48) like "O" (ASCII 79)
  231. // if "0" is not in the realm of captcha code letters
  232. if (!in_array("0", $this->letters)) {
  233. $captchaCode = str_replace("0", "O", $captchaCode);
  234. }
  235. // Sanity check
  236. for ($i = 0; $i < PMF_String::strlen( $captchaCode ); $i++) {
  237. if (!in_array($captchaCode[$i], $this->letters)) {
  238. return false;
  239. }
  240. }
  241. // Search for this Captcha in the db
  242. $query = sprintf("
  243. SELECT
  244. id
  245. FROM
  246. %sfaqcaptcha
  247. WHERE
  248. id = '%s'",
  249. PMF_Db::getTablePrefix(),
  250. $this->_config->getDb()->escape($captchaCode));
  251. if ($result = $this->_config->getDb()->query($query)) {
  252. $num = $this->_config->getDb()->numRows($result);
  253. if ($num > 0) {
  254. $this->code = $captchaCode;
  255. $this->removeCaptcha($captchaCode);
  256. return true;
  257. }
  258. }
  259. return false;
  260. }
  261. /**
  262. * This function checks the provided captcha code
  263. * if the captcha code spam protection has been activated from the general PMF configuration.
  264. *
  265. * @param string $code Captcha Code
  266. * @return bool
  267. */
  268. public function checkCaptchaCode($code)
  269. {
  270. if ($this->_config->get('spam.enableCaptchaCode')) {
  271. return $this->validateCaptchaCode($code);
  272. } else {
  273. return true;
  274. }
  275. }
  276. //
  277. // private functions
  278. //
  279. /**
  280. * Draw random lines
  281. *
  282. * @return resource
  283. */
  284. private function drawlines()
  285. {
  286. $color1 = rand(150, 185);
  287. $color2 = rand(185, 225);
  288. $nextline = 4;
  289. $w1 = 0;
  290. $w2 = 0;
  291. for ($x = 0; $x < $this->width; $x += (int)$nextline) {
  292. if ($x < $this->width) {
  293. imageline($this->img, $x + $w1, 0, $x + $w2, $this->height - 1, rand($color1, $color2));
  294. }
  295. if ($x < $this->height) {
  296. imageline($this->img, 0, $x - $w2, $this->width - 1, $x - $w1, rand($color1, $color2));
  297. }
  298. if (function_exists('imagettftext') && (count($this->fonts) > 0)) {
  299. $nextline += rand(-5, 7);
  300. if ($nextline < 1) {
  301. $nextline = 2;
  302. }
  303. }
  304. else {
  305. $nextline += rand(1, 7);
  306. }
  307. $w1 += rand(-4, 4);
  308. $w2 += rand(-4, 4);
  309. }
  310. return $this->img;
  311. }
  312. /**
  313. * Draw the Text
  314. *
  315. * @return resource
  316. */
  317. private function drawText()
  318. {
  319. $len = PMF_String::strlen($this->code);
  320. $w1 = 15;
  321. $w2 = $this->width / ($len + 1);
  322. for ($p = 0; $p < $len; $p++) {
  323. $letter = $this->code[$p];
  324. if (count($this->fonts) > 0) {
  325. $font = $this->fonts[rand(0, count($this->fonts) - 1)];
  326. }
  327. $size = rand(20, $this->height / 2.2);
  328. $rotation = rand(-23, 23);
  329. $y = rand($size + 3, $this->height-5);
  330. // $w1 += rand(- $this->width / 90, $this->width / 40 );
  331. $x = $w1 + $w2*$p;
  332. $c1 = array(); // fore char color
  333. $c2 = array(); // back char color
  334. do {
  335. $c1['r'] = mt_rand(30, 199);
  336. } while ($c1['r'] == $this->_backgroundColor['r']);
  337. do {
  338. $c1['g'] = mt_rand(30, 199);
  339. } while ($c1['g'] == $this->_backgroundColor['g']);
  340. do {
  341. $c1['b'] = mt_rand(30, 199);
  342. } while ($c1['b'] == $this->_backgroundColor['b']);
  343. $c1 = imagecolorallocate($this->img, $c1['r'], $c1['g'], $c1['b']);
  344. do {
  345. $c2['r'] = ($c1['r'] < 100 ? $c1['r'] * 2 : mt_rand(30, 199));
  346. } while (($c2['r'] == $this->_backgroundColor['r']) && ($c2['r'] == $c1['r']));
  347. do {
  348. $c2['g'] = ($c1['g'] < 100 ? $c1['g'] * 2 : mt_rand(30, 199));
  349. } while (($c2['g'] == $this->_backgroundColor['g']) && ($c2['g'] == $c1['g']));
  350. do {
  351. $c2['b'] = ($c1['b'] < 100 ? $c1['b'] * 2 : mt_rand(30, 199));
  352. } while (($c2['b'] == $this->_backgroundColor['b']) && ($c2['b'] == $c1['b']));
  353. $c2 = imagecolorallocate($this->img, $c2['r'], $c2['g'], $c2['b']);
  354. // Add the letter
  355. if (function_exists('imagettftext') && (count($this->fonts) > 0)) {
  356. imagettftext($this->img, $size, $rotation, $x + 2, $y, $c2, $font, $letter);
  357. imagettftext($this->img, $size, $rotation, $x + 1, $y + 1, $c2, $font, $letter);
  358. imagettftext($this->img, $size, $rotation, $x, $y-2, $c1, $font, $letter);
  359. } else {
  360. $size = 5;
  361. $c3 = imagecolorallocate($this->img, 0, 0, 255);
  362. $x = 20;
  363. $y = 12;
  364. $s = 30;
  365. imagestring($this->img, $size, $x + 1 + ($s * $p), $y+1, $letter, $c3);
  366. imagestring($this->img, $size, $x + ($s * $p), $y, $letter, $c1);
  367. }
  368. }
  369. return $this->img;
  370. }
  371. /**
  372. * Create the background
  373. *
  374. * @return resource
  375. */
  376. private function createBackground()
  377. {
  378. $this->img = imagecreate($this->width, $this->height);
  379. $this->_backgroundColor['r'] = rand(220, 255);
  380. $this->_backgroundColor['g'] = rand(220, 255);
  381. $this->_backgroundColor['b'] = rand(220, 255);
  382. $colorallocate = imagecolorallocate(
  383. $this->img,
  384. $this->_backgroundColor['r'],
  385. $this->_backgroundColor['g'],
  386. $this->_backgroundColor['b']
  387. );
  388. imagefilledrectangle($this->img, 0, 0, $this->width, $this->height, $colorallocate);
  389. return $this->img;
  390. }
  391. /**
  392. * Generate a Captcha Code
  393. *
  394. * @param integer $caplength Length of captch code
  395. * @return string
  396. */
  397. private function generateCaptchaCode($caplength)
  398. {
  399. // Start garbage collector for removing old (==unresolved) captcha codes
  400. // Note that we would like to avoid performing any garbaging of old records
  401. // because these data could be used as a database for collecting ip addresses,
  402. // eventually organizing them in subnetwork addresses, in order to use
  403. // them as an input for PMF IP banning.
  404. // This because we always perform these 3 checks on the public forms
  405. // in which captcha code feature is attached:
  406. // 1. Check against IP/Network address
  407. // 2. Check against banned words
  408. // 3. Check against the captcha code
  409. // so you could ban those "users" at the address level (1.).
  410. // If you want to look over your current data you could use this SQL query below:
  411. // SELECT DISTINCT ip, useragent, COUNT(ip) AS times
  412. // FROM faqcaptcha
  413. // GROUP BY ip
  414. // ORDER BY times DESC
  415. // to find out *bots and human attempts
  416. $this->garbageCollector();
  417. // Create the captcha code
  418. for ($i = 1; $i <= $caplength; $i++) {
  419. $j = floor(rand(0,34));
  420. $this->code .= $this->letters[$j];
  421. }
  422. if (!$this->saveCaptcha()) {
  423. return $this->generateCaptchaCode($caplength);
  424. }
  425. return $this->code;
  426. }
  427. /**
  428. * Save the Captcha
  429. *
  430. * @return boolean
  431. */
  432. private function saveCaptcha()
  433. {
  434. $select = sprintf("
  435. SELECT
  436. id
  437. FROM
  438. %sfaqcaptcha
  439. WHERE
  440. id = '%s'",
  441. PMF_Db::getTablePrefix(),
  442. $this->code
  443. );
  444. $result = $this->_config->getDb()->query($select);
  445. if ($result) {
  446. $num = $this->_config->getDb()->numRows($result);
  447. if ($num > 0) {
  448. return false;
  449. } else {
  450. $insert = sprintf("
  451. INSERT INTO
  452. %sfaqcaptcha
  453. (id, useragent, language, ip, captcha_time)
  454. VALUES
  455. ('%s', '%s', '%s', '%s', %d)",
  456. PMF_Db::getTablePrefix(),
  457. $this->code,
  458. $this->userAgent,
  459. $this->_config->getLanguage()->getLanguage(),
  460. $this->ip,
  461. $this->timestamp);
  462. $this->_config->getDb()->query($insert);
  463. return true;
  464. }
  465. }
  466. return false;
  467. }
  468. /**
  469. * Remove the Captcha
  470. *
  471. * @param string $captchaCode Captch code
  472. * @return void
  473. */
  474. private function removeCaptcha($captchaCode = null)
  475. {
  476. if ($captchaCode == null) {
  477. $captchaCode = $this->code;
  478. }
  479. $query = sprintf("DELETE FROM %sfaqcaptcha WHERE id = '%s'", PMF_Db::getTablePrefix(), $captchaCode);
  480. $this->_config->getDb()->query($query);
  481. }
  482. /**
  483. * Delete old captcha records.
  484. *
  485. * During normal use the <b>faqcaptcha</b> table would be empty, on average:
  486. * each record is created when a captcha image is showed to the user
  487. * and deleted upon a successful matching, so, on average, a record
  488. * in this table is probably related to a spam attack.
  489. *
  490. * @param int $time The time (sec) to define a captcha code old and ready
  491. * to be deleted (default: 1 week)
  492. * @return void
  493. */
  494. private function garbageCollector($time = 604800)
  495. {
  496. $delete = sprintf("
  497. DELETE FROM
  498. %sfaqcaptcha
  499. WHERE
  500. captcha_time < %d",
  501. PMF_Db::getTablePrefix(),
  502. $_SERVER['REQUEST_TIME'] - $time);
  503. $this->_config->getDb()->query($delete);
  504. }
  505. /**
  506. * Get Fonts
  507. *
  508. * @return array
  509. */
  510. private function getFonts()
  511. {
  512. return glob(__DIR__ . '/fonts/*.ttf');
  513. }
  514. }