Zpět

Začínáme s PHP - díl 5.

Texty v databázi

V pátém dílu seriálu o tvorbě dynamických doplňků na webové stránky probereme ukládání textů do databáze, dokončíme zabezpečení SQL, nakousneme obranu proti spamovacím robotům a vytvoříme si svoji první návštěvní knihu.

Návštěvní kniha (anglicky guestbook) je stránka, na kterou můžou návštěvníci připisovat vzkazy. Provedení se může lišit od jednoduchého malého okénka (shoutboard) po plnohodnotné jednovláknové fórum s možností odpovídání, citací, přihlašování a podobně. V tomhle článku zůstaneme spíš na tom jednodušším konci spektra. Ale dost teorie, jde se na věc.

Datové struktury

Na ukládání vzkazů od návštěvníků nám postačí jedna tabulka, ve které bude každý řádek odpovídat jednomu vzkazu. Ukládat budeme:

Na text vzkazu nám asi nebude stačit TINYTEXT (255 znaků), ale TEXT postačí určitě - 64 KB nemá ani celý tenhle článek. Stejně by posloužil jakýkoli dostatečně dlouhý VARCHAR.

Na identifikační kód použijeme dostatečně velké přirozené číslo s automaticky nastavovanou hodnotou, jako obvykle.

Jméno autora bude pravděpodobně celkem krátké, dlouhé traktáty tam píšou snad jen hloupí roboti. Dejme tomu, že 30 znaků by mohlo stačit. Teoreticky by se autoři mohli podepisovat přímo v textu vzkazu a oddělené políčko pro jméno by tedy nebylo nutné, ale nebylo by to moc praktické (půlka lidí by na to zapomínala).

Datum a případně i čas odeslání jsou docela užitečné údaje. Zároveň slouží i pro orientaci návštěvníka: podle nich bezpečně pozná, jakým směrem jsou příspěvky v knize řazeny (tedy pokud mu to nenaznačíme nějakým jiným způsobem, což doporučuji). Jako datový typ můžeme použít DATE nebo DATETIME, v našem příkladu si vystačíme s datem.

Tabulka by tedy mohla vypadat nějak takto:

CREATE TABLE nkniha
(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
datumvlozeni DATE,
autor VARCHAR(30),
obsah TEXT
)

Pozn.: čistě teoreticky by se kniha dala vytvořit i bez databáze, pouze zápisem do souboru. Ale to má několik háčků: zaprvé obtížné řazení od nejnovějšího příspěvku, zadruhé obtížné stránkování a zatřetí problémy, pokud přijde víc příspěvků současně. První dva háčky se dají obejít vytvořením samostatného souboru pro každý příspěvek (třeba s číselným jménem), se třetím nic nenaděláme.

Odesílání vzkazů

První základní funkce knihy. Potřebujeme na to odesílací formulář, do kterého nám návštěvníci budou svoje vzkazy psát. Stačit nám budou dvě textová políčka, datum a kód si doplníme sami:

<form action="nkniha.php" method="post">
Vaše jméno:<br>
<input type="text" name="jmeno" maxlength="30" size="30"><br>
Váš vzkaz:<br>
<textarea name="vzkaz" rows="7" cols="50" wrap="soft"></textarea><br>
<input type="submit" value="Ulož do knihy">
</form>

Po odeslání formuláře se zadaný vzkaz předá skriptu nkniha.php. Asi nejpraktičtější bude, když dáme všechno - formulář, zpracování i zobrazení - do jednoho skriptu (pak je atribut action zbytečný), ale teoreticky by to šlo zařídit i odděleně.

Skript nkniha.php tedy dostal návštěvníkův vzkaz a měl by ho uložit do databáze. Tady máte první nástřel - schválně zkuste přijít na to, co je na něm špatně:

mysql_query("INSERT INTO nkniha VALUES (NULL, CURDATE(), '".$_POST['jmeno']."', '".$_POST['vzkaz']."')",$spojeni));

Příkaz INSERT, čili vložení řádku do tabulky, už dávno známe. Hodnota NULL na místě identifikačního kódu je v pořádku - nic jiného by tam ani nešlo, dosazení správného čísla zajistí auto_increment. Funkce SQL CURDATE() je novinka: dá nám aktuální datum přesně v takovém tvaru, jaký vyžaduje datový typ DATE. Zadané jméno a vzkaz, které se uloží do sloupců autor a obsah, jsou syntakticky správně. Parametr $spojeni je jako obvykle výstup z mysql_connect, na tom není co zkazit. Takže?

