Topic: Request new captcha

It would be nice if there was a way to do it.
Reloading the page isn't that great because it empties the form.

Speaking of the captcha, the "O" looks like a zero and can therefor lead to errors.
Similarly the letter "Q" is hard to tell from "O" (the little line could be captcha interferences).
Maybe these letters could just be removed from captcha generation.

2

Re: Request new captcha

Well, ,the "O" vs zero thing is moot because numbers are not used. The O vs Q is an issue and I actually did try removing Qs from use but that created really subtle bugs that were maddeningly hard to track down. Also, since XmlHttpRequest is used to "pre-verify" before submitted a form, you get immediate feedback if the captcha doesn't match, before having to wait for a file upload, so the cost of trying a second guess isn't too high.

Having said all that, the existing captcha code was supposed to be a proof-of-concept kind of thing and stop spammers, but isn't the best. If you have other code for generating captcha images, it should be easy to drop-in. Just replace includes/verification.php with the image generating code (but keep the first two lines).

As for requesting a new captcha: I've been meaning to look into this.

If you know of captcha generating code, I can take a look at making the modification.

3 (edited by Kolya 2007-01-25 18:11:54)

Re: Request new captcha

A new user won't know that numbers aren't used. ;)
SMF forum software has an optional captcha when registering where you can request a new captcha. You can see it here: http://www.strangebedfellows.de/index.p … n=register

Here's the code (sources/register.php)

    // Generate a visual verification code to make sure the user is no bot.
    $context['visual_verification'] = empty($modSettings['disable_visual_verification']);
    if ($context['visual_verification'])
    {
        $context['use_graphic_library'] = in_array('gd', get_loaded_extensions());
        $context['verificiation_image_href'] = $scripturl . '?action=verificationcode;rand=' . md5(rand());

        // Only generate a new code if one hasn't been set yet
        if (!isset($_SESSION['visual_verification_code']))
        {
            // Skip I, J, L, O and Q.
            $character_range = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P'), range('R', 'Z'));

            // Generate a new code.
            $_SESSION['visual_verification_code'] = '';
            for ($i = 0; $i < 5; $i++)
                $_SESSION['visual_verification_code'] .= $character_range[array_rand($character_range)];
        }
    }
}

And the code from Sources/Subs-Graphics.

