Форма обратной связи для WordPress, без плагина. Защита от спама и полевые испытания ASCII-каптчи

Или еще раз возвращаясь к напечатанному. Сегодня поговорим о защите от спама.
В первоначально описанном способе копия был баг (промотайте в конец), из-за которого можно было легко и непринужденно загадить ящик получателя спамом, да еще и в автоматическом режиме. Отправить картинки, вирусы, или загадить чужой ящик не получится, но вот закидать «Войной и миром» ящик владельца сайта вполне таки да.
Чтобы уменьшить такую вероятность, добавим в скрипт отправки почты каптчу. Поскольку скрипт, отправляющий почту (mail.php) к компонентам WordPress не относится, то каптчу придется изобретать свою. Лично мне это оказалось даже хорошо, т.к. совсем недавно я писал о создании ASCII-каптчи, копия, и мне прямо-таки жгло показать ее работу в реальном проекте, а не только в учебных примерах. Посему ее и используем для защиты от спама.

Постановка задачи

Переписать скрипт mail.php так, чтобы он смог использовать ASCII-каптчу.

Краткое описание процесса

1. Пользователь в форме обратной связи, вводит сообщение.

2. По нажатию кнопки «Отправить» сообщение передается скрипту mail.php
3. Если сообщение было отправлено из формы, то скрипт генерирует каптчу, HTML-страницу, содержащую параметры сообщения (имя, текст, электронный адрес пользователя), ASCII-изображение каптчи, поле для ввода кода, элементы управления (кнопки) с помощью которых пользователь может ввести код, обновить код подтверждения, отправить код и сообщение.
4. Скрипт также должен обработать возможные ошибки. Если они есть, пользователю выводится соответствующее сообщение и страница с формой ввода сообщения открывается вновь.
5. Если каптча введена неверно, пользователю демонстрируется сообщение об ошибке ввода каптчи, и дается возможность повторить ввод каптчи. Информация в сообщении сохраняется.
6. Если код введен верно и другие ошибки отсутствуют, сообщение передается на заранее указанный в скрипте e-mail.

Предварительные мелочи

1. Условимся, что код каптчи будем отправлять с помощью cookie, примерно так, как это описано здесь, чтобы ненароком не поломать сессию движка WordPress.
2. Подключаем каптчу:

include ('captcha.php');

3. Пишем небольшую функцию, заменяющую перевод строки CR+LF, который по стандарту должен поступать WWW-серверу из формы на HTML-тег <br> (ниже скажу, где оно надо):

function br_repl($str)
	{
		return str_replace("\r\n","<br>",$str);
	}

Функция, формирующая форму с каптчей и отправленными данными

Формирование кода каптчи, ее отображение в виде псевдографики, отображение пользователю введенных в форме отправки сообщения данных могут несколько раз повториться при работе скрипта. Соответственно, целесообразно объединить эти действия в одну функцию, которую позже вызывать, где надо.

function createform ($name, $email, $sub, $message, $allnum, $errcaptcha)
{
...
}

Функции передаются следующие параметры:
$name (строка) — имя, которое ввел пользователь в форме отправки сообщений для связи с владельцем сайта.
$email (строка) — e-mail пользователя
$sub (строка) — тема сообщения
$message (строка) — текст сообщения
$allnum (массив строк) — массив, содержащий все ASCII-изображения цифр каптчи, копия.
$errcaptcha (логическое значение) — флаг, установленный в истину (true) означающий, что предыдущий код каптчи был введен неверно. Если флаг установлен в false, форма отображается первый раз, попыток ввода каптчи не было. См. основную часть скрипта ниже.

Внутри функции:

1. Получаем код каптчи нужной длины (функция из файла captcha.php, см. описание здесь копия):

$captchacode=getcaptchacode(6);

2. Отправляем пользователю cookie с MD5-хэшем сгенерированного кода и временем действия 5 минут.

setcookie('mycaptchamd5',md5($captchacode),time()+300);

3. Получаем псевдографическое изображение каптчи (функция из файла captcha.php):

$captcha=getpgnum($captchacode,$allnum," ",1,false,false);