Jasně, bezpečnost. Kdo dával minule pozor, ví, co to je SQL injekce:

Vaše jméno:

Váš vzkaz:

Po dosazení zadaných textů do výše uvedeného příkazu by databáze dostala tohle:

INSERT INTO nkniha VALUES (NULL, CURDATE(), 'vtipálek', 'nic'); DROP TABLE nkniha; --')

Ano, totéž jako známý vtip z xkcd. Apostrof za slovem "nic" ukončil textový parametr, závorka a středník ukončují příkaz INSERT, následuje nový příkaz a nakonec dvě pomlčky zakomentují případný zbytek původního příkazu, aby ho SQL nebralo jako syntaktickou chybu. Jak prosté, milý Watsone...

V našem případě to naštěstí není tak horké, funkce mysql_query totiž dokáže zpracovat pouze jeden příkaz. Když jich dostane víc (i když jsou syntakticky v pořádku a oddělené středníkem), neprovede ani jeden a rovnou vrátí false. Ale byla by chyba se na to spoléhat - co kdyby to nějaká verze dělala jinak, že jo.

Uživatelské texty vkládané do příkazů proto musíme vždy prohnat nějakou escapovací funkcí, která zajistí, že se z textu stane opravdu jenom neškodný text, který SQL za žádných okolností nevyhodnotí jako příkaz. První možností je standardní phpčkovská funkce addslashes, která ale používá neměnný algoritmus a nebere v úvahu případné syntaktické odlišnosti konkrétní databáze. Nejjistější je mysql_real_escape_string, která je stavěná databázím SQL na míru:

$zabezpeceny_text=mysql_real_escape_string($puvodni_text,$spojeni);

Funkce před každý apostrof a další řídicí znaky vloží znak "\". Interpret si po vyhodnocení příkazu lomítka vyhází, takže v databázi už bude text uložený bez nich, přesně tak, jak ho uživatel napsal.

Mysql_real_escape_string je strašně dlouhé slovo, z hlediska pohodlí se vyplatí zabalit si ho do nějaké funkce s kratším jménem. Já například používám tohle:

function zabezpec($retezec)
{
global $spojeni;
return mysql_real_escape_string($retezec,$spojeni);
}

Novinkou je pro nás slovíčko global, které říká, že proměnná $spojeni je globální. To je nutné, protože v PHP se všechny proměnné definované uvnitř funkcí berou jako lokální. Kdyby tam ten řádek nebyl, mysql_real_escape_string by skončila chybou, protože by v omylem vytvořené lokální proměnné $spojeni pochopitelně nenašla platné spojení s databází.

Pozn.: Obecně se používání globálních proměnných moc nedoporučuje. Ne že by to bylo technicky špatně, jenom se za "čistší" považuje předávání dat přes parametry nebo přes metody objektů. Ale jinak je to stejné jako s jinými obecně nedoporučovanými prasárnami: někdy může jedno GOTO nebo globální proměnná elegantně zachránit ošklivě zamotanou situaci :-). Ale samozřejmě platí: nepřehánět, jinak se v tom ztratíte daleko rychleji.

Takže tedy ukládání příspěvků do knihy, znovu a lépe:

mysql_query("INSERT INTO nkniha VALUES (NULL, CURDATE(), '"
            .zabezpec($_POST['jmeno'])."', '"
            .zabezpec($_POST['vzkaz'])."')",$spojeni));

Teď už je to v pořádku, tohle nám nikdo nehackne.

Kdy zabezpečení provádět, to už je na vás. Buď až těsně před použitím jako v tomto příkladě, nebo o něco dříve, nebo si třeba můžeme hned na začátku pro jistotu zabezpečit rovnou celý POST a máme po starostech:

foreach($_POST as $klic=>$hodnota)
 $zpost[$klic]=mysql_real_escape_string($hodnota,$spojeni);

Pole $zpost možná občas použiju i v dalším textu. Vždycky bude představovat zabezpečenou kopii $_POST a nebudu zbytečně opakovat, jak jsme se k ní dostali. Obdobný význam bude mít pole $zget pro $_GET. Samozřejmě pozor na to, že občas nám zabezpečení může nadělat víc škody než užitku, například by mohlo rozhodit očekávané formáty vstupních dat. Proto se tenhle postup dá použít jenom někdy a je dobré nechat si původní Post přístupný.