// Create the image for the visual verification code.
function showCodeImage($code)
{
    global $settings;

    // Is this GD2? Needed for pixel size.
    $testGD = get_extension_funcs('gd');
    $gd2 = in_array('imagecreatetruecolor', $testGD) && function_exists('imagecreatetruecolor');
    unset($testGD);

    // The amount of pixels inbetween characters.
    $character_spacing = 1;

    // The color of the characters shown (red, green, blue).
    $foreground_color = array(64, 101, 136);
    $background_color = array(236, 237, 243);

    // Has the theme author requested a custom color?
    if (isset($settings['verification_foreground'], $settings['verification_background']))
    {
        $foreground_color = $settings['verification_foreground'];
        $background_color = $settings['verification_background'];
    }

    if (!is_dir($settings['default_theme_dir'] . '/fonts'))
        return false;

    // Get a list of the available fonts.
    $font_dir = dir($settings['default_theme_dir'] . '/fonts');
    $font_list = array();
    $ttfont_list = array();
    while ($entry = $font_dir->read())
    {
        if (preg_match('~^(.+)\.gdf$~', $entry, $matches) === 1)
            $font_list[] = $entry;
        elseif (preg_match('~^(.+)\.ttf$~', $entry, $matches) === 1)
            $ttfont_list[] = $entry;
    }

    if (empty($font_list))
        return false;

    // Create a list of characters to be shown.
    $characters = array();
    $loaded_fonts = array();
    for ($i = 0; $i < strlen($code); $i++)
    {
        $characters[$i] = array(
            'id' => $code{$i},
            'font' => array_rand($font_list),
        );

        $loaded_fonts[$characters[$i]['font']] = null;
    }

    // Load all fonts and determine the maximum font height.
    foreach ($loaded_fonts as $font_index => $dummy)
        $loaded_fonts[$font_index] = imageloadfont($settings['default_theme_dir'] . '/fonts/' . $font_list[$font_index]);

    // Determine the dimensions of each character.
    $total_width = $character_spacing * strlen($code) + 10;
    $max_height = 0;
    foreach ($characters as $char_index => $character)
    {
        $characters[$char_index]['width'] = imagefontwidth($loaded_fonts[$character['font']]);
        $characters[$char_index]['height'] = imagefontheight($loaded_fonts[$character['font']]);

        $max_height = max($characters[$char_index]['height'], $max_height);
        $total_width += $characters[$char_index]['width'];
    }

    // Create an image.
    $code_image = imagecreate($total_width, $max_height);

    // Draw the background.
    $bg_color = imagecolorallocate($code_image, $background_color[0], $background_color[1], $background_color[2]);
    imagefilledrectangle($code_image, 0, 0, $total_width - 1, $max_height - 1, $bg_color);

    // Randomize the foreground color a little.
    for ($i = 0; $i < 3; $i++)
        $foreground_color[$i] = rand(max($foreground_color[$i] - 3, 0), min($foreground_color[$i] + 3, 255));
    $fg_color = imagecolorallocate($code_image, $foreground_color[0], $foreground_color[1], $foreground_color[2]);

    // Color for the dots.
    for ($i = 0; $i < 3; $i++)
        $dotbgcolor[$i] = $background_color[$i] < $foreground_color[$i] ? rand(0, max($foreground_color[$i] - 20, 0)) : rand(min($foreground_color[$i] + 20, 255), 255);
    $randomness_color = imagecolorallocate($code_image, $dotbgcolor[0], $dotbgcolor[1], $dotbgcolor[2]);

    // Fill in the characters.
    $cur_x = 0;
    foreach ($characters as $char_index => $character)
    {
        // Can we use true type fonts?
        $can_do_ttf = function_exists('imagettftext');
        if (!empty($can_do_ttf))
        {
            // GD2 handles font size differently.
            $font_size = $gd2 ? rand(15, 18) : rand(18, 25);

            // Work out the sizes - also fix the character width cause TTF not quite so wide!
            $font_x = $cur_x + 5;
            $font_y = $max_height - rand(2, 8);

            // What font face?
            if (!empty($ttfont_list))
            {
                $fontface = $settings['default_theme_dir'] . '/fonts/' . $ttfont_list[rand(0, count($ttfont_list) - 1)];
                //log_error($fontface);
            }

            // What color are we to do it in?
            $is_reverse = rand(0, 1);
            $char_color = imagecolorallocate($code_image, rand(max($foreground_color[0] - 2, 0), $foreground_color[0]), rand(max($foreground_color[1] - 2, 0), $foreground_color[1]), rand(max($foreground_color[2] - 2, 0), $foreground_color[2]));

            $angle = rand(-100, 100) / 10;
            $show_letter = rand(0, 1) ? $character['id'] : strtolower($character['id']);
            $fontcord = @imagettftext($code_image, 18, $angle, $font_x, $font_y, $char_color, $fontface, $show_letter);
            if (empty($fontcord))
                $can_do_ttf = false;
            elseif ($is_reverse)
            {
                imagefilledpolygon($code_image, $fontcord, 4, $fg_color);
                // Put the character back!
                imagettftext($code_image, 18, $angle, $font_x, $font_y, $randomness_color, $fontface, $show_letter);
            }

            if ($can_do_ttf)
                $cur_x = max($fontcord[2], $fontcord[4]) + 3;
        }

        if (!$can_do_ttf)
        {
            // Rotating the characters a little...
            if (function_exists('imagerotate'))
            {
                $char_image = function_exists('imagecreatetruecolor') ? imagecreatetruecolor($character['width'], $character['height']) : imagecreate($character['width'], $character['height']);
                $char_bgcolor = imagecolorallocate($char_image, $background_color[0], $background_color[1], $background_color[2]);
                imagefilledrectangle($char_image, 0, 0, $character['width'] - 1, $character['height'] - 1, $char_bgcolor);
                imagechar($char_image, $loaded_fonts[$character['font']], 0, 0, $character['id'], imagecolorallocate($char_image, rand(max($foreground_color[0] - 2, 0), $foreground_color[0]), rand(max($foreground_color[1] - 2, 0), $foreground_color[1]), rand(max($foreground_color[2] - 2, 0), $foreground_color[2])));
                $rotated_char = imagerotate($char_image, rand(-100, 100) / 10, $char_bgcolor);
                imagecopy($code_image, $rotated_char, $cur_x, 0, 0, 0, $character['width'], $character['height']);
                imagedestroy($rotated_char);
                imagedestroy($char_image);
            }
    
            // Sorry, no rotation available.
            else
                imagechar($code_image, $loaded_fonts[$character['font']], $cur_x, floor(($max_height - $character['height']) / 2), $character['id'], imagecolorallocate($code_image, rand(max($foreground_color[0] - 2, 0), $foreground_color[0]), rand(max($foreground_color[1] - 2, 0), $foreground_color[1]), rand(max($foreground_color[2] - 2, 0), $foreground_color[2])));
            $cur_x += $character['width'] + $character_spacing;
        }
    }

    // Make the background color transparent.
    imagecolortransparent($code_image, $bg_color);

    // Add some randomness to the background.
    for ($i = rand(0, 2); $i < $max_height; $i += rand(1, 2))
        for ($j = rand(0, 10); $j < $total_width; $j += rand(1, 15))
            imagesetpixel($code_image, $j, $i, rand(0, 1) ? $fg_color : $randomness_color);

    // Put in some lines too.
    $num_lines = 2;
    for ($i = 0; $i < $num_lines; $i++)
    {
        if (rand(0, 1))
        {
            $x1 = rand(0, $total_width);
            $x2 = rand(0, $total_width);
            $y1 = 0; $y2 = $max_height;
        }
        else
        {
            $y1 = rand(0, $max_height);
            $y2 = rand(0, $max_height);
            $x1 = 0; $x2 = $total_width;
        }

        imageline($code_image, $x1, $y1, $x2, $y2, rand (0, 1) ? $fg_color : $randomness_color);
    }

    // Show the image.
    if (function_exists('imagegif'))
    {
        header('Content-type: image/gif');
        imagegif($code_image);
    }
    else
    {
        header('Content-type: image/png');
        imagepng($code_image);
    }

    // Bail out.
    imagedestroy($code_image);
    die();
}

