reCAPTCHA Component & Helper for CakePHP

By Howard Lince (Howard)
an easy-to-use component and helper set to use reCAPTCHA on your website.
This is an easy to use component and helper set for you to get reCAPTCHA(including mailhide) running on your website now. If you don't know what reCAPTCHA is please head to http://recaptcha.net/ now to find out! Please note this code is largely derived off of the library provided for reCAPTCHA - it's just repurposed for ease of use in CakePHP.

Create the following files:
Download code app/controllers/components/recaptcha.php 
app/views/helpers/recaptcha.php

code for app/controllers/components/recaptcha.php

Component Class:

Download code <?php 
 
class RecaptchaComponent extends Object {
    var 
$publickey "";
    var 
$privatekey"";
    
    var 
$is_valid false;
    var 
$error "";
    
    function 
startup(&$controller){
        
Configure::write("Recaptcha.apiServer","http://api.recaptcha.net");
        
Configure::write("Recaptcha.apiSecureServer","https://api-secure.recaptcha.net");
        
Configure::write("Recaptcha.verifyServer","api-verify.recaptcha.net");
        
Configure::write("Recaptcha.pubKey"$this->publickey);
        
Configure::write("Recaptcha.privateKey"$this->privatekey);
        
        
$this->controller =& $controller;
        
$this->controller->helpers[] = "Recaptcha";
    }
    
    function 
valid($form){
        if (isset(
$form['recaptcha_challenge_field']) && isset($form['recaptcha_response_field'])){
            if(
$this->recaptcha_check_answer(
                
$this->privatekey
                
$_SERVER["REMOTE_ADDR"],
                
$form['recaptcha_challenge_field'], 
                
$form['recaptcha_response_field']
            ) == 
0)
                return 
false;

            if (
$this->is_valid)
                return 
true;
        }
        return 
false;
    }
    
    
/**
      * Calls an HTTP POST function to verify if the user's guess was correct
      * @param string $privkey
      * @param string $remoteip
      * @param string $challenge
      * @param string $response
      * @param array $extra_params an array of extra variables to post to the server
      * @return ReCaptchaResponse
      */
    
function recaptcha_check_answer ($privkey$remoteip$challenge$response$extra_params = array()){
        if (
$privkey == null || $privkey == ''){
            die (
"To use reCAPTCHA you must get an API key from <a href='http://recaptcha.net/api/getkey'>http://recaptcha.net/api/getkey</a>");
        }
    
        if (
$remoteip == null || $remoteip == ''){
            die (
"For security reasons, you must pass the remote ip to reCAPTCHA");
        }        
            
            
//discard spam submissions
            
if ($challenge == null || strlen($challenge) == || $response == null || strlen($response) == 0) {
                    
$this->is_valid false;
                    
$this->error 'incorrect-captcha-sol';
                    return 
0;
            }

            
$response $this->_recaptcha_http_post(Configure::read('Recaptcha.verifyServer'), "/verify",
                                              array (
                                                     
'privatekey' => $privkey,
                                                     
'remoteip' => $remoteip,
                                                     
'challenge' => $challenge,
                                                     
'response' => $response
                                                     
) + $extra_params
                                              
);
    
            
$answers explode ("\n"$response [1]);
            
            if (
trim ($answers [0]) == 'true') {
                    
$this->is_valid true;
                    return 
1;
            }else{
                    
$this->is_valid false;
                    
$this->error $answers [1];
                    return 
0;
            }
    }
    
    
    
/**
     * Submits an HTTP POST to a reCAPTCHA server
     * @param string $host
     * @param string $path
     * @param array $data
     * @param int port
     * @return array response
     */
    
function _recaptcha_http_post($host$path$data$port 80) {

        
$req $this->_recaptcha_qsencode ($data);

        
$http_request  "POST $path HTTP/1.0\r\n";
        
$http_request .= "Host: $host\r\n";
        
$http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n";
        
$http_request .= "Content-Length: " strlen($req) . "\r\n";
        
$http_request .= "User-Agent: reCAPTCHA/PHP\r\n";
        
$http_request .= "\r\n";
        
$http_request .= $req;

        
$response '';
        if( 
false == ( $fs = @fsockopen($host$port$errno$errstr10) ) ) {
                die (
'Could not open socket');
        }

        
fwrite($fs$http_request);

        while ( !
feof($fs) )
                
$response .= fgets($fs1160); // One TCP-IP packet
        
fclose($fs);
        
$response explode("\r\n\r\n"$response2);

        return 
$response;
    }
    
    
    
/**
     * Encodes the given data into a query string format
     * @param $data - array of string elements to be encoded
     * @return string - encoded request
     */
    
function _recaptcha_qsencode ($data) {
        
$req "";
        foreach ( 
$data as $key => $value )
                
$req .= $key '=' urlencodestripslashes($value) ) . '&';

        
// Cut the last '&'
        
$req=substr($req,0,strlen($req)-1);
        return 
$req;
    }
}
?>