4. Проверяем флаг ошибки ($errcaptcha), если он установлен, присваиваем переменной $emess строку, содержащую соответствующее сообщение:

$emess="";
if ($errcaptcha)
{
$emess="<b><font color='red'>Проверочный код введен неверно</font></b>";
}

5. Возвращаем HTML-страницу с формой, содержащей все данные, ASCII-изображение каптчи и элементы управления:

return "<html><head><meta http-equiv='Content-Type'
content='text/html; charset=utf-8'><style type='text/css'>
			TABLE {
			width: 300px; /* Ширина таблицы */
			border-collapse: collapse; /* Убираем двойные линии между ячейками */
			}
			TD, TH {
			padding: 3px; /* Поля вокруг содержимого таблицы */
			border: 1px solid gold; /* Параметры рамки */
			font: 12pt/10pt monospace;
			}
			#footer {
			position: fixed; /* Фиксированное положение */
			left: 0; bottom: 0; /* Левый нижний угол */
			padding: 10px; /* Поля вокруг текста */	
			width: 100%; /* Ширина слоя */
			}
			</style><title>Отправка сообщения</title><
/head><body bgcolor='black' text='silver'><center><h2>Отправка сообщения</h2><br>
			<b>Проверьте ваши данные и подтвердите, что вы не робот</b></center>
			
			<table align='center'>
			<tr><td>Name:</td><td>$name</td></tr>
			<tr><td>E-mail:</td><td>$email</td></tr>
			<tr><td>Subject:</td><td>$sub</td></tr>
			<tr><td colspan='2'><center>Message</center></td></tr>
			<tr><td colspan='2'>".br_repl($message)."</td></tr>
			<tr><td colspan='2'><center>Security code </center></td></tr>
	<tr><td colspan='2'><center><code><font color='lime'><pre>".$captcha."
</pre></font></code></center></td></tr>
	<tr><td colspan='2'><center>$emess</center></td></tr>".
"<tr><td colspan='2'><center><form action='".$_SERVER['PHP_SELF']."' method='POST'>
				<p><b>Введите проверочный код</b></br>
				<p><input type='text' name='captchacode'></p>
				<p>
		<input type='submit' name='checkcode' value='Проверить код'>
		<input type='submit' name='updcode' value='Обновить код'>
				</p>
				<input type='hidden' name='name' value='$name'>
				<input type='hidden' name='email' value='$email'> 
				<input type='hidden' name='sub' value='$sub'>
				<input type='hidden' name='message' value='$message'>
				<input type='hidden' name='myself' value='true'>
			</form></center></td></tr>
	<tr><td colspan='2'><center><font color='#0099FF'>Для изменения данных вернитесь в 
форму отправки с помощью кнопки 'Назад' браузера</font></center></td></tr>
			</table></body></html>
";

После заполнения всех полей и нажатия кнопки «Отправить», пользователю будет отображена следующая форма:

Если пользователь введет каптчу с ошибкой, форма будет выглядеть таким образом:

Из кода формы должно быть понятно, что каптча обрабатывается тем же самым скриптом (<form action='".$_SERVER['PHP_SELF']."' method='POST'>).

Особое внимание следует обратить на скрытые (hidden) поля формы. Они хранят данные, переданные пользователем в форме ввода сообщения во время обработки.

Смотреть здесь

Текст сообщения перед отображением обрабатывается ранее созданной функцией br_repl($message), чтобы правильно отобразить переносы строки.

Внимание! Скрытое поле myself здесь тоже не просто так, хотя его значение жестко задано в true, оно позволит в дальнейшем определить скрипту, откуда пришли обрабатываемые данные — из HTML-формы ввода сообщения или из самого скрипта после отправки кода каптчи или запроса на ее обновление.

Примечание: Средствами PHP обрабатываются две кнопки submit с разными именами. Теория тут, копия. Практика будет ниже.

Итак, с функциями закончили, приступим к написанию основного тела скрипта.

Основные переменные.

Определим основные переменные:
$err=false; //статус ошибки

Переменная-флаг. Если в процессе выполнения скрипта будет найдена необрабатываемая ошибка, то флаг будет установлен в true, и далее скрипт получит уведомление об ошибке, соответствующе его обработав.