// Create a letter for the visual verification code.
function showLetterImage($letter)
{
    global $settings;

    if (!is_dir($settings['default_theme_dir'] . '/fonts'))
        return false;

    // Get a list of the available font directories.
    $font_dir = dir($settings['default_theme_dir'] . '/fonts');
    $font_list = array();
    while ($entry = $font_dir->read())
        if ($entry{0} !== '.' && is_dir($settings['default_theme_dir'] . '/fonts/' . $entry) && file_exists($settings['default_theme_dir'] . '/fonts/' . $entry . '.gdf'))
            $font_list[] = $entry;

    if (empty($font_list))
        return false;

    // Pick a random font.
    $random_font = $font_list[array_rand($font_list)];

    // Check if the given letter exists.
    if (!file_exists($settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . $letter . '.gif'))
        return false;

    // Include it!
    header('Content-type: image/gif');
    include($settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . $letter . '.gif');

This should be it.

4 (edited by h3 2007-01-25 18:52:58)

Re: Request new captcha

So that was interesting. The following changes adds a feature where you can click on the captcha image to generate a new one. Two files need to be changed, includes/verification.php and js/ochiba.js. Just remove lines preceded by a minus and add lines preceded by a plus, if you're not familiar with diff output.

includes/verification.php

--- release/ochiba-1.1/includes/verification.php     2005-06-17 00:38:26.000000000 -0700
+++ includes/verification.php   2007-01-25 17:55:12.000000000 -0800
@@ -8,7 +8,13 @@
 // | @version $Revision: 1.4 $
 // +----------------------------------------------------------------------+
 
-$s = $_GET["s"] or exit;
+if($_GET['new']) {
+    $s = mt_rand();
+    session_start();
+    $_SESSION['seed'] = $s;
+} else {
+    $s = $_GET["s"] or exit;
+}
 
 $code = verification($s);

js/ochiba.js:

--- release/ochiba-1.1/js/ochiba.js   2005-12-15 20:54:07.000000000 -0800
+++ js/ochiba.js        2007-01-25 18:50:55.000000000 -0800
@@ -143,6 +143,9 @@
        }
        if(me = document.getElementById("verify")) {
                me.value = "";
+        var captcha = me.nextSibling;
+        if(!captcha.src) captcha = captcha.nextSibling; 
+        addEvent(captcha,"click",function(){ this.src='verify?new='+Math.random(); });
        }
 }

