Para entrar en materia quiesiera mencionar que Prestashop a día de hoy (versión 1.6) carece de un módulo gratuito que integre un captcha bien para dicha versión (SlideCaptcha a mí por lo menos no me dejaba configurarlo una vez instalado manualmente además) y los módulos que no son gratuitos tienen un precio que los programadores como yo nos negamos a pagar cuando puede hacerlo uno mismo.
No hace falta decir que hoy en día un formulario sin un captcha es «una diana de 20m» para los ataques masivos de mails, con lo que se me hace imprescindible implementar un captcha en la tienda online. Y la forma más rápida es utilizando el captcha de Google: reCaptcha, que está al servicio del usuario.

Pues bien, me he liado tanto pensado que era más dificil que no me he dado cuenta de que en realidad  es más sencillo de lo que parecía, mucho más sencillo. Empecemos con el ejemplo del formulario de contacto, aunque se puede utilizar también en el resto de formularios (de registro, de login, etc…).

1º. Vamos a la página de reCaptcha y ponemos una etiqueta que equivale a un título e introducimos el dominio del captcha, en el caso de querer hacerlo para esta web sería: ivanros.com

2º Seguimos los pasos de Google para implementarlo.  Copiamos este código en el archivo header.tpl que encontrarás en la carpeta raíz del servidor:
<script src='https://www.google.com/recaptcha/api.js'></script>
Y copiamos este código en /themes/[TU TEMA ACTIVO*]/contact-form.tpl (que es el archivo que muestra el html del formulario de contacto)
<div class="g-recaptcha" data-sitekey="[aquí la clave del sitio que te facilita Google]"></div>
*El tema activo por defecto es: default-bootstrap.

Hasta aquí tenemos la parte que Google te deja bien clarito, pero ahora los que como yo todavía nos queda mucho por aprender nos quedamos atascados hasta que de repente nos viene la inspiración y entre eso y algunos comentarios de soluciones en anteriores versiones de Prestashop damos con la solución.

3º Para terminar tenemos que ir a /controllers/front/ContactController.php (que es el archivo que realiza las comprobaciones del formulario de contacto) y donde pone

	
		
if (!($from = trim(Tools::getValue('from'))) || !Validate::isEmail($from))
	$this->errors[] = Tools::displayError('Invalid email address.');
else if (!$message)
	$this->errors[] = Tools::displayError('The message cannot be blank.');
else if (!Validate::isCleanHtml($message))
	$this->errors[] = Tools::displayError('Invalid message');
else if (!($id_contact = (int)(Tools::getValue('id_contact'))) || !(Validate::isLoadedObject($contact = new Contact($id_contact, $this->context->language->id))))
	$this->errors[] = Tools::displayError('Please select a subject from the list provided. ');
else if (!empty($fileAttachment['name']) && $fileAttachment['error'] != 0)
	$this->errors[] = Tools::displayError('An error occurred during the file-upload process.');
else if (!empty($fileAttachment['name']) && !in_array( Tools::strtolower(substr($fileAttachment['name'], -4)), $extension) && !in_array( Tools::strtolower(substr($fileAttachment['name'], -5)), $extension))
	$this->errors[] = Tools::displayError('Bad file extension');