$usermessage=""; //сообщение, выводимое пользователю

С этой переменной интересно — весь вывод в HTML будет храниться в ней. Объясняю подробнее — если в своем скрипте мы хотим использовать редирект, то всю выводимую информацию нужно показать пользователю после отправки заголовка редиректа. Поэтому мы сначала формируем вывод в отдельной переменной, и только когда нужно выводим его пользователю.

Переменные, для формирования сообщения:

$address="admin@example.org"; //адрес куда отправляем
Примечание: значение переменной заменяем на свое.

$name=""; //имя пользователя
$email=""; //обратный адрес
$sub=""; //тема
$message=""; //сообщение

Переменные для управления редиректом (автоматическим открытием следующей страницы в процессе работы скрипта):

Надо или нет производить редирект:
$redir=false; //статус редиректа

На какую страницу следует отправлять пользователя:

$rediraddr="http://tolik-punkoff.com/obratnaya-svyaz/"; //адрес редиректа пользователя после отправки сообщения
Замените адрес >http://tolik-punkoff.com/obratnaya-svyaz/ на нужный вам.

Время до редиректа в секундах:
$redirtime=3; //время до редиректа (сек)

Проверка на критические ошибки

Собственно, критических ошибок, т.е. таких, случись которые, скрипт не сможет нормально работать, в нашем случае не так много:

1. Обращение к скрипту GET-запросом. Понятно, что скрипт обрабатывает данные из HTML-формы, передающиеся ему запросом POST. Если кому-то вдруг вздумалось вести адрес скрипта напрямую, например, в адресной строке браузера, то скрипту просто нечего будет обрабатывать.
2. Отсутствие необходимых данных (имени отправителя, сообщения, обратного адреса) в запросе POST. Это может быть связано, например, с ошибкой или опечаткой в коде формы ввода сообщения
3. Ошибка функции PHP mail().

Если эти ошибки произошли, то следует вывести пользователю соответствующее сообщение и переопределить его на заданную в переменной $rediraddr страницу.
Возможную ошибку функции mail() будем обрабатывать позже, в ходе основной работы скрипта.

Примечание: Неверный ввод каптчи критической ошибкой не является, в данном случае скрипт должен просто оповестить пользователя о вводе некорректного кода и сгенерировать новый код, не производя редирект, естественно, и сохранив данные введенные в форме отправки сообщения.

Итак, проверяем запрос:

	if ((empty($_POST))||($_SERVER['REQUEST_METHOD']!='POST')) //запрос некорректный
	{
		$usermessage="Ошибка запроса POST! ";
		$err=true;
	}
	else //устанавливаем переменные
	{
		$email = (isset($_POST['email'])) ? $_POST['email'] : false;
		$name = (isset($_POST['name'])) ? $_POST['name'] : false;
		$sub = (isset($_POST['sub'])) ? $_POST['sub'] : false;
		$message = (isset($_POST['message'])) ? $_POST['message'] : false;
		$myself = (isset($_POST['myself'])) ? true : false;
		
		//проверяем заполнение полей
		if (!$name || strlen($name) < 1) //поле имени
		{
			$usermessage.="Укажите свое имя.<br> ";
			$err=true;
		}
		if (!$email || strlen($email) < 3) //поле e-mail
		{
			$usermessage.="Укажите корректный адрес электронной почты.<br> ";
			$err=true;
		}
		if (!$sub || strlen($sub) < 1) //поле темы
		{
			$usermessage.="Укажите тему обращения.<br> ";
			$err=true;
		}
		if(!$message || strlen($message) < 1) //поле сообщения
		{
			$usermessage.="Введите сообщение.<br> ";
			$err=true;
		}
	}

Если массив $_POST пуст или метод вызова скрипта не POST (элемент REQUEST_METHOD массива $_SERVER имеет значение, отличное от 'POST'), то в $usermessage записываем сообщение об ошибке и устанавливаем флаг ошибки $err в true. Иначе, устанавливаем значения необходимых переменных из соответствующих элементов массива $_POST, проверяя, есть эти элементы в массиве.

Для установки значений переменных используем тернарный оператор. Т.е. следующий код:

$email = (isset($_POST['email'])) ? $_POST['email'] : false;

