Em muitos sites as vezes é necessário implementar “captchas”, ou seja, sistemas de verificação de autenticidade do usuário que consistem em um campo para o usuário digitar os caracteres visualizados em uma imagem.

No portal DevMedia existe um tutorial bastante simples de compreender e implementar utilizando a linguagem PHP, o qual pode ser encontrado nesse link.

Porém, levando em consideração as principais regras de acessibilidade para páginas web, percebemos que para deficientes visuais esse sistema pode se tornar bastante complicado e até mesmo impossível de utilizar. Para suprir essa necessidade, o autor Fernando Samboni publicou um artigo em seu blog onde ensina a fazer um captcha com áudio.

Neste artigo será apresentada uma implementação conjunta das duas soluções citadas, buscando aproveitar o melhor de cada um dos códigos.

Faremos 3 arquivos em PHP, precisaremos também de um arquivo de fonte, uma pasta com ícones e uma pasta com áudio de todas as letras do alfabeto e números de 0 a 9:

  • classcaptcha.php: Contém a classe captcha, que cria a imagem, o arquivo de áudio, valida o código digitado.
  • index.php: Contém o formulário com o Captcha e a caixa de texto para validação. Também valida, utilizando-se de uma função da classe captcha e mostra a mensagem de sucesso ou erro.
  • saida.php: cria a imagem ou o arquivo de áudio, de acordo com parâmetro enviado.

<?php

class Captcha {
  var $palavra; // Variável que receberá o captcha
  var $entrada; // Variável que receberá o captcha digitado
  var $validado; //Variável que sinalizará se o captcha é valido ou não
  var $largura; // Definida no arquivo saída.php
  var $altura; // Definida no arquivo saída.php
  var $tamanho_fonte; // Definida no arquivo saída.php
  var $quantidade_letras; // Definida no arquivo saída.php
  
  function Captcha() {
    if (!isset($_SESSION)) session_start(); 
    // Inicia uma nova sessão caso não tenha nenhuma já iniciada
  }

  function check($palavra) {
    $this->entrada = $palavra; //Recebe a palavra digitada
    $this->validar(); // Chama a função validar
    return $this->validado; // Retorna se é valido ou não (true or false)
  }
  
  function validar() { // Função que compara o código captcha digitado com o da sessão
    if ( isset($_SESSION['palavra']) && !empty($_SESSION['palavra']) ) {
      if ($_SESSION['palavra'] == strtolower(trim($this->entrada)) ) {
        $this->validado = true; 
        // Se os códigos são iguais, 
        a variável validado da classe recebe true
        $_SESSION['palavra'] = '';
      } else {
        $this->validado = false; 
        // Se são diferentes, a variável recebe false
      }
    } else {
      $this->validado = false; 
      // Se não há um código captcha na sessão, a variável recebe false
    }
  }

  function palavra() { 
  // Função que cria o código captcha
	$this->palavra = false; 
  // Atribui false à variável da classe que receberá o código captcha
  // Gera e atribui à variável da sessão o código captcha de acordo com o número de letras desejado
	$this->palavra = substr(str_shuffle("ABCDEFGHIJKLMNPOQRSTUVYXWZ0123456789"),0,
  ($this->quantidade_letras)); 
    $this->salvar(); // Chama a função que salva o código gerado na sessão
  }
  
  function getPalavra()  { 
	// Se houver um código captcha na sessão, retorna este código. Senão, retorna nada.
    if (isset($_SESSION['palavra']) && !empty($_SESSION['palavra'])) { 
      return $_SESSION['palavra'];
    } else {
      return '';
    }
  }
  
  function salvar() { // Função que salva o código captcha na sessão
    $_SESSION['palavra'] = strtolower($this->palavra);
  }

  function show() {
    header("Expires: Sun, 1 Jan 2000 12:00:00 GMT");
    header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
    header("Cache-Control: no-store, no-cache, must-revalidate");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Pragma: no-cache");

    $tamanho = $this->tamanho_fonte; //Passa para a variável da função uma variável da classe
    $quantidade = $this->quantidade_letras; //Passa para a variável da função uma variável da classe
    $this->palavra(); //Passa para a variável da função uma variável da classe
    $palavra = $this->palavra; //Passa para a variável da função uma variável da classe
    $altura = $this->altura; //Passa para a variável da função uma variável da classe
    $largura = $this->largura; //Passa para a variável da função uma variável da classe
    $imagem = imagecreate($largura,$altura); // define a largura e a altura da imagem
    $fonte = "arial.ttf"; //voce deve ter essa ou outra fonte de sua preferencia em sua pasta
    $preto  = imagecolorallocate($imagem,0,0,0); // define a cor preta
    $branco = imagecolorallocate($imagem,255,255,255); // define a cor branca
	
	for($i = 1; $i <= $quantidade; $i++){ 
	// atribui as letras a imagem
	imagettftext($imagem,$tamanho,rand(-25,25),(($tamanho + 10)*$i),
  ($tamanho + 10),$branco,$fonte,substr($palavra,($i-1),1)); 
	}
	imagejpeg($imagem); // gera a imagem
	imagedestroy($imagem); // limpa a imagem da memoria
  }