Taking a quick look at that SMF code, it seems pretty easy to drop that code in. The function you need is the showCodeImage() - you could paste that into verification.php, then pass it $code. Looks like you have to also grab the fonts from them, too.

5 (edited by Kolya 2007-01-26 10:43:03)

Re: Request new captcha

Thanks, works great.
To get a "hand"-cursor when pointing at the captcha I did this:
In templates/post.tmpl SEARCH:

<td><input type="text" id="verify" name="verify" size="10"/> <img src="verify?s={seed}" alt="verify" /></td>

REPLACE

<td><div id="verifier" alt="Click for a new captcha" title="Click for a new captcha"><input type="text" id="verify" name="verify" size="10"/> <img src="verify?s={seed}" alt="verify" /></div></td>

And in css/base.css ADD:

#verifier {cursor:pointer;}

EDIT: I edited this a bit to get a tooltip when hovering over the captcha saying: "Click for a new capcha".

One more thing: It would be nice if there was a consistent number of captcha letters. Preferably configurable. :)

6

Re: Request new captcha

Hmm, having a fixed number of characters greatly increases the machine-readability of captchas, whose sole purpose to defeat machine readability, so the idea seemed ill-advised to me at the time I was working on it.

Still, it's pretty easily done. In includes/functions.php line 790:

    $code = strtoupper(substr($code,0,6));
    $code = ereg_replace("[^A-z]","",$code);

Flip these two lines around and change that "6" to whatever length you want. Also, change line 817 from

    return substr($x,0,10);

to

    return $x;

This won't guarantee 100% of the time it'll be the set length (has to do with the way the "random" strings are generated) but it should work most of the time. For better coverage, you can change the md5() call on line 811 to sha1().

7 (edited by Kolya 2007-01-26 05:18:21)

Re: Request new captcha

h3 wrote:

Hmm, having a fixed number of characters greatly increases the machine-readability of captchas, whose sole purpose to defeat machine readability, so the idea seemed ill-advised to me at the time I was working on it.

Ah, I didn't know that. Well in that case I will just leave at that.

To exclude a few letters that might cause user mistakes I added in
includes/functions.php line 792:

$code = ereg_replace("[IJLOQ]","",$code);

But you mentioned noticing errors with that?

Re: Request new captcha

I changed the code here so a tooltip notifies the user that he can change the captcha.

And I noticed that when hovering over "Post" in the menu it says: "Post a image (P)" It should be "Post an image" or "Post a message" (Since an image isn't required by default.) But what does the "(P)" mean? Maybe that's obvious for others but I don't get it.

9

Re: Request new captcha

Oh the letters in parentheses are acceskeys. So on any page, pressing alt-P (Linux) or ctrl-P (Mac) will be the same as clicking the link. (S) is for search, etc.

Re: Request new captcha

Ah, I see. Seems to do nothing on windows.