PageRenderTime 46ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/captcha.php

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