  function getAudibleCode() {
    $letras = array(); // Cria um array
    $palavra = $this->getPalavra(); 
    // A variável recebe um valor retornado pela função getPalavra
	
	if ($palavra == '') { // Verifica se há um código captcha
      $this->palavra(); // Se não houver, chamamos a função palavra, 
      que cria um código captcha
      $palavra = $this->getPalavra(); 
      // A variável recebe um valor retornado pela função getPalavra
    }

    for($i = 0; $i < strlen($palavra); ++$i) { 
    // Atribui uma letra do código captcha para cada item do array
      $letras[] = $palavra{$i};
    }

    return $this->gerarWAV($letras); 
    // Chama a função que gera o arquivo de som de acordo com o array letras
  }

  function gerarWAV($letras) { // Função que gera e retorna o arquivo de som
    $first = true; // use first file to write the header...
    $data_len    = 0;
    $files       = array();
    $out_data    = '';
	$audio_path = './audio/';

    foreach ($letras as $letra) {
      $filename = $audio_path . strtoupper($letra) . '.wav';

      $fp = fopen($filename, 'rb');

      $file = array();

      $data = fread($fp, filesize($filename)); // read file in

      $header = substr($data, 0, 36);
      $body   = substr($data, 44);


      $data = unpack('NChunkID/VChunkSize/NFormat/NSubChunk1ID/
      VSubChunk1Size/vAudioFormat/vNumChannels/VSampleRate/
      VByteRate/vBlockAlign/vBitsPerSample', $header);

      $file['sub_chunk1_id']   = $data['SubChunk1ID'];
      $file['bits_per_sample'] = $data['BitsPerSample'];
      $file['channels']        = $data['NumChannels'];
      $file['format']          = $data['AudioFormat'];
      $file['sample_rate']     = $data['SampleRate'];
      $file['size']            = $data['ChunkSize'] + 8;
      $file['data']            = $body;

      if ( ($p = strpos($file['data'], 'LIST')) !== false) {
        // If the LIST data is not at the end of the file, 
        this will probably break your sound file
        $info         = substr($file['data'], $p + 4, 8);
        $data         = unpack('Vlength/Vjunk', $info);
        $file['data'] = substr($file['data'], 0, $p);
        $file['size'] = $file['size'] - (strlen($file['data']) - $p);
      }

      $files[] = $file;
      $data    = null;
      $header  = null;
      $body    = null;

      $data_len += strlen($file['data']);

      fclose($fp);
    }

    $out_data = '';
    for($i = 0; $i < sizeof($files); ++$i) {
      if ($i == 0) { // output header
        $out_data .= pack('C4VC8', ord('R'), ord('I'), ord('F'), 
        ord('F'), $data_len + 36, ord('W'), ord('A'), ord('V'), 
        ord('E'), ord('f'), ord('m'), ord('t'), ord(' '));

        $out_data .= pack('VvvVVvv',
            16,
            $files[$i]['format'],
            $files[$i]['channels'],
            $files[$i]['sample_rate'],
            $files[$i]['sample_rate'] * (($files[$i]['bits_per_sample'] * 
            $files[$i]['channels']) / 8),
            ($files[$i]['bits_per_sample'] * $files[$i]['channels']) / 8,
            $files[$i]['bits_per_sample'] );

        $out_data .= pack('C4', ord('d'), ord('a'), ord('t'), ord('a'));

        $out_data .= pack('V', $data_len);
      }

      $out_data .= $files[$i]['data'];
    }

    return $out_data;
  }
}
Listagem 1. Arquivo classcaptcha.php

Neste arquivo, temos a classe Captcha que, por sua vez, possui várias variáveis e funções. Vamos listar variáveis e funções e ver uma explicação rápida do que cada uma faz.

Variáveis.

