Další z článků, který po letech potřebuje oprášit jsou Formuláře v PHP - ošetření odesílaných dat. Byť samotné kontroly zůstávají pořád stejné, způsob jejich realizace se po letech vyvinul. Dříve jsme zobrazovali chybová hlášení všechna po kupě někde nad formulářem, pomocí JavaScriptového alertu nebo v případě začátečnické realizace samostatně na externí stránce. 

Dnes uživatelé vyžadují větší pohodlí a intuitivnější chování webových aplikací, a tak zobrazujeme chybová hlášení většinou hned vedle daného políčka. Jak jsem už ale zmínil, princip kontrol není třeba měnit. Článek "Je čas udělat Wordpressu pápá" se datuje na 1. 10. 2007, tehdy jsem přešel na vlastní redakční systém, kde používám úplně stejné kontroly v komentářích. A od té doby jsem nezaznamenal jediný spam - tedy spam od neživého návštěvníka. Základní prvky ochrany tedy fungují stále dobře a je na čase si je připomenout.

Prosím berte v potaz, že původní článek je z roku 2007. Hodně se za tu dobu změnilo, a tak návod nemusí odpovídat dnešním standardům. Ukázka ale funguje na PHP5 a můžete ji využít jako první krok k lepšímu porozumnění PHP.

Ukázkový formulář

<? if(!empty($_GET['sent'])){ ?>
	<p class="success">Formulář byl úspěšně odeslán</p>
<? } ?>

<form action="<?=$_SERVER['REQUEST_URI'];?>" method="post">
	<fieldset>
		<legend>Přidej komentář</legend>
		
		<label>Jméno <b>*</b></label><br />
		<input type="text" name="jmeno" value="<?=(isset($_jmeno) ? $_jmeno : '');?>" /><br />
		<?=(!empty($error_messages['jmeno']) ? '<div class="error">'.$error_messages['jmeno'].'</div>' : ''); ?>
		
		<label>E-mail <b>*</b></label><br />
		<input type="text" name="web" value="<?=(isset($_email) ? $_email : '');?>" /><br />
		<?=(!empty($error_messages['email']) ? '<div class="error">'.$error_messages['email'].'</div>' : ''); ?>
		
		<label>Web</label><br />
		<input type="text" name="email" value="<?=(isset($_web) ? $_web : '');?>" /><br />
		<?=(!empty($error_messages['web']) ? '<div class="error">'.$error_messages['web'].'</div>' : ''); ?>
		
		<label>Vaše zpráva <b>*</b></label><br />
		<textarea name="text" rows="4" cols="10"><?=(isset($_text) ? $_text : '');?></textarea><br />
		<?=(!empty($error_messages['text']) ? '<div class="error">'.$error_messages['text'].'</div>' : ''); ?>
		
		<span class="schovany">
			<label>Tohle políčko ponechte prázdné</label><br />
			<input type="text" name="city" /><br />
			<?=(!empty($error_messages['check1']) ? '<div class="error">'.$error_messages['check1'].'</div>' : ''); ?>
		</span>
		
		<label>Kontrola: Opište číslici pět <b>*</b></label><br />
		<input type="text" name="skype" /><br />
		<?=(!empty($error_messages['check2']) ? '<div class="error">'.$error_messages['check2'].'</div>' : ''); ?>
		
		<button name="submit" type="submit">Odeslat</button><br />
	</fieldset>
</form>

Takhle nějak by mohla vypadat základní kostra formuláře. Doporučuji samozřejmě i atributy FOR a ID pro labely a inputy (popřípadě obalit input labelem). Třídu "schovany" nastylujte na display:none. Pokud se na kód podíváte blíže, můžete si všimnout hned tří úrovní ochrany proti spamu, které za chvíli rozeberu. Další dvě úrovně ochrany pak budou schované už v obsluze odesílacího skriptu.

Zpracování dat po odeslání, krok 1: Základní ošetření

Formulář budeme posílat na stejnou url, na které je, takže zpracování odeslaných dat musí proběhnout ještě před zobrazením jakéhokoli HTML kódu. V případě jednoduchého formuláře, na kterém kontrolu předvádím můžeme začít například takto:

<?php
if(isset($_POST['submit'])){
	$_jmeno = htmlspecialchars(trim($_POST['jmeno']));
	$_email = htmlspecialchars(trim($_POST['web']));
	$_web   = htmlspecialchars(trim($_POST['email']));
	$_text  = htmlspecialchars(trim($_POST['text']));
	
	$_check1 = $_POST['city'];
	$_check2 = htmlspecialchars(trim($_POST['skype']));

htmlspecialchars() a trim() je vše, co k ošetření vstupu potřebujete. Při ukládání do databáze je samozřejmě potřeba myslet na escape uvozovek, ale o databázi se v dnešním článku bavit nebudeme. U větších formulářů pak tato kontrola bude napsaná úplně jinak, ale pro začátek je tato ukázka víc než dostatečná. Ukončující závorku záměrně neuvádím, protože v bloku kódu budeme pokračovat.

Zpracování dat po odeslání, krok 2: Antispam

Na pořadí kroku 2 a 3 samozřejmě nezáleží, já zvolil jako první antispam. Jak nahoře ve formuláři, tak zde v prvních pár řádcích PHP jste si mohli všimnout, že jména pole e-mail a web jsou prohozená. To je první past na roboty. Pokud v poli email bude zavináč, vypíšeme chybu a formulář nezpracujeme. Dále zkontrolujeme odpověď na kontrolní otázku a hodnotu skrytého pole: pokud je vyplněno, opět se jedná o robota. Roboti totiž zpravidla vyplní každé pole formuláře nějakými bláboly - a my si proto řekneme, že právě jedno z polí musí být prázdné. Robot ho vyplní a skončil.

Skrytému poli je také dobré dát nějaký výstižný název, na který se roboti nachytají. V mém případě "city", můžete ale uvést i názvy "email_here" nebo cokoli jiného. I kontrolní pole samo o sobě má název, který láká na vyplnění všeho jiného jen ne jednomístného čísla.

Prohození názvů polí samozřejmě můžeme vynechat. V případě, kdy generujeme celý formulář z databáze a atribut name má hodnoty ve tvaru "input_25", "input_26" a podobně, budou nám bohatě stačit dvě antispamová pole pojmenovaná "email", "skype" či v podobném duchu.

Dále provedeme kontrolu zakázaných slov a počet hypertextových odkazů v těle zprávy. Zakázaná slova můžeme mít uložená v databázi nebo jenom staticky v proměnné, na tom nezáleží. Na čem ale záleží je uvedení těch správných frází, které se často vyskytují ve spamu. Pokud takové slovo vloží sám uživatel, dostane chybové hlášení, slovo smaže a příspěvek může odeslat. Robot ale bude ztracen.

	// antispam
	$evil_words = array('[url', 'viagra', 'website');
	foreach($evil_words as $word){
		if(strpos($_text, $word) !== false){
			$error_messages['text'] = 'Zpráva obsahuje některé ze zakázaných slov: '.implode(', ', $evil_words);
		}
	}
	
	if(substr_count($_text, 'http://') > 5 ){
  		$error_messages['text'] = 'Více než pět hypertextových odkazů není povoleno.';
  	}
  	
  	if(strpos($_web, '@') !== false){
		$error_messages['web'] = 'Jste spam, prosím nebuďte spam.';
	}
	
	if(!empty($_check1)){
		$error_messages['check1'] = 'První kontrolní pole ponechte prázdné.';
	}
	
	if($_check2 != 5){
		$error_messages['check2'] = 'Chybně opsaný kontrolní kód.';
	}

Zpracování dat po odeslání, krok 2: Kontrola uživatelských dat

Po prvním bloku kontrol následuje druhý, kde už jenom zkontrolujeme, zda-li jsou vyplněna všechna pole a jestli mají správný formát. Abychom ošetřili samotné tělo zprávy proti dlouhým řetězcům bez mezer, přidáme poslední blok kódu, které rozdělí slova delší než 50 znaků. Pokud je vše v pořádku (proměnná $error_messages nebyla ani jednou naplněna), můžeme provést uložení formuláře a redirect.

	// kontrola uzivatelskych dat
	if(empty($_jmeno)){
		$error_messages['jmeno'] = 'Pole Jméno je povinné.';
	}
	
	if(!preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/', $_email)){
		$error_messages['email'] = 'Pole E-mail má chybný formát.';
	}

	if(empty($_email)){
		$error_messages['email'] = 'Pole E-mail je povinné.';
	}
	
	if(empty($_text)){
		$error_messages['text'] = 'Pole Vaše zpráva je povinné.';
	}else{
		$text_array  = explode(' ', $_text);
		$text_count  = count($text_array);
		$text_return = NULL;
		
		for($i = 0; $i <= $text_count-1; $i++){
		    $text_array[$i] = wordwrap($text_array[$i], 50, ' ', 1); 
		    $text_return .= $text_array[$i].' '; 
		}
		
		$_text = $text_return;
	}
	
	// uložení dat do DB, odeslání na email + přesměrování
	if(empty($error_messages)){
		$current_url = $_SERVER['REQUEST_URI'];
		$current_url.= (strpos($current_url, '?') === false ? '?sent=1' : '&sent=1');
		
		header('Location: '.$current_url);
		echo '<meta http-equiv="refresh" content="1;url='.$current_url.'" />';
		exit;
	}
} // konec if(isset($_POST['submit']))

Vypsání chyb zpět do formuláře

Jak jste si mohli všimnout v ukázce výchozího HTML, formulář už je na chystaný na zpětný výpis chyb. Pokud bude naplněn některý z klíčů proměnné k tomu určené, na daném místě se vypíše. Pokud bude u některého pole zaznamenáno více chyb, vypíše se vždy jen poslední zaznamenaná: není nutné vypisovat všechny, uživatel formulář odešle a dostane nové chybová hlášení. Když například zapomenu vyplnit e-mail, stačí napsat, že e-mail není vyplněn. Nemusím ještě číst další řádek o tom, že pole neodpovídá regulárnímu výrazu.

Bonus: GEO IP ochrana

Ochrana před přístupy z některých vyjmenovaných zemí je sice věc, která by měla být řešená na úrovni PHP (abychom mohli návštěvníkovi vypsat alespoň hlášení, že je ve špatné lokalitě a celé to mohlo být administrovatelné), ovšem pro malé weby cílené na menší region je tohle vcelku zbytečné. IP adresy některé exotických zemí bývají často využíváné spammery, takže zákazem přístupu z dané země můžeme velice jednoduše přidat další úroveň ochrany. Stačí do souboru .htaccess vložit následující řádky, popřípadě je rozšířit o další domény.

# soubor .htaccess
Deny from .ru # rusko
Deny from .ua # ukrajina
Deny from .cn # cina
Deny from .in # indie
Deny from .sa # saudska arabie

Ukázka