В том случае, если существует (isset()) элемент 'email' массива $_POST, переменной будет присвоено значение $_POST['email'], в противном случае - логическое значение false.
Аналогично поступим и с другими данными, которые должны прийти из формы ввода сообщения, либо из формы ввода кода каптчи. Помните выше было про скрытые поля в форме ввода каптчи?

см. иллюстрацию

Исключение составляет переменная $myself, которая служит для проверки, откуда пришел запрос, от самого ли скрипта отправки сообщения, где соответствующее скрытое поле установлено в true, или в скрипт из внешней формы, где поля myself вообще быть не должно. Если вдруг оно будет во внешней форме - автор сам виноват, проверка каптчи будет безбожно глючить и срабатывать не всегда.

Далее производим проверку на корректность заполнения полей - проверяем, задана ли соответствующая переменная и какова длина строки, если <1, значит поле пустое.

if (!$name || strlen($name) < 1) //поле имени
{
	$usermessage.="Укажите свое имя.<br> ";
	$err=true;
}

Если переменная не задана, то добавляем к переменной $usermessage соответствующее сообщение пользователю и устанавливаем флаг ошибки $err.

Для переменной $email проверяется, чтобы длина строки была не менее 3 символов. Полноценная валидация e-mail адреса для такого скрипта излишество. Во-первых, потому что это само по себе тот еще геморрой, а во-вторых, пользователю перед отправкой сообщения дается возможность перепроверить введенные данные.

Вот что получается, если к скрипту обратились с помощью GET-запроса (кто-то ввел адрес скрипта в браузере напрямую, минуя форму отправки сообщения)



Или, например, если в форме ввода сообщения допущена опечатка, скажем, в названии полей для ввода имени и e-mail, или же они отсутствуют:

Вывод сообщений и редирект

Итак, наконец, мы подобрались к основной работе, к тому, для чего скрипт и предназначен - выводу и проверке кода каптчи и отправке сообщения на заданный e-mail:

     //основная работа	
	if (!$err) //делаем, если нет ошибок
	{			
        ...
        тут будет код
        ...
	}	

Но сначала уделим еще немного вопросу редиректа и вывода сообщений (форм ввода или сообщений об ошибке/успешной отправке сообщения) пользователю.

После всех предварительных проверок (на запрос POST и заполнение всех полей) проверяется флаг ошибки. Если он не установлен ($err==false), то выполняем основную работу, если он установлен, то сразу переходим далее.

В зависимости от состояния флага ошибки устанавливается флаг редиректа, причем делается это с помощью условного оператора, что позволяет установить флаг редиректа в ходе основной работы независимо от флага ошибки. Если использовать тернарный оператор, то флаг редиректа будет жестко привязан к флагу ошибки. Чтобы этого не допустить, используем обычный if:

//если ранее произошла ошибка
//уcтанавливаем флаг редиректа
if ($err)
{
   $redir = true;
} 

Теперь о самом редиректе. Во-первых, должна быть возможность установить его вручную или по наличию ошибки. Возможность установки вручную мы оставили, и воспользуемся ей. Во-вторых, и это самое главное, редирект средствами PHP должен быть выполнен раньше выдачи в браузер любых текстовых сообщений, в т.ч. и HTML-тегов, пустых строк.
Правильный и неправильный код редиректа подробно описан здесь копия.

Используя предыдущую информацию, пишем такой код:

//редирект
if ($redir)
{
	header( 'Refresh: '.$redirtime.'; url='.$rediraddr );
}

Осталось только вывести пользователю сообщение. В нашем случае это может быть:
1. Форма ввода кода каптчи
2. Форма ввода кода каптчи с ошибкой (если предыдущий код каптчи неверный).
3. Сообщение о критической ошибке (не POST-запрос, ошибка заполнения полей, ошибка функции mail())
4. Сообщение об успешно отправленном e-mail

Чтобы соответствующим образом оформить сообщение об ошибке, используем переменную-флаг $err:

//сообщение пользователю
if ($err) //об ошибке
{
	echo "<b><font color='red'>".$usermessage."</font></b></br>
	<font color='blue'>Вы будете перенаправлены обратно через ".$redirtime." 
        секунд(ы)</font>";
}
else //какое-то другое
{
	echo $usermessage;
}