code for app/views/helpers/recaptcha.php

Helper Class:

Download code <?php 
class RecaptchaHelper extends AppHelper {
    var 
$helpers = array('form'); 
    
    function 
display_form($output_method 'return'$error null$use_ssl false){
        
$data $this->__form(Configure::read("Recaptcha.pubKey"),$error,$use_ssl);
        if(
$output_method == "echo")
            echo 
$data;
        else
            return 
$data;
    }
    
    function 
hide_mail($email '',$output_method 'return'){
        
$data $this->recaptcha_mailhide_html(Configure::read('Recaptcha.pubKey'), Configure::read('Recaptcha.privateKey'), $email);
        if(
$output_method == "echo")
            echo 
$data;
        else
            return 
$data;
    }
    
    
/**
     * Gets the challenge HTML (javascript and non-javascript version).
     * This is called from the browser, and the resulting reCAPTCHA HTML widget
     * is embedded within the HTML form it was called from.
     * @param string $pubkey A public key for reCAPTCHA
     * @param string $error The error given by reCAPTCHA (optional, default is null)
     * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false)
    
     * @return string - The HTML to be embedded in the user's form.
     */
    
function __form($pubkey$error null$use_ssl false){
        if (
$pubkey == null || $pubkey == '') {
            die (
"To use reCAPTCHA you must get an API key from <a href='http://recaptcha.net/api/getkey'>http://recaptcha.net/api/getkey</a>");
        }
        
        if (
$use_ssl) {
                    
$server Configure::read('Recaptcha.apiSecureServer');
            } else {
                    
$server Configure::read('Recaptcha.apiServer');
            }
    
            
$errorpart "";
            if (
$error) {
               
$errorpart "&amp;error=" $error;
            }
            return 
'<script type="text/javascript" src="'$server '/challenge?k=' $pubkey $errorpart '"></script>
    
        <noscript>
              <iframe src="'
$server '/noscript?k=' $pubkey $errorpart '" height="300" width="500" frameborder="0"></iframe><br/>
                  <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
                <input type="hidden" name="recaptcha_response_field" value="manual_challenge"/>
              <input type="hidden" name="recaptcha_response_field" value="manual_challenge"/>
        </noscript>'
;
    }

    
/* Mailhide related code */
    
function _recaptcha_aes_encrypt($val,$ky) {
        if (! 
function_exists ("mcrypt_encrypt")) {
            die (
"To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed.");
        }
        
$mode=MCRYPT_MODE_CBC;   
        
$enc=MCRYPT_RIJNDAEL_128;
        
$val=$this->_recaptcha_aes_pad($val);
        return 
mcrypt_encrypt($enc$ky$val$mode"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
    }
    
    function 
_recaptcha_mailhide_urlbase64 ($x) {
        return 
strtr(base64_encode ($x), '+/''-_');
    }
    
    
/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */
    
function recaptcha_mailhide_url($pubkey$privkey$email) {
        if (
$pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) {
            die (
"To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " .
                 
"you can do so at <a href='http://mailhide.recaptcha.net/apikey'>http://mailhide.recaptcha.net/apikey</a>");
        }
        
    
        
$ky pack('H*'$privkey);
        
$cryptmail $this->_recaptcha_aes_encrypt ($email$ky);
        
        return 
"http://mailhide.recaptcha.net/d?k=" $pubkey "&c=" $this->_recaptcha_mailhide_urlbase64 ($cryptmail);
    }
    
    
/**
     * gets the parts of the email to expose to the user.
     * eg, given johndoe@example,com return ["john", "example.com"].
     * the email is then displayed as john...@example.com
     */
    
function _recaptcha_mailhide_email_parts ($email) {
        
$arr preg_split("/@/"$email );
    
        if (
strlen ($arr[0]) <= 4) {
            
$arr[0] = substr ($arr[0], 01);
        } else if (
strlen ($arr[0]) <= 6) {
            
$arr[0] = substr ($arr[0], 03);
        } else {
            
$arr[0] = substr ($arr[0], 04);
        }
        return 
$arr;
    }
    
    
/**
     * Gets html to display an email address given a public an private key.
     * to get a key, go to:
     *
     * http://mailhide.recaptcha.net/apikey
     */
    
function recaptcha_mailhide_html($pubkey$privkey$email) {
        
$emailparts $this->_recaptcha_mailhide_email_parts ($email);
        
$url $this->recaptcha_mailhide_url ($pubkey$privkey$email);
        
        return 
htmlentities($emailparts[0]) . "<a href='" htmlentities ($url) .
            
"' onclick=\"window.open('" htmlentities ($url) . "', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;\" title=\"Reveal this e-mail address\">...</a>@" htmlentities ($emailparts [1]);
    
    }
        

}
?>

To use the reCAPTCHA system load the component

Controller Class:

Download code <?php 
var $components = array('Recaptcha'); 
?>

After you save the component and helper and initiate them, set your public & private keys in "beforeFilter" of your controller to the ones you received when you signed up on the reCAPTCHA website.

Controller Class:

Download code <?php 
function beforeFilter(){
   
$this->Recaptcha->publickey "";
   
$this->Recaptcha->privatekey "";
}
?>

in the view, the helper can be used to verify form submissions or hide your e-mail addresses (NOTE: mcrypt is required for this.)

Controller Class:

Download code <?php 
//create the reCAPTCHA form.
 
$recaptcha->display_form('echo')

//hide an e-mail address
 
$recaptcha->hide_mail("someuser@somdomain.tld",'echo');
?>


in the controller to verify a form submission using reCAPTCHA do the following.

Controller Class:

Download code <?php 
if($this->Recaptcha->valid($this->params['form']))
  
//submission is valid!
else
  
//invalid reCAPTCHA entry.
?>

I hope you enjoy it, this is the first component / helper (let alone helper) pair I've written.

 

Comments 816

CakePHP Team Comments Author Comments
 

Question

1 $this->params['form'] does not get populated?

When I add die(pr($this->params)) inside of my controller I see that it is empty so $recaptcha->recaptcha_check_answer() never gets executed. Recaptcha is included in both $this->helper and $this->component and the form displays without issue. I'm also not seeing any sign of the challenge response in $this->data.

Any ideas where I might be going wrong or why $this->params['form'] refuses to be populated with the captcha response? Any help would be greatly appreciated.


*** update ***
It seems like this may be a problem with browsers? Everything works beautifully in IE 8. $this->params[form] is populated appropriately and I can validate the captcha response.

Unfortunately it fails in Firefox 3.0.4 (safe mode), Safari 3.2, and Opera 9.62. At least it does for me. Anyone else validate or have a solution?
Posted Nov 23, 2008 by Glen Smith
 

Comment

2 Place of the element

In case someone has problems (like I): In the view the element must be called after the form->create() is executed.
Posted Dec 6, 2008 by cakemanager
 

Comment

3 Same problem!

Any ideas where I might be going wrong or why $this->params['form'] refuses to be populated with the captcha response?
I have exactly the same problem, though it has worked for some time, and I don't know why now it doesn't work anymore (and I really don't understand why in the ___form() function there are two inputs with the same name?)

**EDIT**
you need valid XHTML pages for this to work. I've modified my page according to the W3C validator, and now it works well!
Posted Jan 11, 2009 by Arthur
 

Question

4 Illegal hex Digit

When I load a view that I created with the code I get the following error many times. The only variation is digit itself... that changes with the characters of my private key.

Warning (2): pack() [function.pack]: Type H: illegal hex digit L [APP/views/helpers/recaptcha.php, line 79]
I must be missing a step but I can't figure it out. Can anyone help?
Posted Feb 17, 2009 by James Read
 

Comment

5 mcrypt needed

You need to have mcrypt installed as stated above to use some of the helpers functions. Hope this helps!

When I load a view that I created with the code I get the following error many times. The only variation is digit itself... that changes with the characters of my private key.

Warning (2): pack() [function.pack]: Type H: illegal hex digit L [APP/views/helpers/recaptcha.php, line 79]
I must be missing a step but I can't figure it out. Can anyone help?
Posted Feb 27, 2009 by Howard Lince
 

Comment

6 All comments are approved

I believe I might be having the same problems as the users above but it seems all my comments are being approved.

I've used the block


<?php 
if($this->Recaptcha->valid($this->params['form']))
  
//submission is valid!
else
  
//invalid reCAPTCHA entry.
?> 

Any workarounds yet?
Posted Mar 19, 2009 by Sean Culver
 

Comment

7 Security disabledFields

If you're using Security Component, don't forget to add the reCAPTCHA fields to your disabledFields:


$this->Security->disabledFields = array('recaptcha_challenge_field', 'recaptcha_response_field');
Posted Mar 20, 2009 by Joseph Bartlett
 

Comment

8 Typo in tutorial?

Great tutorial. However I think you have a small typo - the second-to-last code block actually goes in the View, but it's labeled "Controller class". That tripped me up until I realized it.

Thanks again!
Posted Apr 14, 2009 by Rolf Kaiser
 

Comment

9 reCaptcha not getting populated?

I believe I might be having the same problems as the users above but it seems all my comments are being approved.

I've used the block


<?php 
if($this->Recaptcha->valid($this->params['form']))
  
//submission is valid!
else
  
//invalid reCAPTCHA entry.
?> 

Any workarounds yet?

Hi here's a solution you can try, worked for me!

Check your corresponding view, app/views/users/register.ctp (in my case)

And make sure the code that displays the reCaptcha form:

$recaptcha->display_form('echo')
is within your form,NOT outside example:

<?php 
    
echo $form->create('User', array('url' => '/users/register'));
    
    
$recaptcha->display_form('echo');

    echo 
$form->input('username');
    echo 
$form->input('password');
    echo 
$form->input('email');
    echo 
$form->input('first_name');
    echo 
$form->input('last_name');
    
    echo 
$form->end('Register'); 
 
?>
Hope this helps! :)
Posted Jun 24, 2009 by Sahan
 

Comment

10 Hex Errors!

When I load a view that I created with the code I get the following error many times. The only variation is digit itself... that changes with the characters of my private key.

Warning (2): pack() [function.pack]: Type H: illegal hex digit L [APP/views/helpers/recaptcha.php, line 79]
I must be missing a step but I can't figure it out. Can anyone help?

Hi try this!

It seems one of the functions in your "/helpers/recaptcha.php" is missing, so add this function in there!


function _recaptcha_aes_pad($val) 
  {
    $block_size = 16;
    $numpad = $block_size - (strlen ($val) % $block_size); 
    return str_pad($val, strlen ($val) + $numpad, chr($numpad));
  }

Then... find the function hide_mail in the same page and add in the Public and Private Keys given for MailHide as variables, see below..


function hide_mail($email = '',$output_method = 'return'){
        
        $pubkey = "01URu9mx0VAUTVkg3Eb6Bn1Q==";
        $privkey = "8AE48922E14FB61F90BDDBE4C61D70E5";
        
        $data = $this->recaptcha_mailhide_html($pubkey, $privkey, $email);
        if($output_method == "echo")
            echo $data;
        else
            return $data;
    }

That solved the problem for me! Cheers! :)
Posted Jun 25, 2009 by Sahan