image_captcha.user.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php
  2. /**
  3. * @file
  4. * Functions for the generation of the CAPTCHA image.
  5. *
  6. * Loosely Based on MyCaptcha by Heine Deelstra
  7. * (http://heine.familiedeelstra.com/mycaptcha-download)
  8. */
  9. /**
  10. * Menu callback function that generates the CAPTCHA image.
  11. */
  12. function image_captcha_image() {
  13. // If output buffering is on: discard current content and disable further buffering.
  14. if (ob_get_level()) {
  15. ob_end_clean();
  16. }
  17. if (!isset($_GET['sid']) || is_array($_GET['sid'])) {
  18. exit();
  19. }
  20. $captcha_sid = $_GET['sid'];
  21. // Get solution (the code to show).
  22. $code = db_query("SELECT solution FROM {captcha_sessions} WHERE csid = :csid",
  23. array(':csid' => $captcha_sid)
  24. )->fetchField();
  25. // Only generate captcha if code exists in the session.
  26. if ($code !== FALSE) {
  27. // Seed the random generators used for image CAPTCHA distortion based on session and code
  28. // to counter attacks that re-request the same challenge and pick the simplest image one or combine info.
  29. $seed = hexdec(substr(md5($captcha_sid . $code), 0, 8));
  30. srand($seed);
  31. mt_srand($seed);
  32. // Generate the image.
  33. $image = @_image_captcha_generate_image($code);
  34. // Check of generation was successful.
  35. if (!$image) {
  36. watchdog('CAPTCHA', 'Generation of image CAPTCHA failed. Check your image CAPTCHA configuration and especially the used font.', array(), WATCHDOG_ERROR);
  37. exit();
  38. }
  39. // Send the image resource as an image file to the client.
  40. $file_format = variable_get('image_captcha_file_format', IMAGE_CAPTCHA_FILE_FORMAT_JPG);
  41. if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_JPG) {
  42. drupal_add_http_header('Content-Type', 'image/jpeg');
  43. imagejpeg($image);
  44. }
  45. else {
  46. drupal_add_http_header('Content-Type', 'image/png');
  47. imagepng($image);
  48. }
  49. // Clean up the image resource.
  50. imagedestroy($image);
  51. }
  52. exit();
  53. }
  54. /**
  55. * Small helper function for parsing a hexadecimal color to a RGB tuple.
  56. */
  57. function _image_captcha_hex_to_rgb($hex) {
  58. // Handle #RGB format/
  59. if (strlen($hex) == 4) {
  60. $hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
  61. }
  62. $c = hexdec($hex);
  63. $rgb = array();
  64. for ($i = 16; $i >= 0; $i -= 8) {
  65. $rgb[] = ($c >> $i) & 0xFF;
  66. }
  67. return $rgb;
  68. }
  69. /**
  70. * Base function for generating a image CAPTCHA.
  71. */
  72. function _image_captcha_generate_image($code) {
  73. // Get font.
  74. $fonts = _image_captcha_get_enabled_fonts();
  75. // Get other settings.
  76. $font_size = (int) variable_get('image_captcha_font_size', 30);
  77. list($width, $height) = _image_captcha_image_size($code);
  78. // Create image resource.
  79. $image = imagecreatetruecolor($width, $height);
  80. if (!$image) {
  81. return FALSE;
  82. }
  83. // Get the background color and paint the background.
  84. $background_rgb = _image_captcha_hex_to_rgb(variable_get('image_captcha_background_color', '#ffffff'));
  85. $background_color = imagecolorallocate($image, $background_rgb[0], $background_rgb[1], $background_rgb[2]);
  86. // Set transparency if needed.
  87. $file_format = variable_get('image_captcha_file_format', IMAGE_CAPTCHA_FILE_FORMAT_JPG);
  88. if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) {
  89. imagecolortransparent($image, $background_color);
  90. }
  91. imagefilledrectangle($image, 0, 0, $width, $height, $background_color);
  92. // Do we need to draw in RTL mode?
  93. global $language;
  94. $rtl = $language->direction && ((bool) variable_get('image_captcha_rtl_support', 0));
  95. // Draw text.
  96. $result = _image_captcha_image_generator_print_string($image, $width, $height, $fonts, $font_size, $code, $rtl);
  97. if (!$result) {
  98. return FALSE;
  99. }
  100. // Add noise.
  101. $noise_colors = array();
  102. for ($i = 0; $i < 20; $i++) {
  103. $noise_colors[] = imagecolorallocate($image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
  104. }
  105. // Add additional noise.
  106. if (variable_get('image_captcha_dot_noise', 0)) {
  107. _image_captcha_image_generator_add_dots($image, $width, $height, $noise_colors);
  108. }
  109. if (variable_get('image_captcha_line_noise', 0)) {
  110. _image_captcha_image_generator_add_lines($image, $width, $height, $noise_colors);
  111. }
  112. // Distort the image.
  113. $distortion_amplitude = .25 * $font_size * variable_get('image_captcha_distortion_amplitude', 0) / 10.0;
  114. if ($distortion_amplitude > 1) {
  115. // Distortion parameters.
  116. $wavelength_xr = (2 + 3 * mt_rand(0, 1000) / 1000) * $font_size;
  117. $wavelength_yr = (2 + 3 * mt_rand(0, 1000) / 1000) * $font_size;
  118. $freq_xr = 2 * 3.141592 / $wavelength_xr;
  119. $freq_yr = 2 * 3.141592 / $wavelength_yr;
  120. $wavelength_xt = (2 + 3 * mt_rand(0, 1000) / 1000) * $font_size;
  121. $wavelength_yt = (2 + 3 * mt_rand(0, 1000) / 1000) * $font_size;
  122. $freq_xt = 2 * 3.141592 / $wavelength_xt;
  123. $freq_yt = 2 * 3.141592 / $wavelength_yt;
  124. $distorted_image = imagecreatetruecolor($width, $height);
  125. if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) {
  126. imagecolortransparent($distorted_image, $background_color);
  127. }
  128. if (!$distorted_image) {
  129. return FALSE;
  130. }
  131. if (variable_get('image_captcha_bilinear_interpolation', FALSE)) {
  132. // Distortion with bilinear interpolation.
  133. for ($x = 0; $x < $width; $x++) {
  134. for ($y = 0; $y < $height; $y++) {
  135. // Get distorted sample point in source image.
  136. $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr);
  137. $theta = $x * $freq_xt + $y * $freq_yt;
  138. $sx = $x + $r * cos($theta);
  139. $sy = $y + $r * sin($theta);
  140. $sxf = (int) floor($sx);
  141. $syf = (int) floor($sy);
  142. if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) {
  143. $color = $background_color;
  144. }
  145. else {
  146. // Bilinear interpolation: sample at four corners.
  147. $color_00 = imagecolorat($image, $sxf, $syf);
  148. $color_00_r = ($color_00 >> 16) & 0xFF;
  149. $color_00_g = ($color_00 >> 8) & 0xFF;
  150. $color_00_b = $color_00 & 0xFF;
  151. $color_10 = imagecolorat($image, $sxf + 1, $syf);
  152. $color_10_r = ($color_10 >> 16) & 0xFF;
  153. $color_10_g = ($color_10 >> 8) & 0xFF;
  154. $color_10_b = $color_10 & 0xFF;
  155. $color_01 = imagecolorat($image, $sxf, $syf + 1);
  156. $color_01_r = ($color_01 >> 16) & 0xFF;
  157. $color_01_g = ($color_01 >> 8) & 0xFF;
  158. $color_01_b = $color_01 & 0xFF;
  159. $color_11 = imagecolorat($image, $sxf + 1, $syf + 1);
  160. $color_11_r = ($color_11 >> 16) & 0xFF;
  161. $color_11_g = ($color_11 >> 8) & 0xFF;
  162. $color_11_b = $color_11 & 0xFF;
  163. // Interpolation factors.
  164. $u = $sx - $sxf;
  165. $v = $sy - $syf;
  166. // Interpolate.
  167. $r = (int) ((1 - $v) * ((1 - $u) * $color_00_r + $u * $color_10_r) + $v * ((1 - $u) * $color_01_r + $u * $color_11_r));
  168. $g = (int) ((1 - $v) * ((1 - $u) * $color_00_g + $u * $color_10_g) + $v * ((1 - $u) * $color_01_g + $u * $color_11_g));
  169. $b = (int) ((1 - $v) * ((1 - $u) * $color_00_b + $u * $color_10_b) + $v * ((1 - $u) * $color_01_b + $u * $color_11_b));
  170. // Build color.
  171. $color = ($r<<16) + ($g<<8) + $b;
  172. }
  173. imagesetpixel($distorted_image, $x, $y, $color);
  174. }
  175. }
  176. }
  177. else {
  178. // Distortion with nearest neighbor interpolation.
  179. for ($x = 0; $x < $width; $x++) {
  180. for ($y = 0; $y < $height; $y++) {
  181. // Get distorted sample point in source image.
  182. $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr);
  183. $theta = $x * $freq_xt + $y * $freq_yt;
  184. $sx = $x + $r * cos($theta);
  185. $sy = $y + $r * sin($theta);
  186. $sxf = (int) floor($sx);
  187. $syf = (int) floor($sy);
  188. if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) {
  189. $color = $background_color;
  190. }
  191. else {
  192. $color = imagecolorat($image, $sxf, $syf);
  193. }
  194. imagesetpixel($distorted_image, $x, $y, $color);
  195. }
  196. }
  197. }
  198. // Release undistorted image.
  199. imagedestroy($image);
  200. // Return distorted image.
  201. return $distorted_image;
  202. }
  203. else {
  204. return $image;
  205. }
  206. }
  207. /**
  208. * Add lines.
  209. */
  210. function _image_captcha_image_generator_add_lines(&$image, $width, $height, $colors) {
  211. $line_quantity = $width * $height / 200.0 * ((int) variable_get('image_captcha_noise_level', 5)) / 10.0;
  212. for ($i = 0; $i < $line_quantity; $i++) {
  213. imageline($image, mt_rand(0, $width), mt_rand(0, $height), mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]);
  214. }
  215. }
  216. /**
  217. * Add dots.
  218. */
  219. function _image_captcha_image_generator_add_dots(&$image, $width, $height, $colors) {
  220. $noise_quantity = $width * $height * ((int) variable_get('image_captcha_noise_level', 5)) / 10.0;
  221. for ($i = 0; $i < $noise_quantity; $i++) {
  222. imagesetpixel($image, mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]);
  223. }
  224. }
  225. /**
  226. * Helper function for drawing text on the image.
  227. */
  228. function _image_captcha_image_generator_print_string(&$image, $width, $height, $fonts, $font_size, $text, $rtl = FALSE) {
  229. // Get characters.
  230. $characters = _image_captcha_utf8_split($text);
  231. $character_quantity = count($characters);
  232. // Get colors.
  233. $background_rgb = _image_captcha_hex_to_rgb(variable_get('image_captcha_background_color', '#ffffff'));
  234. $foreground_rgb = _image_captcha_hex_to_rgb(variable_get('image_captcha_foreground_color', '#000000'));
  235. $background_color = imagecolorallocate($image, $background_rgb[0], $background_rgb[1], $background_rgb[2]);
  236. $foreground_color = imagecolorallocate($image, $foreground_rgb[0], $foreground_rgb[1], $foreground_rgb[2]);
  237. // Precalculate the value ranges for color randomness.
  238. $foreground_randomness = (int) (variable_get('image_captcha_foreground_color_randomness', 100));
  239. if ($foreground_randomness) {
  240. $foreground_color_range = array();
  241. for ($i = 0; $i < 3; $i++) {
  242. $foreground_color_range[$i] = array(
  243. max(0, $foreground_rgb[$i] - $foreground_randomness),
  244. min(255, $foreground_rgb[$i] + $foreground_randomness),
  245. );
  246. }
  247. }
  248. // Set default text color.
  249. $color = $foreground_color;
  250. // The image is seperated in different character cages, one for each character,
  251. // each character will be somewhere inside that cage.
  252. $ccage_width = $width / $character_quantity;
  253. $ccage_height = $height;
  254. foreach ($characters as $c => $character) {
  255. // Initial position of character: in the center of its cage.
  256. $center_x = ($c + 0.5) * $ccage_width;
  257. if ($rtl) {
  258. $center_x = $width - $center_x;
  259. }
  260. $center_y = 0.5 * $height;
  261. // Pick a random font from the list.
  262. $font = $fonts[array_rand($fonts)];
  263. // Get character dimensions for TrueType fonts.
  264. if ($font != 'BUILTIN') {
  265. $bbox = imagettfbbox($font_size, 0, drupal_realpath($font), $character);
  266. // In very rare cases with some versions of the GD library, the x-value
  267. // of the left side of the bounding box as returned by the first call of
  268. // imagettfbbox is corrupt (value -2147483648 = 0x80000000).
  269. // The weird thing is that calling the function a second time
  270. // can be used as workaround.
  271. // This issue is discussed at http://drupal.org/node/349218.
  272. if ($bbox[2] < 0) {
  273. $bbox = imagettfbbox($font_size, 0, drupal_realpath($font), $character);
  274. }
  275. }
  276. else {
  277. $character_width = imagefontwidth(5);
  278. $character_height = imagefontheight(5);
  279. $bbox = array(
  280. 0,
  281. $character_height,
  282. $character_width,
  283. $character_height,
  284. $character_width,
  285. 0,
  286. 0,
  287. 0,
  288. );
  289. }
  290. // Random (but small) rotation of the character.
  291. // TODO: add a setting for this?
  292. $angle = mt_rand(-10, 10);
  293. // Determine print position: at what coordinate should the character be
  294. // printed so that the bounding box would be nicely centered in the cage?
  295. $bb_center_x = .5 * ($bbox[0] + $bbox[2]);
  296. $bb_center_y = .5 * ($bbox[1] + $bbox[7]);
  297. $angle_cos = cos($angle * 3.1415 / 180);
  298. $angle_sin = sin($angle * 3.1415 / 180);
  299. $pos_x = $center_x - ($angle_cos * $bb_center_x + $angle_sin * $bb_center_y);
  300. $pos_y = $center_y - (-$angle_sin * $bb_center_x + $angle_cos * $bb_center_y);
  301. // Calculate available room to jitter: how much can the character be moved
  302. // so that it stays inside its cage?
  303. $bb_width = $bbox[2] - $bbox[0];
  304. $bb_height = $bbox[1] - $bbox[7];
  305. $dev_x = .5 * max(0, $ccage_width - abs($angle_cos) * $bb_width - abs($angle_sin) * $bb_height);
  306. $dev_y = .5 * max(0, $ccage_height - abs($angle_cos) * $bb_height - abs($angle_sin) * $bb_width);
  307. // Add jitter to position.
  308. $pos_x = $pos_x + mt_rand(-$dev_x, $dev_x);
  309. $pos_y = $pos_y + mt_rand(-$dev_y, $dev_y);
  310. // Calculate text color in case of randomness.
  311. if ($foreground_randomness) {
  312. $color = imagecolorallocate($image,
  313. mt_rand($foreground_color_range[0][0], $foreground_color_range[0][1]),
  314. mt_rand($foreground_color_range[1][0], $foreground_color_range[1][1]),
  315. mt_rand($foreground_color_range[2][0], $foreground_color_range[2][1])
  316. );
  317. }
  318. // Draw character.
  319. if ($font == 'BUILTIN') {
  320. imagestring($image, 5, $pos_x, $pos_y, $character, $color);
  321. }
  322. else {
  323. imagettftext($image, $font_size, $angle, $pos_x, $pos_y, $color, drupal_realpath($font), $character);
  324. }
  325. // For debugging purposes: draw character bounding box (only valid when rotation is disabled).
  326. // imagerectangle($image, $pos_x + $bbox[0], $pos_y + $bbox[1], $pos_x + $bbox[2], $pos_y + $bbox[7], $color);
  327. }
  328. // Return a sign of success.
  329. return TRUE;
  330. }