Основной рабочий процесс

Приведу его код целиком, ниже дам пояснения:

if (!$err) //делаем, если нет ошибок
{			
   if ( (!isset($_COOKIE['mycaptchamd5'])) || !$myself ) //cookie не установлен, 
                                                         //каптча не введена
   {			
      //генерируем форму с каптчей и данными
      $usermessage=createform ($name,$email, $sub,$message,$allnum,false); 
   }
   else //каптча введена или нажата кнопка 'Обновить код'
   {
      if (isset($_POST['updcode'])) //обновить код
      {
         //генерируем форму с каптчей и данными
         $usermessage=createform ($name,$email, $sub,$message,$allnum,false); 
      }
			
      if (isset($_POST['checkcode'])) //проверить код
      {
         //извлекаем код, введенный пользователем в соотв. поле формы и получаем
	 //MD5-хэш
         $usercodemd5=md5(trim($_POST['captchacode'])); 	         
		 
         $gencodemd5=$_COOKIE['mycaptchamd5']; //вытаскиваем ранее сохраненный хэш
			
	   //проверка каптчи
	   if ($usercodemd5==$gencodemd5) //код введен верно
	   {
	      setcookie("mycaptchamd5","",time()-300); //удаляем cookie
             //отправка сообщения
             //формируем сообщение
             $mes = "Имя: ".$name."\n\nТема: " .$sub."\n\nСообщение: ".$message.
             "\n\n"."E-mail to answer: $email\n\n";
             //отправляем
           $send = mail ($address,$sub,$mes,
           "Content-type:text/plain; charset = UTF-8\r\nFrom:$address");
           if ($send) //сообщение успешно отправлено
              {
                  //сообщение пользователю об успехе
	          $usermessage=".<center><b><font color='blue'>
                  Сообщение успешно отправлено!<br>
	          Форма обратной связи будет открыта через ".$redirtime." 
                  секунд(ы) </center></b></font>";
                  redir=true; //устанавливаем статус редиректа
             }
              else //ошибка функции mail()
              {
                 $usermessage="Внутренняя ошибка при отправке сообщения! :(";
                 $err=true;						
              }
        }
        else //код неправильный
        {
           //генерируем форму с каптчей и данными
           $usermessage=createform ($name,$email, $sub,$message,$allnum,true); 
        }
      }
    }
}

Итак, если после проверки того, что запрос POST и проверки полей POST-запроса, ошибок не обнаружено, делаем основную работу по проверке каптчи, формированию и отправке сообщения. (if (!$err)).

Далее, проверяем, установлен ли cookie и из какой формы пришел запрос - из формы ввода сообщения или из формы проверки каптчи и подтверждения данных:
(if ( (!isset($_COOKIE['mycaptchamd5'])) || !$myself )). Для проверки cookie проверяем наличие нашего cookie с именем mycaptchamd5 в суперглобальном массиве $_COOKIE, а для проверки, откуда именно пришел POST-запрос, используется скрытое поле myself, которое есть в форме проверки каптчи (<input type='hidden' name='myself' value='true'>). В зависимости от этого поля устанавливается переменная $myself.

Если хоть одно из этих условий не соблюдено, будем считать, что пользователь пришел в скрипт проверки каптчи из формы отправки сообщения. Генерируем форму с каптчей и данными, код каптчи и засылаем пользователю cookie:

$usermessage=createform ($name,$email, $sub,$message,$allnum,false);

тут все понятно, первые 4 переменные - данные из POST-запроса (см. выше), переменная $allnum - из скрипта captcha.php, а false - генерируем форму без сообщения об ошибочно введенной каптче, тому ще пользователь ее не вводил еще. Функция createform() была подробно описана выше.

Иначе, получается, что cookie уже заслана пользователю, и он нажал одну из кнопок submit в форме проверки каптчи. Либо кнопку 'Проверить код', либо кнопку 'Обновить код', которым, соответственно, присвоены имена upcode и checkcode в форме.

