Hi Guys!
In my last post here, I discussed the creation of a captcha code to secure web forms using the HTML 5 Canvas tag along with JavaScript and PHP as the server side language. I loaded a custom web font using the JavaScript Font Face API to draw a random text string on the canvas with it. The random Captcha text was also refreshable in case the site visitor did not find it legible enough.
I assume that the reader knows PHP Web Forms and Sessions, CSS, HTML5 Canvas JavaScript API, JavaScript for browsers including FontFace API, Audio API and AJAX. You may read the appropriate tutorials from the web if you don’t.
So, what’s new in this post? I have used a text to speech API to let users hear audio captcha, in case the captcha is not very legible or the user has a vision problem.
How to create a random string
The following code creates a random text and initializes it into the PHP session.
<?php @session_start(); $_SESSION["captcha_hash_voice"] = substr(hash('joaat', microtime()), 0, 6); ?>
As you can see, a timestamp is created with a call to the microtime() function. It is then hashed with the JOAAT algorithm. We then extract the first six characters of this hash as our captcha string.
Creating The Web Form
We will create the HTML form or web form using HTML and CSS for coding and styling. Here it is:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style type="text/css"> #captcha_img{ display:none; } #captcha_code{ width: 180px; } #captcha{ border: solid 1px black; float:left; } #refresh_img{ cursor:pointer; } #play_audio{ cursor:pointer; } </style> <script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script> </head> <body> <h1>Canvas Captcha with Voice</h1> <?php if(isset($_SESSION["form_msg"])){ echo $_SESSION["form_msg"]; unset($_SESSION["form_msg"]); } ?> <img src="images/captcha.jpg" alt="captcha image" id="captcha_img" /> <form action="" method="POST"> <canvas id="captcha" width="218" height="60" ></canvas> <span > <img src="images/refresh.png" alt="refresh image" id="refresh_img" title="Refresh" /><br/> <img src="images/audio.png" alt="play_audio" id="play_audio" title="Play audio" /></span> <br /><br /> <input type="text" name="captcha_code" value="" id="captcha_code" maxlength="6" /> <input type="submit" name="submit" value="Submit" /> </form> <script src="script/js/script.js"></script> </body> </html>
As you can see, we declare an HTML 5 document with the DOCTYPE declaration. We fetch the latest jQuery file from https://code.jquery.com/ in the script tag in the head of the HTML 5 document we just created.
We have created an invisible image fetching the image from the path “images/captcha.jpg” relative to our document or web page. We have also used some CSS to style the text input of our web form.
Notice the call to fetch “script/js/script.js”. We will discuss this later.
The Script to which Our Form is Submitted
You will notice the script above the <form> tag. In this script, we will simply compare the value of the “captcha_code” form field (i.e. $_POST[“captcha_code”]) and the random string stored in the PHP session (i.e. $_SESSION[“captcha_hash_voice”]) when the form is submitted by the user.
The code is given below:
<?php @session_start(); if(isset($_POST["submit"]) && $_POST["submit"] == "Submit") { if($_POST["captcha_code"] != $_SESSION["captcha_hash_voice"]) { //failed $_SESSION["form_msg"] = "<span style='color:red;'>Security challenge failed!</span>"; header("Location: index.php"); exit; } else{ //passed $_SESSION["form_msg"] = "<span style='color:green;'>Security challenge passed!</span>"; header("Location: index.php"); exit; } } else{ $_SESSION["captcha_hash_voice"] = substr(hash('joaat', microtime()), 0, 6); } ?>
The Script That Renders the Captcha String on the Canvas
Perhaps you noticed the call to fetch “script/js/script.js”. Why do we need to do this?
This is needed because we have to reference the PHP session variable $_SESSION[“captcha_hash_voice”], so that we can render it on the HTML 5 canvas using JavaScript.
When the web page loads for the first time, the following code renders the captcha:
var canvasObj = document.getElementById('captcha'); // we need this to load the font var myFont = new FontFace('myFont', 'url(font/Moms_typewriter.ttf)'); myFont.load().then(function(font){ // with canvas, if this is ommited won't work document.fonts.add(font); console.log('Font loaded'); console.log(canvasObj); var captchaImage = document.getElementById("captcha_img"); var context = canvasObj.getContext("2d"); context.drawImage(captchaImage,0,0); context.font='28px myFont'; var capText = ''; jQuery.get("script/getcaptchastring.php", {'action':'fetch'}, function(response){ capText = response; var capArray = capText.split(''); drawLines(context); rotateText(context, capArray); }); });
As you can see, there are two JavaScript functions named “drawLines” and “rotateText”, which need elaboration.
The “drawLines” function draws zigzag lines in the captcha image background by obtaining a 2D graphics context of the canvas object using the line of JavaScript as follows:
var context = canvasObj.getContext("2d");
This complete JavaScript function is as follows:
function drawLines(context) { //random lines context.moveTo(0,0); context.lineTo(80, 60); context.stroke(); context.moveTo(50,0); context.lineTo(20, 60); context.stroke(); context.moveTo(58, 60); context.lineTo(100, 0); context.stroke(); context.moveTo(110, 0); context.lineTo(140, 60); context.stroke(); context.moveTo(120, 60); context.lineTo(190, 0); context.stroke(); }
The “rotateText” function renders the six characters of the split string, which was previously set in PHP session. It rotates the canvas 2D graphical context this way and that, sumultaneously rendering the individual characters of the captcha in the custom font. After finishing its job, the context is restored to its initial orientation.
function rotateText(context, capArray) { //rotated captcha context.rotate(10 * Math.PI / 180); context.fillText(capArray[0], 35, 40); context.rotate(5 * Math.PI / 180); context.fillText(capArray[1], 59, 35); context.rotate(-5 * Math.PI / 180); context.fillText(capArray[2], 79, 30); context.rotate(-10 * Math.PI / 180); context.fillText(capArray[3], 99, 30); context.rotate(10 * Math.PI / 180); context.fillText(capArray[4], 119, 30); context.rotate(-5 * Math.PI / 180); context.fillText(capArray[5], 134, 25); context.rotate(-5 * Math.PI / 180); context.restore(); }
Creating a Refreshable Captcha
In the code listing that creates our web form, notice the following line:
<img src="images/refresh.png" alt="refresh image" id="refresh_img" />
We will add a click handler to it so that captcha can be refreshed on click. We will use AJAX so as to get the PHP session variable from the server side, modify it and render it without reloading the entire page. Refreshing will change the session variable and hence the captcha string, but the logic to render the changed string remains the same.
Note that on each refresh, we need to clear the Canvas of the zigzag lines and the wavy text, so that there is no overwriting and spoiling the appearance of the captcha.
Here is the code:
//Client side code jQuery(document).ready(function() { var canvasObj = document.getElementById('captcha'); var captchaImage = document.getElementById("captcha_img"); var context = canvasObj.getContext("2d"); jQuery("#refresh_img").click(function(){ jQuery.get("script/getcaptchastring.php", {'action':'refresh'}, function(response){ var capArray = response.split(''); context.clearRect(0, 0, canvasObj.width, canvasObj.height); context.drawImage(captchaImage,0,0); context.font='28px myFont'; drawLines(context); rotateText(context, capArray); }); }); });
There is an AJAX call to “script/getcaptchastring.php” which modifies the captcha string. The following is the code listing for getcaptchastring.php:
<?php //server side code @session_start(); if(isset($_GET['action'])) { if($_GET['action'] == 'fetch') //render captcha first time { echo $_SESSION["captcha_hash_voice"]; exit(); } elseif($_GET['action'] == 'refresh') //refresh captcha on clicking refresh icon { $captcha_hash_new = substr(hash('joaat', microtime()), 0, 6); $_SESSION["captcha_hash_voice"] = $captcha_hash_new; echo $_SESSION["captcha_hash_voice"]; exit(); } } ?>
Add Audio Functionality
Now let us add voice functionality.
We will use Google’s Text To Speech API, to feed the captcha string to it and get the speech version as result.
Let us see the client-side code, which is an addition to script/js/script.js:
//audio jQuery("#play_audio").click(function(){ var audioClip = new Audio("script/playback.php?d="+(new Date()).getMilliseconds()); audioClip.load(); //load the audio clip audioClip.play(); //play the audio clip });
As you can see, we are creating an object of Audio in JavaScript. We are passing the script at the path “playback.php” to it. We are using a query string while loading playback.php so as to prevent caching while playing the sound clip with it. So that the audio clip changes as the captcha string or, in other words the $_SESSION token changes, on refreshing or reloading, for instance.
The code of playback.php is below:
<?php @session_start(); header("Content-Type: audio/mpeg"); $audio_split = str_split($_SESSION["captcha_hash_voice"]); //read letter by letter foreach($audio_split as $k=>$voice) { $voice = rawurlencode(htmlspecialchars($voice)); $audio = file_get_contents('https://translate.google.com/translate_tts?ie=UTF-8&client=gtx&q='.$voice.'&tl=en-IN'); echo $audio; } exit; ?>
The following line:
header("Content-Type: audio/mpeg");
is used to tell the browser to treat the output of playback.php as a sound or audio clip.
We feed the captcha string character by character to the text to speech API at:
‘https://translate.google.com/translate_tts?ie=UTF8&client=gtx&q=’.$voice.’&tl=en-IN’
When the speaker icon in the web form, given above, is clicked, the characters are read by the API ,one by one, so that the user can hear them distinctly. The user can then type in his answer to the captcha challenge. If the captcha is refreshed and the speaker icon is clicked, the new captcha will be read aloud for the user, letter by letter.
Conclusion
So all in all, we did the following:
- Initialized the first six characters of the hash of microtime() as a session variable in PHP
- Created a HTML 5 page with a web form and Canvas tag.
- Created a PHP script to which the form will be submitted, to check the captcha challenge.
- Created a script to render the Captcha string on the background image, with zigzag lines, with wavy characters using a custom font.
- Created Server side and client side logic to modify the session variable, return it to the client side and render it to the captcha background each time refresh image is clicked and the canvas is cleared.
- Created functionality to read aloud the captcha rendered on the screen, on clicking the speaker icon on the web form, before or after clicking the refresh icon.
The code can be downloaded here, and the demo can be found here.
I hope you find it useful, with the addition of voice functionality, since we often need to secure web forms with some kind of captcha and some users may have trouble reading the captcha.