PageRenderTime 57ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/Captcha.php

https://github.com/bistory/kohana-captcha
PHP | 477 lines | 237 code | 64 blank | 176 comment | 24 complexity | 3cc0e3e5303f2925ec84a3b8381284f7 MD5 | raw file
  1. <?php defined('SYSPATH') OR die('No direct access.');
  2. /**
  3. * Captcha abstract class.
  4. *
  5. * @package Captcha
  6. * @author Michael Lavers
  7. * @author Kohana Team
  8. * @copyright (c) 2008-2010 Kohana Team
  9. * @license http://kohanaphp.com/license.html
  10. */
  11. abstract class Captcha
  12. {
  13. /**
  14. * @var object Captcha singleton
  15. */
  16. public static $instance;
  17. /**
  18. * @var string Style-dependent Captcha driver
  19. */
  20. protected $driver;
  21. /**
  22. * @var array Default config values
  23. */
  24. public static $config = array
  25. (
  26. 'style' => 'basic',
  27. 'width' => 150,
  28. 'height' => 50,
  29. 'complexity' => 4,
  30. 'background' => '',
  31. 'fontpath' => '',
  32. 'fonts' => array(),
  33. 'promote' => FALSE,
  34. );
  35. /**
  36. * @var string The correct Captcha challenge answer
  37. */
  38. protected $response;
  39. /**
  40. * @var string Image resource identifier
  41. */
  42. protected $image;
  43. /**
  44. * @var string Image type ("png", "gif" or "jpeg")
  45. */
  46. protected $image_type = 'png';
  47. /**
  48. * Singleton instance of Captcha.
  49. *
  50. * @param string $group Config group name
  51. * @return object
  52. */
  53. public static function instance($group = 'default')
  54. {
  55. if ( ! isset(Captcha::$instance))
  56. {
  57. // Load the configuration for this group
  58. $config = Kohana::$config->load('captcha')->get($group);
  59. // Set the captcha driver class name
  60. $class = 'Captcha_'.ucfirst($config['style']);
  61. // Create a new captcha instance
  62. Captcha::$instance = $captcha = new $class($group);
  63. // Save captcha response at shutdown
  64. //register_shutdown_function(array($captcha, 'update_response_session'));
  65. }
  66. return Captcha::$instance;
  67. }
  68. /**
  69. * Constructs a new Captcha object.
  70. *
  71. * @throws Kohana_Exception
  72. * @param string Config group name
  73. * @return void
  74. */
  75. public function __construct($group = NULL)
  76. {
  77. // Create a singleton instance once
  78. empty(Captcha::$instance) and Captcha::$instance = $this;
  79. // No config group name given
  80. if ( ! is_string($group))
  81. {
  82. $group = 'default';
  83. }
  84. // Load and validate config group
  85. if ( ! is_array($config = Kohana::$config->load('captcha')->get($group)))
  86. throw new Kohana_Exception('Captcha group not defined in :group configuration',
  87. array(':group' => $group));
  88. // All captcha config groups inherit default config group
  89. if ($group !== 'default')
  90. {
  91. // Load and validate default config group
  92. if ( ! is_array($default = Kohana::$config->load('captcha')->get('default')))
  93. throw new Kohana_Exception('Captcha group not defined in :group configuration',
  94. array(':group' => 'default'));
  95. // Merge config group with default config group
  96. $config += $default;
  97. }
  98. // Assign config values to the object
  99. foreach ($config as $key => $value)
  100. {
  101. if (array_key_exists($key, Captcha::$config))
  102. {
  103. Captcha::$config[$key] = $value;
  104. }
  105. }
  106. // Store the config group name as well, so the drivers can access it
  107. Captcha::$config['group'] = $group;
  108. // If using a background image, check if it exists
  109. if ( ! empty($config['background']))
  110. {
  111. Captcha::$config['background'] = str_replace('\\', '/', realpath($config['background']));
  112. if ( ! is_file(Captcha::$config['background']))
  113. throw new Kohana_Exception('The specified file, :file, was not found.',
  114. array(':file' => Captcha::$config['background']));
  115. }
  116. // If using any fonts, check if they exist
  117. if ( ! empty($config['fonts']))
  118. {
  119. Captcha::$config['fontpath'] = str_replace('\\', '/', realpath($config['fontpath'])).'/';
  120. foreach ($config['fonts'] as $font)
  121. {
  122. if ( ! is_file(Captcha::$config['fontpath'].$font))
  123. throw new Kohana_Exception('The specified file, :file, was not found.',
  124. array(':file' => Captcha::$config['fontpath'].$font));
  125. }
  126. }
  127. // Generate a new challenge
  128. $this->response = $this->generate_challenge();
  129. }
  130. /**
  131. * Update captcha response session variable.
  132. *
  133. * @return void
  134. */
  135. public function update_response_session()
  136. {
  137. // Store the correct Captcha response in a session
  138. Session::instance()->set('captcha_response', sha1(UTF8::strtoupper($this->response)));
  139. }
  140. /**
  141. * Validates user's Captcha response and updates response counter.
  142. *
  143. * @staticvar integer $counted Captcha attempts counter
  144. * @param string $response User's captcha response
  145. * @return boolean
  146. */
  147. public static function valid($response)
  148. {
  149. // Maximum one count per page load
  150. static $counted;
  151. // User has been promoted, always TRUE and don't count anymore
  152. if (Captcha::instance()->promoted())
  153. return TRUE;
  154. // Challenge result
  155. $result = (bool) (sha1(UTF8::strtoupper($response)) === Session::instance()->get('captcha_response'));
  156. // Increment response counter
  157. if ($counted !== TRUE)
  158. {
  159. $counted = TRUE;
  160. // Valid response
  161. if ($result === TRUE)
  162. {
  163. Captcha::instance()->valid_count(Session::instance()->get('captcha_valid_count') + 1);
  164. }
  165. // Invalid response
  166. else
  167. {
  168. Captcha::instance()->invalid_count(Session::instance()->get('captcha_invalid_count') + 1);
  169. }
  170. }
  171. return $result;
  172. }
  173. /**
  174. * Gets or sets the number of valid Captcha responses for this session.
  175. *
  176. * @param integer $new_count New counter value
  177. * @param boolean $invalid Trigger invalid counter (for internal use only)
  178. * @return integer Counter value
  179. */
  180. public function valid_count($new_count = NULL, $invalid = FALSE)
  181. {
  182. // Pick the right session to use
  183. $session = ($invalid === TRUE) ? 'captcha_invalid_count' : 'captcha_valid_count';
  184. // Update counter
  185. if ($new_count !== NULL)
  186. {
  187. $new_count = (int) $new_count;
  188. // Reset counter = delete session
  189. if ($new_count < 1)
  190. {
  191. Session::instance()->delete($session);
  192. }
  193. // Set counter to new value
  194. else
  195. {
  196. Session::instance()->set($session, (int) $new_count);
  197. }
  198. // Return new count
  199. return (int) $new_count;
  200. }
  201. // Return current count
  202. return (int) Session::instance()->get($session);
  203. }
  204. /**
  205. * Gets or sets the number of invalid Captcha responses for this session.
  206. *
  207. * @param integer $new_count New counter value
  208. * @return integer Counter value
  209. */
  210. public function invalid_count($new_count = NULL)
  211. {
  212. return $this->valid_count($new_count, TRUE);
  213. }
  214. /**
  215. * Resets the Captcha response counters and removes the count sessions.
  216. *
  217. * @return void
  218. */
  219. public function reset_count()
  220. {
  221. $this->valid_count(0);
  222. $this->valid_count(0, TRUE);
  223. }
  224. /**
  225. * Checks whether user has been promoted after having given enough valid responses.
  226. *
  227. * @param integer $threshold Valid response count threshold
  228. * @return boolean
  229. */
  230. public function promoted($threshold = NULL)
  231. {
  232. // Promotion has been disabled
  233. if (Captcha::$config['promote'] === FALSE)
  234. return FALSE;
  235. // Use the config threshold
  236. if ($threshold === NULL)
  237. {
  238. $threshold = Captcha::$config['promote'];
  239. }
  240. // Compare the valid response count to the threshold
  241. return ($this->valid_count() >= $threshold);
  242. }
  243. /**
  244. * Magically outputs the Captcha challenge.
  245. *
  246. * @return mixed
  247. */
  248. public function __toString()
  249. {
  250. return $this->render(TRUE);
  251. }
  252. /**
  253. * Returns the image type.
  254. *
  255. * @param string $filename Filename
  256. * @return string|boolean Image type ("png", "gif" or "jpeg")
  257. */
  258. public function image_type($filename)
  259. {
  260. switch (strtolower(substr(strrchr($filename, '.'), 1)))
  261. {
  262. case 'png':
  263. return 'png';
  264. case 'gif':
  265. return 'gif';
  266. case 'jpg':
  267. case 'jpeg':
  268. // Return "jpeg" and not "jpg" because of the GD2 function names
  269. return 'jpeg';
  270. default:
  271. return FALSE;
  272. }
  273. }
  274. /**
  275. * Creates an image resource with the dimensions specified in config.
  276. * If a background image is supplied, the image dimensions are used.
  277. *
  278. * @throws Kohana_Exception If no GD2 support
  279. * @param string $background Path to the background image file
  280. * @return void
  281. */
  282. public function image_create($background = NULL)
  283. {
  284. // Check for GD2 support
  285. if ( ! function_exists('imagegd2'))
  286. throw new Kohana_Exception('captcha.requires_GD2');
  287. // Create a new image (black)
  288. $this->image = imagecreatetruecolor(Captcha::$config['width'], Captcha::$config['height']);
  289. // Use a background image
  290. if ( ! empty($background))
  291. {
  292. // Create the image using the right function for the filetype
  293. $function = 'imagecreatefrom'.$this->image_type($background);
  294. $this->background_image = $function($background);
  295. // Resize the image if needed
  296. if (imagesx($this->background_image) !== Captcha::$config['width']
  297. or imagesy($this->background_image) !== Captcha::$config['height'])
  298. {
  299. imagecopyresampled
  300. (
  301. $this->image, $this->background_image, 0, 0, 0, 0,
  302. Captcha::$config['width'], Captcha::$config['height'],
  303. imagesx($this->background_image), imagesy($this->background_image)
  304. );
  305. }
  306. // Free up resources
  307. imagedestroy($this->background_image);
  308. }
  309. }
  310. /**
  311. * Fills the background with a gradient.
  312. *
  313. * @param resource $color1 GD image color identifier for start color
  314. * @param resource $color2 GD image color identifier for end color
  315. * @param string $direction Direction: 'horizontal' or 'vertical', 'random' by default
  316. * @return void
  317. */
  318. public function image_gradient($color1, $color2, $direction = NULL)
  319. {
  320. $directions = array('horizontal', 'vertical');
  321. // Pick a random direction if needed
  322. if ( ! in_array($direction, $directions))
  323. {
  324. $direction = $directions[array_rand($directions)];
  325. // Switch colors
  326. if (mt_rand(0, 1) === 1)
  327. {
  328. $temp = $color1;
  329. $color1 = $color2;
  330. $color2 = $temp;
  331. }
  332. }
  333. // Extract RGB values
  334. $color1 = imagecolorsforindex($this->image, $color1);
  335. $color2 = imagecolorsforindex($this->image, $color2);
  336. // Preparations for the gradient loop
  337. $steps = ($direction === 'horizontal') ? Captcha::$config['width'] : Captcha::$config['height'];
  338. $r1 = ($color1['red'] - $color2['red']) / $steps;
  339. $g1 = ($color1['green'] - $color2['green']) / $steps;
  340. $b1 = ($color1['blue'] - $color2['blue']) / $steps;
  341. if ($direction === 'horizontal')
  342. {
  343. $x1 =& $i;
  344. $y1 = 0;
  345. $x2 =& $i;
  346. $y2 = Captcha::$config['height'];
  347. }
  348. else
  349. {
  350. $x1 = 0;
  351. $y1 =& $i;
  352. $x2 = Captcha::$config['width'];
  353. $y2 =& $i;
  354. }
  355. // Execute the gradient loop
  356. for ($i = 0; $i <= $steps; $i++)
  357. {
  358. $r2 = $color1['red'] - floor($i * $r1);
  359. $g2 = $color1['green'] - floor($i * $g1);
  360. $b2 = $color1['blue'] - floor($i * $b1);
  361. $color = imagecolorallocate($this->image, $r2, $g2, $b2);
  362. imageline($this->image, $x1, $y1, $x2, $y2, $color);
  363. }
  364. }
  365. /**
  366. * Returns the img html element or outputs the image to the browser.
  367. *
  368. * @param boolean $html Output as HTML
  369. * @return mixed HTML, string or void
  370. */
  371. public function image_render($html, $request = NULL)
  372. {
  373. // Output html element
  374. if ($html === TRUE)
  375. return '<img src="'.URL::site('captcha/'.Captcha::$config['group']).'" width="'.Captcha::$config['width'].'" height="'.Captcha::$config['height'].'" alt="Captcha" class="captcha" />';
  376. // Send the correct HTTP header
  377. /*Request::current()->headers['Content-Type'] = 'image/'.$this->image_type;
  378. Request::current()->headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0';
  379. Request::current()->headers['Pragma'] = 'no-cache';
  380. Request::current()->headers['Connection'] = 'close';*/
  381. /*Request::current()->response()->headers('Content-Type', 'image/'.$this->image_type);
  382. Request::current()->response()->headers('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
  383. Request::current()->response()->headers('Pragma', 'no-cache');
  384. Request::current()->response()->headers('Connection', 'close');*/
  385. $request->headers(array(
  386. 'Content-Type' => 'image/'.$this->image_type,
  387. 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
  388. 'Pragma' => 'no-cache',
  389. 'Connection' => 'close'
  390. ));
  391. // Pick the correct output function
  392. $function = 'image'.$this->image_type;
  393. $function($this->image);
  394. // Free up resources
  395. imagedestroy($this->image);
  396. }
  397. /* DRIVER METHODS */
  398. /**
  399. * Generate a new Captcha challenge.
  400. *
  401. * @return string The challenge answer
  402. */
  403. abstract public function generate_challenge();
  404. /**
  405. * Output the Captcha challenge.
  406. *
  407. * @param boolean $html Render output as HTML
  408. * @return mixed
  409. */
  410. abstract public function render($html = TRUE, $request = NULL);
  411. } // End Captcha Class