Как работать с двумя и более кнопками типа submit в форме, я объяснял здесь или здесь, так что все должно быть понятно - надо проверить, какое имя кнопки есть в запросе POST, в случае с кнопкой submit оно там будет одно, какую кнопку нажали, такое и будет.

Если нажата кнопка 'Обновить код', заново генерируем форму с каптчей и данными (они сохранятся, т.к. придут скрипту в скрытых полях - см. выше) и засылаем новую cookie:

if (isset($_POST['updcode'])) //обновить код
{
     $usermessage=createform ($name,$email, $sub,$message,$allnum,false); //генерируем
     //форму с каптчей и данными
}

Если это не так, значит, нажата кнопка 'Проверить код'.
Проверяем это 🙂

if (isset($_POST['checkcode']))
{
...
}

Тут бы корректнее сделать вложенные условия, но фактически, ничего особо страшного не произойдет - если кто-то изменил запрос так, что не будет ни одного значения (checkcode или upcode), сообщение вам не придет, а кулхацкер сам дурак.

Если нажата эта кнопка, проверяем корректность введенного кода каптчи:

1. Вытаскиваем в отдельную переменную $usercodemd5 введенный пользователем в соответствующее поле формы (<input type='text' name='captchacode'>) код каптчи, и получаем его MD5-хэш:

$usercodemd5=md5(trim($_POST['captchacode']));

2. Если значения переменных совпадают (if ($usercodemd5==$gencodemd5)) - код введен верно, отправляем сообщение.
3. Иначе, генерируем новую каптчу и новую cookie:

. . .
else //код неправильный
{
     $usermessage=createform ($name,$email, 
     $sub,$message,$allnum,true); 
}

Обратите внимание. В функции createform() последний параметр установлен в true. Это уведомит пользователя о неправильном вводе каптчи.

Но к редиректу не приведет, флаг редиректа по умолчанию false, а пока в основном рабочем процессе мы его не трогали.

Если же каптча введена правильно, то отправляем сообщение на указанный в скрипте e-mail:

1. Удаляем более не нужный cookie: setcookie("mycaptchamd5","",time()-300);
2. Формируем сообщение: $mes = "Имя: ".$name."\n\nТема: " .$sub."\n\nСообщение: ".$message."\n\n"."E-mail to answer: $email\n\n";
3. Отправляем, используя функцию mail ()

$send = mail ($address,$sub,$mes,"Content-type:text/plain; charset = UTF-8\r\nFrom:$address");

4. Проверяем на ошибки, соответственно, устанавливая флаг ошибки $err, или формируя сообщение пользователю об успехе:

if ($send) //сообщение успешно отправлено
{
   //сообщение пользователю об успехе
   $usermessage="<center><b><font color='blue'>Сообщение успешно отправлено!<br>
   Форма обратной связи будет открыта через ".$redirtime." секунд(ы)   ";
   $redir=true; //устанавливаем статус редиректа
}
else //ошибка функции mail()
{
   $usermessage="Внутренняя ошибка при отправке сообщения! :(";
   $err=true;						
}

Итак, мы защитили скрипт отправки сообщений от всякой школоты и кулхацкеров.

А ТЕПЕРЬ, ВНИМАНИЕ, ВОПРОС
Обход этой каптчи:

Реализация не совсем промышленная. Мне хочется, чтобы вы подумали, над тем, как данный способ защиты обойти. Можете воспользоваться своим методом и попытаться заспамить мне почтовый ящик. Кто заспамит - получит минус два вопроса на зачете.
Можно спамить просто так, но желательно, с описанием способа. Я знаю 5.

Комментарии на lj.rossia.org будут открыты для всех, можно анонимно предлагать, как способы обхода, так и способы защиты. Стирать не буду ничего, кроме флуда, спама и личных оскорблений.

Для самых ленивых, кто не удосужился все прочесть. Инструкция по установке.

Форма обратной связи для WordPress, без плагина здесь или здесь

Все делается так же, с той лишь разницей, что обновленный архив скачиваете по ссылке ниже, и не забываете закачать captcha.php в директорию темы, вместе с mail.php.

Ссылки

Смотреть код скрипта на PasteBin
Скачать все одним архивом
Тесты и куски кода

Заметка в PDF

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*