else
{

Añadimos esto antes del último else:

else if (!($gcaptcha = (int)(Tools::getValue('g-recaptcha-response'))))
	$this->errors[] = Tools::displayError('Captcha no verificado');

Lo que hacemos con esto es recoger en $gcaptcha el valor de g-recaptcha-response que es el resultado que envía Google de la operación del captcha y si no tiene valor añadimos al error que muestre «Captcha no verificado».

Quedando el código así:

	
		
if (!($from = trim(Tools::getValue('from'))) || !Validate::isEmail($from))
	$this->errors[] = Tools::displayError('Invalid email address.');
else if (!$message)
	$this->errors[] = Tools::displayError('The message cannot be blank.');
else if (!Validate::isCleanHtml($message))
	$this->errors[] = Tools::displayError('Invalid message');
else if (!($id_contact = (int)(Tools::getValue('id_contact'))) || !(Validate::isLoadedObject($contact = new Contact($id_contact, $this->context->language->id))))
	$this->errors[] = Tools::displayError('Please select a subject from the list provided. ');
else if (!empty($fileAttachment['name']) && $fileAttachment['error'] != 0)
	$this->errors[] = Tools::displayError('An error occurred during the file-upload process.');
else if (!empty($fileAttachment['name']) && !in_array( Tools::strtolower(substr($fileAttachment['name'], -4)), $extension) && !in_array( Tools::strtolower(substr($fileAttachment['name'], -5)), $extension))
	$this->errors[] = Tools::displayError('Bad file extension');
else if (!($gcaptcha = (int)(Tools::getValue('g-recaptcha-response'))))
	$this->errors[] = Tools::displayError('Captcha no verificado');
else
{

Puedes comprobar ahora a realizar rellenar el formulario de contacto que si no escribes el captcha te saldrá «Captcha no verificado» y si lo rellenas todo correctamente se enviará el correo sin problemas.

Y eso es todo. Fácil, ¿no?. Pues me ha llevado mucho más tiempo pensarlo que escribirlo. Por lo que os lo facilito al resto de comunidad internauta, puesto que seguro no seré ni el primero ni el último con este dilema.

Un saludo!
Iván Ros

 

EDITO: lo mejor es ponerlo en un override. Creamos un archivo llamado ContactController.php dentro de /override/controllers/front/ con este código:

<?php
/*
* 2015 Iván Ros
*
* RECAPTCHA
*/

class ContactController extends ContactControllerCore {
	
	/**
	 * Sobreescritura del método postProcess para añadir el requisito del reCaptcha
	 */
	public function postProcess()
	{
		if (Tools::isSubmit('submitMessage'))
		{
			$extension = array('.txt', '.rtf', '.doc', '.docx', '.pdf', '.zip', '.png', '.jpeg', '.gif', '.jpg');
			$file_attachment = Tools::fileAttachment('fileUpload');
			$message = Tools::getValue('message'); // Html entities is not usefull, iscleanHtml check there is no bad html tags.
			$seguir = true;
			if (!($from = trim(Tools::getValue('from'))) || !Validate::isEmail($from))
				$this->errors[] = Tools::displayError('Invalid email address.');
				$seguir = false;
			if (!$message)
				$this->errors[] = Tools::displayError('The message cannot be blank.');
				$seguir = false;
			if (!Validate::isCleanHtml($message))
				$this->errors[] = Tools::displayError('Invalid message');
				$seguir = false;
			if (!($id_contact = (int)Tools::getValue('id_contact')) || !(Validate::isLoadedObject($contact = new Contact($id_contact, $this->context->language->id))))
				$this->errors[] = Tools::displayError('Please select a subject from the list provided. ');
				$seguir = false;
			if (!empty($file_attachment['name']) && $file_attachment['error'] != 0)
				$this->errors[] = Tools::displayError('An error occurred during the file-upload process.');
				$seguir = false;
			if (!empty($file_attachment['name']) && !in_array(Tools::strtolower(substr($file_attachment['name'], -4)), $extension) && !in_array(Tools::strtolower(substr($file_attachment['name'], -5)), $extension))
				$this->errors[] = Tools::displayError('Bad file extension');
				$seguir = false;
			// EDITO IVÁN - Añadimos el reCaptcha
			if (!($gcaptcha = (int)(Tools::getValue('g-recaptcha-response'))))
				$this->errors[] = Tools::displayError('Captcha no verificado');
				$seguir = false;
			if ($seguir == true)
			{
				$customer = $this->context->customer;
				if (!$customer->id)
					$customer->getByEmail($from);

				$id_order = (int)$this->getOrder();

				if (!((
						($id_customer_thread = (int)Tools::getValue('id_customer_thread'))
						&& (int)Db::getInstance()->getValue('
						SELECT cm.id_customer_thread FROM '._DB_PREFIX_.'customer_thread cm
						WHERE cm.id_customer_thread = '.(int)$id_customer_thread.' AND cm.id_shop = '.(int)$this->context->shop->id.' AND token = \''.pSQL(Tools::getValue('token')).'\'')
					) || (
						$id_customer_thread = CustomerThread::getIdCustomerThreadByEmailAndIdOrder($from, $id_order)
					)))
				{
					$fields = Db::getInstance()->executeS('
					SELECT cm.id_customer_thread, cm.id_contact, cm.id_customer, cm.id_order, cm.id_product, cm.email
					FROM '._DB_PREFIX_.'customer_thread cm
					WHERE email = \''.pSQL($from).'\' AND cm.id_shop = '.(int)$this->context->shop->id.' AND ('.
						($customer->id ? 'id_customer = '.(int)$customer->id.' OR ' : '').'
						id_order = '.(int)$id_order.')');
					$score = 0;
					foreach ($fields as $key => $row)
					{
						$tmp = 0;
						if ((int)$row['id_customer'] && $row['id_customer'] != $customer->id && $row['email'] != $from)
							continue;
						if ($row['id_order'] != 0 && $id_order != $row['id_order'])
							continue;
						if ($row['email'] == $from)
							$tmp += 4;
						if ($row['id_contact'] == $id_contact)
							$tmp++;
						if (Tools::getValue('id_product') != 0 && $row['id_product'] == Tools::getValue('id_product'))
							$tmp += 2;
						if ($tmp >= 5 && $tmp >= $score)
						{
							$score = $tmp;
							$id_customer_thread = $row['id_customer_thread'];
						}
					}
				}
				$old_message = Db::getInstance()->getValue('
					SELECT cm.message FROM '._DB_PREFIX_.'customer_message cm
					LEFT JOIN '._DB_PREFIX_.'customer_thread cc on (cm.id_customer_thread = cc.id_customer_thread)
					WHERE cc.id_customer_thread = '.(int)$id_customer_thread.' AND cc.id_shop = '.(int)$this->context->shop->id.'
					ORDER BY cm.date_add DESC');
				if ($old_message == $message)
				{
					$this->context->smarty->assign('alreadySent', 1);
					$contact->email = '';
					$contact->customer_service = 0;
				}

				if ($contact->customer_service)
				{
					if ((int)$id_customer_thread)
					{
						$ct = new CustomerThread($id_customer_thread);
						$ct->status = 'open';
						$ct->id_lang = (int)$this->context->language->id;
						$ct->id_contact = (int)$id_contact;
						$ct->id_order = (int)$id_order;
						if ($id_product = (int)Tools::getValue('id_product'))
							$ct->id_product = $id_product;
						$ct->update();
					}
					else
					{
						$ct = new CustomerThread();
						if (isset($customer->id))
							$ct->id_customer = (int)$customer->id;
						$ct->id_shop = (int)$this->context->shop->id;
						$ct->id_order = (int)$id_order;
						if ($id_product = (int)Tools::getValue('id_product'))
							$ct->id_product = $id_product;
						$ct->id_contact = (int)$id_contact;
						$ct->id_lang = (int)$this->context->language->id;
						$ct->email = $from;
						$ct->status = 'open';
						$ct->token = Tools::passwdGen(12);
						$ct->add();
					}

					if ($ct->id)
					{
						$cm = new CustomerMessage();
						$cm->id_customer_thread = $ct->id;
						$cm->message = $message;
						if (isset($file_attachment['rename']) && !empty($file_attachment['rename']) && rename($file_attachment['tmp_name'], _PS_UPLOAD_DIR_.basename($file_attachment['rename'])))
						{
							$cm->file_name = $file_attachment['rename'];
							@chmod(_PS_UPLOAD_DIR_.basename($file_attachment['rename']), 0664);
						}
						$cm->ip_address = (int)ip2long(Tools::getRemoteAddr());
						$cm->user_agent = $_SERVER['HTTP_USER_AGENT'];
						if (!$cm->add())
							$this->errors[] = Tools::displayError('An error occurred while sending the message.');
					}
					else
						$this->errors[] = Tools::displayError('An error occurred while sending the message.');
				}

				if (!count($this->errors))
				{
					$var_list = array(
									'{order_name}' => '-',
									'{attached_file}' => '-',
									'{message}' => Tools::nl2br(stripslashes($message)),
									'{email}' =>  $from,
									'{product_name}' => '',
								);

					if (isset($file_attachment['name']))
						$var_list['{attached_file}'] = $file_attachment['name'];

					$id_product = (int)Tools::getValue('id_product');

					if (isset($ct) && Validate::isLoadedObject($ct) && $ct->id_order)
					{
						$order = new Order((int)$ct->id_order);
						$var_list['{order_name}'] = $order->getUniqReference();
						$var_list['{id_order}'] = (int)$order->id;
					}

					if ($id_product)
					{
						$product = new Product((int)$id_product);
						if (Validate::isLoadedObject($product) && isset($product->name[Context::getContext()->language->id]))
							$var_list['{product_name}'] = $product->name[Context::getContext()->language->id];
					}

					if (empty($contact->email))
						Mail::Send($this->context->language->id, 'contact_form', ((isset($ct) && Validate::isLoadedObject($ct)) ? sprintf(Mail::l('Your message has been correctly sent #ct%1$s #tc%2$s'), $ct->id, $ct->token) : Mail::l('Your message has been correctly sent')), $var_list, $from, null, null, null, $file_attachment);
					else
					{
						if (!Mail::Send($this->context->language->id, 'contact', Mail::l('Message from contact form').' [no_sync]',
							$var_list, $contact->email, $contact->name, $from, ($customer->id ? $customer->firstname.' '.$customer->lastname : ''),
									$file_attachment) ||
								!Mail::Send($this->context->language->id, 'contact_form', ((isset($ct) && Validate::isLoadedObject($ct)) ? sprintf(Mail::l('Your message has been correctly sent #ct%1$s #tc%2$s'), $ct->id, $ct->token) : Mail::l('Your message has been correctly sent')), $var_list, $from, null, $contact->email, $contact->name, $file_attachment))
									$this->errors[] = Tools::displayError('An error occurred while sending the message.');
					}
				}

				if (count($this->errors) > 1)
					array_unique($this->errors);
				elseif (!count($this->errors))
					$this->context->smarty->assign('confirmation', 1);
			}
		}
	}
}

?>

Lo que conseguimos con esto, además de tenerlo en un override, si os dais cuenta, he quitado los elseif por if solo para que, como en el registro, muestre todos los errores de una vez, y no pare por cada error. Mucho más útil.