Dobrá, proti SQL injekci jsme obrnění. Zbývá ještě nepodstatná drobnost zvaná doubleposty, čili duplicitní příspěvky. Ty vznikají, když někdo po odeslání vzkazu během netrpělivého čekání na odezvu serveru zmáčkne F5 - tím se data z formuláře odešlou znovu (některé prohlížeče se ptají, některé odesílají rovnou). Zabráníme tomu tak, že ihned po uložení příspěvku skript restartujeme, ale tentokrát bez formulářových dat. Jak? Skokem pomocí funkce header:

header('Location: nkniha.php');

Nebo kratší a blbuvzdornější forma, která říká "skoč na ten skript, ve kterém zrovna jsi":

header('Location: .');

To samozřejmě předpokládá, že zpracování nového příspěvku je to první, co náš skript dělá, a nic během toho nevypisuje na stránku, jinak by header nefungoval. Ale to už znáte. Další samozřejmá věc je, že header voláme z vnitřku podmínky "pokud byl zadán nějaký text", jinak by se nám skript zacyklil donekonečna.

Zobrazování příspěvků

je druhá základní funkce knihy - bez ní by to nebyla kniha, ale černá díra.

První otázka je, v jakém pořadí chceme vzkazy ukazovat: buď odshora dolů od nejstaršího k nejnovějšímu (což se líp čte, proto se to používá na fórech nebo tam, kde se očekávají hodně dlouhé příspěvky), nebo obráceně (takže bez zdlouhavého rolování rovnou vidíte nejnovější konec diskuse). Dejme tomu, že vzkazy v naší knize budeme řadit od nejnovějšího. Kód pro zobrazení všech vzkazů najednou by mohl vypadat třeba takhle:

$vysledek=mysql_query("SELECT datumvlozeni, autor, obsah FROM nkniha ORDER BY id DESC",$spojeni);
while ($radek=mysql_fetch_array($vysledek)):
  echo '<p><b>'.$radek['autor'].'</b> ';
  echo '('.$radek['datumvlozeni'].')<br>';
  echo $radek['obsah'].'</p>';
endwhile;

To je asi nejjednodušší možná varianta. Má jeden podstatný nedostatek: texty sypeme do HTML kódu stránky tak, jak je uživatelé napsali, a prohlížeči, snaž se. Můžete si být jisti, že se brzy najdou vtipálci, kteří vám knihu zaneřádí v lepším případě blikajícími obrázky přes celou obrazovku (stačí tag <img>) a odkazy na stránky prodávající zaručeně pravé hodinky a zázračné pilulky, v horším případě pak klientskými skripty a aktivními objekty, které si s počítačem návštěvníka mohou dělat, co chtějí. Takže tudy ne, přátelé. Chce to text projít a potenciálně nebezpečné tagy zneškodnit.

První možnost je funkce strip_tags. Ta ze zadaného textu veškeré HTML tagy (včetně komentářů a úseků <?php ... ?>) prostě vymaže. Kdyby se vám to nehodilo, můžete ještě doplnit druhý parametr se seznamem tagů, které se mají zachovat, např.:

$novy_text=strip_tags($puvodni_text,'<br><p><img>');

V takovém případě ale pozor, že u zachovaných tagů zůstanou všechny jejich atributy, i ty potenciálně nebezpečné (onload, onmouseover apod.).

No jo, ale co když nám do knihy někdo bude chtít napsat, že a<b nebo že nemá rád tag <font>? V takovém případě by mu strip_tags smazala půl příspěvku. Naštěstí existuje druhá možnost, funkce htmlspecialchars. Ta aktivní znaky jako <, ", & a podobně přepíše na HTML entity (&lt; &quot; &amp; atd.), takže je potom prohlížeč jednak zobrazí tak, jak byly napsány, a jednak je nebude vyhodnocovat jako HTML kód, takže nám nehrozí žádné skriptové útoky.

Nebezpečného HTML kódu jsme se tedy zbavili, ovšem pisatelé tím přišli o veškeré možnosti formátování, včetně tak základní věci, jako je zalamování řádků. Enter napsaný do textového pole sice vloží do textu znak \n (nový řádek), ale ten se v HTML projeví jenom jako mezera. Naštěstí i na tohle máme chytrou funkci, nl2br, která před všechny znaky \n vloží tagy <br />. Kdybyste chtěli klasický tvar <br>, musíte přidat druhý parametr s hodnotou false:

$novy_text=nl2br($puvodni_text,false);

Při true nebo při vynechaném druhém parametru se vkládá varianta s lomítkem.

To je sice docela hezké, ale poněkud jednoúčelové. Co kdybychom chtěli nahrazovat úseky textu nějak obecněji? Třeba textového smajlíka ":-)" grafickým "<img src="smajlik1.gif">"? S tím si poradí funkce str_replace(co,čím,kde):

$novy_text=str_replace(':-)','<img src="smajlik1.gif">',$puvodni_text);

Str_replace je velice užitečná věc, která určitě najde uplatnění i v mnoha jiných aplikacích.

Poslední věc, která by se nám mohla nelíbit, je datum. Pokud ho zobrazíme tak, jak nám přijde z databáze, bude vypadat nějak jako 2012-08-10. To je dobré leda tak pro Američany, my bychom radši 10. 8. 2012. Na formátování data má SQL funkci DATE_FORMAT, pro uvedený tvar data by vypadala takhle:

SELECT DATE_FORMAT(datumvlozeni,'%e. %c. %Y') FROM nkniha

První parametr je datum ke zformátování, druhý je požadovaný tvar. Každý kód %+písmeno má nějaký význam (třeba "číslo dne", "číslo měsíce bez úvodní nuly" apod.), kompletní seznam najdete v manuálu. Ostatní znaky (např. ty tečky a mezery) se zobrazí tak, jak jsou. Výsledkem funkce je textový řetězec.

No jo, jenže jak se ten výsledek bude jmenovat, čili jaký index máme použít ve funkcích mysql_fetch_array nebo assoc? Upřímně řečeno, nevím a ani to vědět nepotřebuju. V SQL se totiž dá použít alias, tj. předefinovat jméno libovolného sloupce. Dělá se to klíčovým slovem AS (česky "jako") a v našem případě by to mohlo vypadat třeba takhle:

$vysledek=mysql_query("SELECT DATE_FORMAT(datumvlozeni,'%e. %c. %Y') AS upravenedatum, autor, obsah FROM nkniha ORDER BY id DESC",$spojeni);
$radek=mysql_fetch_array($vysledek);
echo $radek['upravenedatum'];

Aliasy jsou docela užitečná věcička - jak u takovýchto funkcí s blíže neurčeným názvem výsledku, tak např. u výběru dat z více tabulek současně, kde se může sejít několik sloupců se stejným jménem (typicky třeba id) a je potřeba je nějak odlišit. Ale o tom si povíme jindy.

Ale dost už vylepšování textů, nebo nám z nich nic nezbyde. Další důležitou otázkou je, kolik příspěvků najednou chceme ukázat. Vypisovat všechny by nebylo moc praktické - čím víc jich bude, tím déle by se kniha načítala a v půl kilometru vysoké stránce by se stejně nikdo nevyznal. Takže to raději nějak omezíme, nejlépe stránkováním po určitém počtu vzkazů, a přidáme nějaké ovládací prvky, kterými si návštěvník může nalistovat všechno, co bude chtít.

Stránkování zobrazených příspěvků

Předpokládám, že na další stránky nás dostane nějaký odkaz "zobraz starší/novější vzkazy", čili nějaká proměnná předaná metodou GET (POSTový formulář s jedním tlačítkem by teoreticky šel taky, ale neprošli by přes něj vyhledávací roboti). Na určení polohy by se dala použít přímo položka id, ale předpokládám, že vzkazy budeme občas i mazat, takže některé hodnoty id přestanou existovat a byly by z toho potíže. Takže si zavedeme hodnotu "číslo stránky". Dejme tomu, že deset nejnovějších vzkazů bude na stránce 0, dalších 10 bude na stránce 1 atd., až k tomu úplně nejstaršímu. V SQL se výběr provede omezením Selectu pomocí klíčového slova LIMIT:

SELECT * FROM tabulka LIMIT x,n

Tím se z tabulky vybere n řádků, počínaje od xtého (x se počítá od nuly). Dá se to kombinovat i s dalšími omezujícími podmínkami, jako třeba WHERE (v takovém případě Limit ořezává až to, co po Where zbyde). V našem konkrétním případě tedy může příkaz vypadat např. takto:

$vysledek=mysql_query("SELECT * FROM nkniha ORDER BY id DESC LIMIT ".10*$stranka.",10",$spojeni);

Na stránce 0 se vybere deset nejnovějších. Na stránce 1 se těch deset nejnovějších přeskočí a vybere se dalších 10 od jedenáctého dál a tak dále.