  • $palavra - Receberá o código captcha gerado pela função palavra().
  • $entrada - Receberá o código captcha digitado que será validado na função check($palavra).
  • $validado - Sinalizará se o código captcha checado na função check($palavra) é valido ou não.
  • $largura - Variável que recebe o valor da largura da imagem, definida no arquivo saida.php
  • $altura - Variável que recebe o valor da altura da imagem, definida no arquivo saida.php
  • $tamanho_fonte - Variável que recebe o valor do tamanho da fonte a ser utilizada na imagem, definida no arquivo saida.php
  • $quantidade_letras - Variável que recebe o número de caracteres a serem utilizados na imagem, definida no arquivo saida.php

Estas variáveis estão declaradas fora das funções contidas na classe Captcha porque são utilizadas por mais de uma função ou porque são definidas em algum outro arquivo que chama a classe, que é o caso das variáveis que são utilizadas na construção da imagem e são definidas no arquivo saida.php.

Funções:

  • function Captcha() – Inicia uma sessão caso não haja nenhuma já iniciada.
  • function check() – é chamada quando o formulário presente no arquivo index.php é enviado. Ela recebe o código digitado, guarda ele na variável $entrada, chama a função validar() e retorna a variável $validado.
  • function validar() – é chamada na função check(). Recebe o código captcha enviado pelo formulário e compara com o código gerado e salvo na sessão. Atribui true (se o código digitado for válido) ou false (se o código estiver incorreto) à variável $validado, que será retornada pela mesma função check().
  • function palavra() – é chamada na função getAudibleCode(), caso a função getPalavra() não encontre um código captcha salvo na sessão, e também na função show(). Esta função gera um código captcha de acordo com a quantidade de caracteres que o usuário deseja e chama a função salvar() para salvar este código na sessão.
  • function getPalavra() – é chamada na função getAudibleCode(). Esta função verifica se há um código captcha salvo na sessão. Se houver, ela retorna este código. Se não houver, retorna uma string vazia.
  • function salvar() – é chamada na função palavra() para salvar o código captcha gerado por esta função na sessão.
  • function show() – é chamada no arquivo saida.php, depois de estabelecidos a largura, altura, quantidade de caracteres e tamanho da fonte. Utiliza-se destes dados para gerar uma imagem com um código captcha, código este gerado pela função palavra() chamada dentro desta função.
  • function getAudibleCode() – é chamada no arquivo saída.php. Ela chama a função getPalavra() para verificar se há um código captcha salvo na sessão. Se houver, utiliza-se deste código. Se não houver, chama a função palavra() para gerar um código e salvá-lo na sessão e chama a função getPalavra() novamente para obter o código, que agora temos certeza que existe. Depois, passa este código para um array e chama a função gerarWav(), passando o array com o código captcha como parâmetro.
  • function gerarWAV() – é retornada pela função getAudibleCode(). Recebe desta função o array $letras. Utiliza-se do caminho presente na variável $audio_path para acessar os arquivos de áudio. Utiliza um loop (foreach) para salvar no array $files o áudio correspondente à cada letra presente no array $letras . Depois, utiliza outro loop para gerar o arquivo de áudio.

Vimos até aqui as variáveis e funções presentes na classe captcha. Para utilizar suas variáveis e funções em um arquivo diferente daquele que contém a classe, primeiro precisamos incluir o arquivo da classe no nosso arquivo externo (include("classcaptcha.php");). Depois, precisamos associá-la a uma variável, como visto no arquivo index.php ou saida.php ($img = new Captcha();). Para atribuir um valor a uma variável global da classe (aquelas que estão declaradas fora das funções), utilizamos o nome da variável à qual a classe foi associada em nosso arquivo externo seguido dos caracteres "->" e o nome da variável, como visto no arquivo saida.php, onde atribuímos valores a algumas variáveis relacionadas à imagem ($img->altura = 50;). Para chamar uma função desta classe no arquivo externo, o processo é o mesmo, sempre não se esquecendo de abrir e fechar parênteses depois do nome da função. Caso a função necessite de parâmetro(s), passamos este(s) entre os parênteses (sem parâmetro: $img->show(); com parâmetro: $img->check($_POST['code']);). Para utilizarmos as variáveis e funções dentro do arquivo da classe, trocamos o nome da variável associada à classe dos exemplos anteriores por $this, que representa o arquivo que contém as variáveis e funções (Variável: $this->entrada; Função sem parâmetro: $this->salvar(); Função com parâmetro: $this->gerarWAV($letras);).

Fora a adaptação do script para uma classe, vamos às modificações feitas em relação ao código do Darlan.

Primeira modificação


imagettftext($imagem,
$tamanho_fonte,rand(-25,25),
($tamanho_fonte*$i),
($tamanho_fonte + 10),
$branco,
$fonte,
substr($palavra,($i-1),1));
Listagem 2. Primeiro trecho do código original

imagettftext($imagem,
$tamanho,
rand(-25,25),
(($tamanho + 10)*$i),
($tamanho + 10),
$branco,
$fonte,
substr($palavra,($i-1),1));
Listagem 3. Primeiro trecho modificado

Notem que, na parte do código referente ao posicionamento x (horizontal) da letra na imagem ($tamanho_fonte*$i), soma-se 10 ao tamanho da fonte (($tamanho + 10)*$i). A explicação é simples. Dar um espaçamento maior entre as letras do código captcha na imagem, para melhor visualização de usuários com baixa visão.

Segunda Modificação


$palavra = substr(str_shuffle("AaBbCcDdEeFfGgHhIiJjKkLlMmNnPpQqRrSsTtUuVvYyXxWwZz23456789"),
0,($quantidade_letras));
Listagem 4. Segundo trecho de código original

$this->palavra = substr(str_shuffle("ABCDEFGHIJKLMNPOQRSTUVYXWZ0123456789"),
0,($this->quantidade_letras));
Listagem 5. Segundo trecho

Notem que, no código original, há letras maiúsculas e minúsculas juntas e não estão presentes o número “1” e a letra “O”, provavelmente pelo motivo de causarem confusão com o número “0” e as letras “I” maiúscula e “L” minúscula. A letra e número ausentes foram adicionados ao conjunto de caracteres disponível para o código. Em contrapartida, as letras minúsculas foram retiradas do mesmo conjunto, por dois motivos simples. O primeiro motivo é que usuários com baixa visão costumam aprender a ler e escrever com letra bastão, ou maiúscula. O segundo, é que no arquivo de áudio não será especificado ao usuário se a letra é maiúscula ou minúscula. Portanto, o código é gerado com letras maiúsculas, mas na parte que salva o código na sessão, as letras são passadas para minúsculo.


function salvar() {
    $_SESSION['palavra'] = strtolower($this->palavra);
  }
Listagem 6. Convertendo letras para minúsculo

Da mesma forma, quando é feita a validação, a comparação entre o código digitado e o gerado, o código digitado também é passado para minúsculo, por não haver distinção entre maiúsculo e minúsculo no arquivo de áudio.


function validar() { // Função que compara o código captcha digitado com o da sessão
  if ( isset($_SESSION['palavra']) && !empty($_SESSION['palavra']) ) {
    if ($_SESSION['palavra'] == strtolower(trim($this->entrada)) ) {
      $this->validado = true
      $_SESSION['palavra'] = '';
    } else {
      $this->validado = false; 
    }
  } else {
    $this->validado = false;
  }
}
Listagem 7. Função de validação do código digitado

<?php
if (isset($_POST["code"])) { 
// Se o formulário foi submetido, entraremos nesta condição
  include("classcaptcha.php"); 
  // Inclui a classe captcha
  $img = new Captcha(); // Chamamos a classe
  $valid = $img->check($_POST['code']); 
  // Checa se o captcha é válido de acordo com o que foi submetido por formulário

  if($valid == true) echo "<a href='#' onclick='return false;'>
  <font color='#006600'><strong>Código correto!</strong>
  </font></a><br />";
  else echo "<a href='#' onclick='document.getElementById(\"code\").focus(); return false;'>
  <font color='#FF0000'><strong>Você entrou com um código inválido!
  </strong></font></a><br />";
}

?>
<form method="post" action="índex.php">
Digite o texto da imagem abaixo:<br /> 
<!-- Como endereço da imagem, indicamos o arquivo saida.php com o parâmetro referente à imagem-->
<img id="image" src="saida.php?show=true" alt="imagem"><br />
<a href="#" onclick="document.getElementById('image')
.src = 'saida.php?show=true&sid=' + Math.random(); return false">
<img src="images/refresh.png" alt="Trocar Imagem" title="Trocar Imagem"></a> 
<!-- Como link para o arquivo de áudio, indicamos o arquivo 
saida.php com o parâmetro referente ao áudio-->
<a href="saida.php?play=true">
<img src="images/audio.png" alt="Ouvir texto da imagem" 
title="Ouvir texto da imagem"></a><br />
<input type="text" name="code" id="code" />
<input type="submit" value="Validar">
</form>
Listagem 8. Arquivo index.php, dentro de body

Neste arquivo, verificamos inicialmente se existe a variável enviada pelo formulário (if (isset($_POST["code"]))). Se o formulário já foi enviado, incluímos o arquivo que contém a classe captcha (include("classcaptcha.php");), chamamos a classe ($img = new Captcha();) e, posteriormente, chamamos a função que valida o código digitado, passando como parâmetro o código submetido pelo formulário ($valid = $img->check($_POST['code']);). Se a variável $valid receber true, mostramos uma mensagem de sucesso, se receber false, uma mensagem de erro. Notem que a mensagem é retornada como um link. Isso porque os leitores de tela localizam com maior facilidade links e objetos, como campos de texto, botões, etc. E retornamos a mensagem antes do formulário para o deficiente encontrar primeiro a resposta de sucesso ou erro. Se a retornarmos depois, fica confuso para o mesmo saber se o código captcha por ele digitado foi aceito ou não, pois ele encontrará o formulário primeiro, podendo digitar novamente o código, sem encontrar a mensagem de sucesso ou erro caso ele não percorra a página toda.

Depois, há o formulário, responsável por enviar o código. Notem que enviamos o formulário para o mesmo arquivo(). A explicação é simples. Para simplificar a navegação do deficiente visual, não tendo que digitar o código, ser redirecionado a uma página que informa se o código estava certo ou não. Se errado, ele teria que voltar à página que contém o formulário, digitar novamente, verificar se estava correto. Para nós, isso é uma tarefa simples, basta alguns cliques com o mouse. Para o deficiente visual não, pois ele teria que percorrer item por item da página até chegar aonde ele quer.

Continuando, dentro do formulário, temos a imagem com o código. Notem que passamos como endereço da imagem o arquivo saida.php mais um parâmetro via URL (?show=true). Isso porque, de acordo com o parâmetro passado, o arquivo saida.php gera uma imagem ou um arquivo de áudio.

Depois, temos o link responsável por trocar de imagem, se for necessário () e o link onde obteremos o arquivo de áudio (). Notem que, no link para o arquivo de áudio, também passamos como endereço o arquivo saida.php. Porém, o parâmetro passado pela URL é diferente. Através desse novo parâmetro, o arquivo saida.php gera o arquivo de áudio, em vez de imagem.

Finalmente, o campo onde digitamos o código e o botão que enviará o formulário.


<?php
   include ("classcaptcha.php"); // inclui o arquivo de classe
   $img = new Captcha(); // atribui a classe a uma variável

   if(isset($_GET['play']) && $_GET['play'] == 'true'){ 
   // Verifica se o parâmetro passado é referente ao link de áudio
   header('Content-type: audio/x-wav');
   header('Content-Disposition: attachment; name="som.wav"');
   header('Cache-Control: no-store, no-cache, must-revalidate');
   header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
   header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
   
   /* Chama a função da classe captcha responsável por verificar se há um código 
   gerado e, dentro desta função, chama a função que cria o arquivo de áudio */
   echo $img->getAudibleCode();  
   
   // Se o parâmetro passado não é referente ao áudio, verifica se é referente à imagem
   } else if(isset($_GET['show']) && $_GET['show'] == 'true') { 
   $img->quantidade_letras = 5; // Atribui a quantidade de letras
   $img->tamanho_fonte = 20; // Atribui o tamanho da fonte
   $img->largura = 200; // Atribui a largura da imagem
   $img->altura = 50; // Atribui a altura da imagem
   $img->show(); // Chama a função que gera a imagem
   }
?>
Listagem 9. Arquivo saida.php

No último arquivo, incluímos o arquivo com a classe (include ("classcaptcha.php");) e a associamos a uma variável ($img = new Captcha();). Depois, temos uma condição que verifica se o parâmetro play, passado por url, existe e se seu conteúdo é o que desejamos (if(isset($_GET['play']) && $_GET['play'] == 'true')). Se a condição for verdadeira, chamamos a função que cria o arquivo de áudio ($img->getAudibleCode();). Se a condição não for satisfeita, entramos em uma nova condição, que verifica se o parâmetro show existe e se seu conteúdo é o que desejamos. Caso a condição seja verdadeira, definimos a altura, largura, o tamanho da fonte e a quantidade de letras que a imagem com o código captcha terá ($img->quantidade_letras = 5; $img->tamanho_fonte = 20; $img->largura = 200; $img->altura = 50;) e chamamos a função que irá gerar tal imagem ($img->show();).

Uma dica para quem for utilizar um captcha, não necessariamente esse: não utilizem somente a validação do captcha via Javascript ou Ajax. Se o navegador estiver com Javascript desativado, o captcha se torna inútil.

Espero que o conteúdo aqui apresentado seja útil a aquém precisar implementar esse tipo de funcionalidade em páginas web PHP. Até a próxima oportunidade.

Referências