Pozn.: Možná vás napadlo, že bychom stejně dobře mohli z databáze vzít všechno a výsledky si probrat až v PHP v nějakém cyklu. Ano, teoreticky by to samozřejmě šlo, ale bylo by to výrazně pomalejší a náročnější na paměť. Zatímco SQL stačí jednou si přečíst zadaný příkaz, sáhnout do databáze a za pár milisekund vám vrátí požadovaný očesaný výběr, PHP musí v každém průchodu očesávacím cyklem znovu a znovu interpretovat každý řádek. V PHP obecně platí, že je vždycky výhodnější používat předkompilované standardní funkce nebo pečlivě nakombinované databázové příkazy, než skládat složité algoritmy a cykly z jednoduchých příkazů.

Stránkovaný výběr tedy umíme, ale ještě se potřebujeme nějak dostat k hodnotě $stranka.

Dejme tomu, že výchozí stav bude stránka 0 - tu zobrazíme, pokud žádné číslo nepřijde nebo pokud místo čísla přijde nějaký nesmysl (ať už neúmyslný překlep nebo pokus o SQL injekci). Pokud nějaké platné číslo přijde, použijeme ho. Třeba takhle:

if (isset($_GET['stranka'])           //je nějaké číslo zadané?
    and is_numeric($_GET['stranka'])  //a je to opravdu číslo?
    and ($_GET['stranka']>=0))        //a je nezáporné?
  $stranka=$_GET['stranka'];        //ano, použijeme ho
  else $stranka=0;                  //ne, použijeme výchozí stránku

Jakmile známe aktuální stránku, můžeme sestavit stránkovací odkazy:

echo '<a href="nkniha.php?stranka='.$stranka-1.'">Novější příspěvky</a>';
echo '<a href="nkniha.php?stranka='.$stranka+1.'">Starší příspěvky</a>';

Samozřejmě bude potřeba umístit je na nějaké vhodné místo. Kam, to je věc názoru. Já osobně nemám rád, když mají podobu šipek doleva a doprava nebo slov "předchozí" a "další". Vzkazy v knize jsou poskládané svisle, tak proč by se mělo najednou listovat vodorovně? A slovem "předchozí" se myslí předchozí (dříve napsané, tedy starší) vzkazy, nebo předchozí (dříve navštívená, tedy novější) stránka? Proto mám ve svojí knize šipky nahoru a dolů. Ale to už je vaše věc, rozhodně vám nechci svůj styl nějak vnucovat :-).

Docela příjemná vychytávka může být to, že odkaz nahoru skočí na spodní konec novější stránky (odkazem s #kotvou), což čtenářům ušetří mačkání Endu (stejně by četli odspoda). Také není od věci odkaz na novější stránku nezobrazovat, pokud jsme na stránce 0. Obdobně můžeme naopak nezobrazit odesílací formulář, pokud na stránce 0 nejsme. O něco těžší by bylo nezobrazit odkaz na starší stránku, pokud jsme úplně na začátku knihy. Dále můžeme přidat odkazy úplně na začátek a úplně na konec. Trefit se na stránku 0 je triviální, číslo poslední stránky by se vypočítalo z celkového počtu vzkazů v knize. Ten by se zjistil takto:

SELECT COUNT(*) FROM nkniha

Příkaz nám vrátí jednoprvkovou návratovou tabulku, ze které si příslušné číslo vytáhneme třeba přes mysql_fetch_row.

Nyní tedy víme, jak ukládat vzkazy do databáze a jak je z ní vybírat a zobrazovat. Zbývá poslední kapitola:

Komentářový spam

Cílem spammera je rozšířit po internetu co nejvíc odkazů na určité stránky. Vyhledávače si pak myslí, že když na ně vede tolik odkazů z tolika míst, jsou asi fakt dobré, a tak je ve výsledcích zobrazují na přednějších místech.

Většina spammerů to samozřejmě nedělá ručně, ale napíší si na to robota - program, který automaticky prochází internet, hledá slibně vypadající formuláře a zkouší do nich psát. Co jsem tak vypozoroval, spamboti se na našich stránkách chovají zhruba takhle:

  1. Nejdřív se musí zorientovat a najít návštěvní knihu. Orientaci jim velice usnadňují odkazy s texty jako "Guestbook", "Discussion board", "Forum" a podobně. První obrannou taktikou tedy je pojmenovat odkazy nějak jinak - například česky, tomu roboti obvykle nerozumějí. Když už potřebujete angličtinu, docela pomáhá, když nahradíte některá písmena ASCII kódy (třeba &#101; místo e) - lidé to přečtou bez problémů, ale pro robota je to úplně jiný řetězec.
  2. Nejhloupější roboti hledání vzdají, ale ostatní dříve nebo později formulář knihy najdou. Nejdřív se podívají, jestli vypadá jako návštěvní kniha. Má aspoň jedno velké textové pole (textarea)? Má odesílací tlačítko, na kterém je v ideálním případě napsáno něco jako "Submit", "Post", "Save" nebo "Send"? Jsou tam ještě nějaká další textová políčka, nejlépe s předvyplněnými hodnotami jako "@", "http://" a tak? Jestli to vypadá slibně, robot do formuláře nasype svoje naprogramované bláboly a odešle je. V té chvíli musí zasáhnout naše druhá a nejdůležitější obranná linie, která rozezná spam od smysluplného příspěvku a zahodí ho. Metody rozpoznávání spamu si podrobněji probereme za chvilku.
  3. Po chvilce si robot stránku znovu načte a podívá se, jak jeho první pokus dopadl. Jestli tam svůj výplod najde, zajásá a začne knihu stejným nebo podobným textem bombardovat, dokud ji úplně nezamoří. Pokud ho ale nenajde, usoudí, že to buď návštěvní kniha není nebo že je nějak moc dobře zabezpečená a odtáhne s nepořízenou (proto se třeba moc nespamuje přes mailovací formuláře). Jestli jsme robota napoprvé neprokoukli a něco už nám do knihy napsal, můžeme ho teď aspoň odlákat tím, že nové příspěvky zobrazíme až po nějakém čase (třeba po hodině nebo po ručním schválení) nebo na jiné stránce. Otázka je, jak moc to pomůže proti spambotům a jestli to spíš neodradí živé návštěvníky.
  4. Stránky, kde se spamování daří, si roboti určitě dobře zapamatují a budou se tam rádi vracet.

Jak odhalit robota?

I druhou obrannou linii můžeme rozškatulkovat do několika vrstev. V první se snažíme roboty nachytat: zkoumáme, jakým způsobem odeslali formulář nebo kde v něm udělali nějakou botu. Sem patří různé kontrolní otázky, maskované ovládací prvky a podobně. Druhá vrstva zkoumá samotný text vzkazu a snaží se uhádnout, jestli je to spam nebo ne. To ovšem vyžaduje určitý stupeň umělé inteligence a rozsáhlou databázi vzorků, což přesahuje jak úroveň tohoto seriálu, tak úroveň mých znalostí :-). Třetí vrstva jsou různé závěrečné nouzové pojistky (jako třeba omezení počtu příspěvků), které zabrání totálnímu zahlcení knihy v případě, kdyby nějaký chytřejší robot prošel až sem.

Několik různých technik přechytračení robotů si teď probereme podrobněji.

Neviditelné prvky formuláře

Roboti vidí stránku jako HTML zdroják a je jim celkem jedno, jak vypadá v prohlížeči. Nejspíš je ani nenapadne zkoumat všechny přidružené definice CSS, jestli náhodou někde něco nemá vlastnost "display:none". To znamená, že na roboty můžeme nastražit past: pokud něco napíšou do neviditelného textového políčka nebo kliknou na neviditelné tlačítko, máme je - člověk to udělat nemůže.

<html>
<head>
<style type="text/css">
.viditelna {display:block}
.neviditelna {display:none}
</style>
...
</head>
<body>
...
<form method="post">
<input type="text" name="jmeno" class="viditelna">
<input type="text" name="past1" value="http://" class="neviditelna">
<textarea name="vzkaz" class="viditelna"></textarea>
<input type="submit" name="past2" value="Submit" class="neviditelna">
<input type="submit" name="odeslani" value="Uložit" class="viditelna">
</form>
...
<?php
...
if ( isset($_POST['past1']) and ($_POST['past1']<>'http://')
     or isset($_POST['past2']) ):
   ...je to robot...
else:
   ...je to člověk...
endif;
...

(Vyhodnocení dat jsem pro přehlednost dal až za hlavičku HTML a formulář, v praxi by bylo zřejmě úplně na začátku, aby se v případě potřeby dala použít funkce header.)

Výhoda téhle taktiky je jasná: neotravuje návštěvníky, protože si jí vůbec nevšimnou. Navíc je poměrně účinná, obzvláště když alternativních odesílacích tlačítek vytvoříte víc a jen jedno bude správné, případně pokud je ještě navíc náhodně střídáte. Nevýhody? Pokud se sem spammer podívá ručně, prokoukne to hned, svého robota upraví a má vystaráno. Někteří roboti se možná v CSS vyznají a stylu display:none si všimnou. Nebo budou zkoušet jedno tlačítko po druhém a nakonec se trefí. Také nevím, jak by to fungovalo v čistě textových prohlížečích s hlasovým výstupem pro slepce (v nejhorším tam můžeme dát popisky jako "sem nic nepište a na tohle neklikejte, nebo vám to smažu").

Matení Javascriptem

Tak, jako se dá pomocí CSS text skrýt, dá se Javascriptem napsat nebo různě měnit. Se správnou interpretací JS mají občas problémy i některé starší prohlížeče, natožpak roboti. Takže pokud si například formulář zobrazíme pomocí document.write() nebo si vhodným způsobem pohrajeme s událostmi onclick, onsubmit a onchange, zamotají se v tom.

Výhody jsou stejné jako prve: neviditelost pro lidi a účinné odpálkování robotů. Drobná nevýhoda: musíme spoléhat na to, že návštěvníci mají Javascript zapnutý a že jim v prohlížeči funguje tak, jak má. Dešifrování může být obtížné i pro živého spammera, ale jakmile ho prokoukne, napíše si parazitní formulář (tj. kopii našeho formuláře na svém počítači) a už se to s námi poveze.

Kontrola formátu zadaných dat

Tohle už trochu hraničí s inteligentními spamfiltry. Můžeme využít toho, že někteří boti píší dlouhé traktáty tam, kde je nečekáme (do políček pro jména, adresy a tak) nebo umisťují do textu spoustu odkazů (a href=...). Když se nám to nezdá, můžeme příspěvek zahodit. Délku textového řetězce zjistíme pomocí funkce strlen:

if (strlen($_POST['jmeno'])>50):
   ...podezřelé!...
else:
   ...asi v pořádku...
endif;

Výhoda: další nenápadná vrstva. Nevýhody: malá účinnost (roboti už většinou tak blbí nejsou) a možnost nechtěného smazání příspěvku od človka (nevěřili byste, jaké hrůzy si v návštěvních knihách vyměňují třeba programátoři).

Kontrola přes session nebo cookie

Tohle nemám ověřené, berte to spíš jako brainstorming. Skript, který zobrazuje formulář knihy, by mohl zároveň do session nebo cookie vložit nějakou proměnnou a při vyhodnocování odeslaných dat zkontrolovat její přítomnost. To by ztížilo použití parazitních formulářů, protože by tu proměnnou musely generovat taky. Z HTML kódu formuláře to není poznat, spammera by muselo napadnout, že má pátrat v sušenkách.

Turingův test neboli Captcha

Jde o různé kontrolní otázky, opisování pokrouceného textu z obrázků a podobné věci, které by člověk teoreticky měl zvládnout levou zadní, ale roboti by si na nich měli vylámat zuby. Účinnost je zpravidla poměrně slušná, ale hodně záleží na tom, jaký test zvolíme a jak je který robot vybavený. Zásadní nevýhoda je, že testy otravují především lidi a občas jim dělají větší potíže než robotům (nečitelné obrázky apod.). Další mínus je omezená použitelnost pro slepce a textové prohlížeče.

Různých kontrolních testů existuje nepřeberné množství, tady uvedu jenom pár příkladů, se kterými jsem se dosud setkal:

Ale počkat... náhodně vybraný obrázek nebo otázka je hezká věc, ale jak náš skript při vyhodnocování odpovědi pozná, kterou otázku předtím vybral?

První a nejtriviálnější možnost je otázku nějak označit přímo ve formuláři, třeba skrytým prvkem (input type="hidden") s nějakým kódem otázky. Vyhodnocovací algoritmus to potom má jednoduché: patří k tomuhle kódu tahle odpověď? Ano/ne, hotovo. Nevýhoda je v tom, že jakmile se živý spammer mrkne do zdrojáku, vykopíruje si kód otázky do parazitního formuláře, přihodí svoji odpověď a rozjede to ve velkém.

Lepší je kód otázky nějak zamaskovat. Buď tím, že se pošle jinudy než přes formulář (třeba přes session nebo cookie, což je asi nejobvyklejší postup), nebo že se zahashuje, třeba funkcemi md5(), crypt() nebo hash(). Ideálně třeba v kombinaci s aktuálním datem - nikoho nebude bavit každý den ručně krmit robota novým kódem. Nejlepší bude ukázat si to na příkladu. Dejme tomu, že máme otázky a odpovědi uložené v polích $otazky a $odpovedi s indexy od 1 do 100.

echo '<form method="post">';
$cislootazky=rand(1,100); //náhodný výběr otázky
echo $otazky[$cislootazky];
echo '<input type="text" name="odpoved">';
echo '<input type="hidden" name="kontrolnihash" value="'.md5($odpovedi[$cislootazky].date('Ymd')).'">';
echo '<input type="submit" value="Odeslat"></form>';

A teď vyhodnocení:

if (md5($_POST['odpoved'].date('Ymd'))==$_POST['kontrolnihash'])
  ...v pořádku...
else
  ...špatná odpověď...

Živý spammer sice ze zdrojáku formuláře zjistí, jaký hash odpovídá dnešnímu datu a téhle odpovědi, ale samotné datum ani odpověď z něj nevykouká.

Jo, a máme tu pár nových funkcí: md5 vytvoří z daného řetězce hash (32znakový text z písmen '0'..'f'), rand(a,b) dává náhodné celé číslo z rozsahu a..b a date dává aktuální datum zformátované do daného tvaru (viz manuál). Kdyby nám šlo o bezpečnost (např. šifrování hesel), je lepší místo md5 použít crypt.

To by k captchám zatím mohlo stačit, teď si ještě probereme posledních pár taktik spamové války:

Omezení kadence příspěvků

Tohle už patří do kategorie "poslední záchrana, když všechny předchozí metody selžou". Jde o omezení počtu příspěvků, které se dají odeslat v určitém časovém intervalu (třeba během jednoho dne) z jednoho počítače. Prakticky jediná použitelná identifikace počítačů jsou v tomto případě IP adresy, protože na cookies se nám roboti nejspíš zvysoka vy-víte-co. Počítání přístupů a ukládání IP adres už jsme si nacvičili v předchozích dílech o počítadlech a anketách, takže by to pro vás neměl být problém. Pokud zjistíte překročení maximálního dovoleného počtu, přestanete příspěvky z téhle adresy ukládat a případně rovnou schováte i formulář. Ale počítejte s tím, že se takhle ubráníte jedině amatérům, co spamují ručně z domova. Jakmile je někdo schovaný za proxy serverem, který mu IP každou chvíli mění, nebo ovládá velkou síť počítačů s různými adresami (botnet), ochranu vám pohodlně obejde.

Mazání duplicit

Další poslední záchrana. Normální člověk vám do knihy nenafláká deset navlas stejných vzkazů. Maximálně tak dva, když se uklikne, a i tak je ten druhý zbytečný. Takže můžeme každý nový příspěvek porovnat s několika předchozími a pokud se s některým shoduje, zahodit ho. Nevýhoda je, že roboti svoje bláboly většinou aspoň nepatrně obměňují.

Na závěr...

Nebojte se, není to tak horké. Spammeři si umějí spočítat, kolik práce se jim ještě vyplatí vynaložit a kolik už ne. Nejvýhodnější je hacknout nějaký hodně rozšířený systém (jako třeba fórum PHPBB nebo návštěvní knihy od Blueboardu). Potom se ještě vyplatí zaspamovat nějaké velké a často navštěvované servery. Zbytek světa a amatérské stránky s jednotkami až desítkami návštěv za den už jim nestojí za individuální námahu. Raději si budou piplat svoje univerzální roboty, kteří zvládnou zaneřádit dostatečné množství dostatečně málo zajištěných stránek, ale na celý internet zatím nestačí.

A to je vše, přátelé!

Ještě bychom si mohli probrat mazání vzkazů z knihy, ale pro začátek si vystačíte s administračním rozhraním MySQL a časem vás určitě napadne, že se u každého příspěvku dá zobrazit nenápadný odkaz s jeho idčkem a nějak vhodně ho využít už by mělo být celkem jednoduché. Jestli ne, počkejte si na příští díl, kde si sestrojíme pořádné fórum s vlákny, registrovanými uživateli a administrátorským rozhraním (jestli se někdy dokopu k tomu, abych ho napsal).

Zpět

Reklamy: