10. díl - Anketa - Tvoříme anketu

PHP Databáze pro začátečníky Anketa - Tvoříme anketu

Vítejte u dalšího pokračování seriálu o tvorbě dynamických doplňků na webové stránky. Dnes se naučíme pracovat s IP adresami, probereme základy zabezpečení SQL a pár nových triků a funkcí.

Připomínám, že jednotlivé díly seriálu na sebe navazují a k jednou probrané látce už se nevracím. Začátečníkům proto doporučuji číst od začátku, jinak nemůžu zaručit, že se v tom vyznají.

Začněme rovnou příkladem

Minule jsme si napsali jednoduchoučké počítadlo přístupů, které při každém načtení stránky zvětšilo jedno číslo v databázi o 1 a zobrazilo ho. Tentokrát se pustíme do něčeho složitějšího: do ankety. V našem případě to bude udělátko, které vypíše jednu otázku a několik možných odpovědí a když některou z nich návštěvník vybere, v příslušné škatulce přibude jeden bod a anketa se zamkne, aby jeden člověk nemohl hlasovat víckrát než jednou.

Co k tomu budeme potřebovat?

Řekněme, že anket budeme chtít mít na stránce několik, vzájemně nezávislých a s různými otázkami i odpověďmi. To znamená, že musíme navrhnout tabulky, do kterých se nám vejde toto:

  • Text otázky, pro každou anketu jeden.
  • Texty odpovědí, pro každou anketu obecně libovolný počet.
  • Počty hlasů u jednotlivých odpovědí, tj. čísla, kterých bude stejný počet jako textů.
  • Pojistka proti vícenásobnému hlasování, pro každou anketu zvlášť.

Text otázky bude nějaký vhodný textový řetězec (stačí velice krátký, třeba TINYTEXT nebo nějaký VARCHAR), plus nějaký identifikační kód, který určuje, ke které anketě otázka patří. Dejme tomu, že si ankety budeme číslovat, takže ten kód může být třeba celé číslo; předpokládám, že nebudeme provozovat tisíce anket najednou, takže nám postačí i ten nejmenší TINYINT. Pro účely optimalizace ho můžeme označit za primární klíč (PRIMARY KEY), pro účely pohodlí můžeme ještě dodat AUTO_INCREMENT, aby se každé nově přidané anketní otázce automaticky přiřadil kód o 1 větší než té předchozí, ale nutné to není - pokud anket není moc, může být výhodnější číslovat si je ručně.
Odpovědi do stejné tabulky nacpat nemůžeme, protože nevíme, kolik jich ve které anketě bude, a proměnný počet sloupců se udělat nedá. Sice bychom si mohli připravit sloupce třeba pro dvacet odpovědí a u menších anket některé z nich nechat nevyužité, ale podle zákona schválnosti bychom stejně jednou zjistili, že jich potřebujeme nejméně dvacet jedna. Takže to uděláme jinak, odpovědi půjdou do samostatné tabulky.

Texty odpovědí a počty hlasů bude nejlepší držet hezky pohromadě v jedné tabulce. Odpověď bude zase nějaký krátký text, počet hlasů nějaké dostatečně velké přirozené číslo - třeba INT UNSIGNED. Potom samozřejmě potřebujeme kód ankety, abychom věděli, ke které to patří.
Mimochodem, tím nám vznikne přímo ukázkový vztah neboli relace (odtud pojem "relační databáze") mezi tabulkou otázek a tabulkou odpovědí. Vztah je typu 1:n, tedy k jedné otázce z první tabulky se váže libovolný počet odpovědí z tabulky druhé. Zároveň si tohle uspořádání můžeme představit i jako stromovitou hierarchickou strukturu: otázku jako kořen a odpovědi jako větve na stejné úrovni. To je dobré si zapamatovat: jakoukoli hierarchii můžeme v relační databázi nasimulovat tak, že si každý uzel pamatuje svého jediného nadřízeného.
Konec terminologické odbočky, zpátky k tabulce. Bude nám text, počet hlasů a kód ankety stačit? Teoreticky možná ano, ale je potřeba si uvědomit, že až návštěvník odešle svůj hlas, budeme ho muset nějak jednoznačně a pokud možno úsporně identifikovat. Posílat přes odkaz nebo formulář celý text odpovědi je krajně nepraktické, nehledě na to, že texty klidně můžou být v několika anketách stejné (jako třeba odpověď "Ano"). Také potřebujeme něco, podle čeho by se odpovědi v anketě řadily - ne vždy to chceme podle abecedy. Takže přidáme ještě unikátní identifikační kód, nejlépe číselný (v takovém případě můžeme s výhodou použít auto_increment, protože na odpovědi ruční číslování určitě potřebovat nebudeme).

Pojistku proti vícenásobnému hlasování si necháme na později, první pokus si pro jednoduchost napíšeme bez ní. Předem prozradím, že jenom přidáme třetí tabulku a pár podmínek navíc, první dvě tabulky zůstanou beze změny.

Takhle nějak by tedy mohly vypadat příkazy pro vytvoření tabulek otázek a odpovědí:

CREATE TABLE otazky
(
Otazka TINYTEXT,
KodAnkety TINYINT PRIMARY KEY
)

CREATE TABLE odpovedi
(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
KodAnkety TINYINT,
PocetHlasu INT UNSIGNED,
Odpoved TINYTEXT
)

Místo Tinytextu jsme stejně dobře mohli použít VARCHAR(něco), klidně i kratší než těch 255 znaků. Také jsme mohli u všech důležitých položek (kromě klíčů, tam je to zbytečné) dodat NOT NULL, aby se nedaly vložit prázdné. Ale to už jsou jenom drobnosti, které na další postup nemají žádný vliv. Na velikosti písmen v názvech sloupců nezáleží, takže např. PocetHlasu můžeme později psát třeba samými malými písmeny, nebude to vadit.

Tabulky máme, co dál?

Příprava ankety k použití

Dejme tomu, že na svých stránkách o lehké atletice chceme rozjet anketu o nejoblíbenější sport. Otázka je tedy celkem jasná, kód ankety si zvolíme třeba 1, protože je to naše první anketa. Vložení otázky do tabulky, by mohlo vypadat např. takto:

INSERT INTO otazky VALUES ('Jaký je váš nejoblíbenější sport?',1)

Vkládání odpovědí do druhé tabulky není o moc složitější:

INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Lyžování');
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Plavání');
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Běhání');
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Geohashing');
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Gaučing');

Hodnota NULL v prvním sloupci bude díky auto_incrementu automaticky nahrazena vzestupnou číselnou řadou (1, 2, 3...). Jednička u kódu ankety je jasná - odpovědi patří k výše uvedené otázce číslo jedna. Nula je počáteční počet hlasů, teoreticky bychom sem mohli dát i nějaké jiné číslo (pokud třeba přenášíme hlasy odjinud nebo pokud chceme statistiku trochu popostrčit vhodným směrem :-) ). Text odpovědi je jasný.

A teď co s tím. Buď tyto příkazy pustíme přímo přes nějaké webové rozhraní k MySQL (např. PHPMyAdmin), které obvykle bývá k dispozici; v takovém případě jenom pozor na kódování české diakritiky. Nebo bychom mohli každý řádek obalit do mysql_query, všechno uložit do jednoho PHP skriptu, ten nahrát na server a přes prohlížeč spustit. Tím by sice odpadly potenciální problémy s češtinou, ale kdo se s tím má pořád tak zdlouhavě patlat. Protože lenost je matka pokroku a protože se tím naučíme spoustu nových věcí, vyrobíme si na tvorbu anket administrátorské rozhraní se vším všudy. Ale protože to není ani nezbytně nutné ani úplně triviální, necháme si to až úplně na konec a nejdřív si napíšeme skript, který anketu zobrazí a zpracuje hlasy od návštěvníků.

Zobrazení ankety

Pro jednoduchost zatím necháme stranou estetickou stránku věci (obvyklý rámeček kolem ankety a podobně) a vypíšeme jenom nejdůležitější údaje uspořádané v neviditelné tabulce. První řádek (resp. záhlaví) tabulky bude vyhrazen pro otázku. Další řádky budou rozdělené do dvou buněk, v levé bude text odpovědi zároveň fungující jako klikatelný odkaz pro odeslání hlasu a v pravé bude aktuální počet hlasů pro tuto odpověď. Dopadnout to může dejme tomu takhle:

$cislo=1; //číslo ankety
$vysledek=mysql_query("SELECT otazka FROM otazky WHERE kodankety=$cislo",$spojeni);
$radek=mysql_fetch_row($vysledek);
echo '<table><tr><td colspan="2">'.$radek[0].'</td></tr>';

Proměnnou $cislo jsme si zavedli proto, aby se následující kód nemusel opisovat pro každou anketu zvlášť. Nakonec si z toho stejně uděláme funkci a číslo jí můžeme pohodlně předat jako parametr.

Druhým příkazem jsme si do proměnné $vysledek z databáze vytáhli všechny otázky, které mají kód ankety 1. Taková je samozřejmě jenom jedna, takže výsledek bude tabulka o velikosti 1x1. $spojeni je jako obvykle proměnná, kterou jsme dostali od funkce mysql_connect.

Třetí příkaz z návratové tabulky vytáhne první (a zároveň i jediný) řádek. Bude to pole všehovšudy s jedním prvkem, takže nám nehrozí popletení indexů a je jednodušší použít fetch_row, která dává pole indexované čísly.

Poslední příkaz vypíše HTML kód pro první řádek tabulky a do něj vloží z připraveného pole první (resp. nultou) položku - text otázky.

Pozn.: Uvnitř řetězců v uvozovkách se jména proměnných automaticky nahrazují jejich hodnotou, takže místo textu "$cislo" se v příkazu objeví jednička.

Otázka byla jednoduchá, teď odpovědi:

$vysledek=mysql_query("SELECT id, pocethlasu, odpoved FROM odpovedi WHERE kodankety=$cislo ORDER BY id ASC",$spojeni);
while ($radek=mysql_fetch_array($vysledek))
 {
 echo '<tr><td>';
 echo '<a href="anketa.php?hlasujpro='.$radek['id'].'">'.$radek['odpoved'].'</a>';
 echo '</td><td>';
 echo $radek['pocethlasu'];
 echo '</td></tr>';
 };
echo '</table>';

Pozor, tady se nám v SQL objevila nová konstrukce: ORDER BY neboli "seřaď podle". Tím jsme řekli, že v návratové tabulce $vysledek chceme mít řádky uspořádané podle hodnoty ve sloupci id, a to vzestupně (ASC jako ascending). Vzestupně znamená, že první řádek bude mít id nejmenší a poslední největší. Opačně by bylo DESC (descending). ASC je výchozí hodnota, takže by nám stačilo napsat jenom order by něco. Řadit samozřejmě můžeme podle čehokoli (texty by se braly abecedně). Kdybychom řazení neuvedli, výběr by sice asi přišel v takovém pořadí, v jakém byl do databáze vložen (což je to, co teď zrovna chceme), ale obecně se na to nedá moc spoléhat.

Následuje cyklus, který bude postupně vybírat z tabulky výsledků jednotlivé řádky a předávat je přes pole $radek, odkud si je vyzvedneme a po jednotlivých položkách zobrazíme.

Tvar hlasovacího odkazu jsem si zvolil, teoreticky by nemusel vypadat zrovna takhle. Po kliknutí přejde na skript anketa.php, což bude předpokládám přímo tenhle, který zrovna píšeme. Zároveň metodou Get předá proměnnou $_GET['hlasujpro'] a v ní hodnotu položky id od dané odpovědi.

Nakonec uzavřeme tabulku a jsme hotovi.

Zpracování hlasu

Návštěvník naší stránky si tedy nechal zobrazit anketu, klikl na jednu odpověď a my se s ní teď musíme vypořádat. Uděláme vlastně úplně totéž, jako v případě počítadla - najdeme v databázi jedno číslo a zvýšíme ho o 1:

if (isset($_GET['hlasujpro']))
 mysql_query("UPDATE odpovedi SET pocethlasu=pocethlasu+1 WHERE id=".$_GET['hlasujpro'],$spojeni);

Ovšem POZOR, tentokrát je v tom jeden velikánský háček: co kdyby nějakého chytráka napadlo prohlédnout si text odkazu, zamyslet se a do adresního řádku ručně naťukat třeba tohle:

http://nase.stran­ka.cz/anketa.php?hla­sujpro=1%20OR%2­05%3d5

%20 je kód mezery a %3d kód rovnítka, rozkódování proběhne automaticky. Po přímém dosazení do příkazu by databáze dostala tohle:

UPDATE odpovedi SET pocethlasu=po­cethlasu+1 WHERE id=1 OR 5=5

Podtržením je zvýrazněn celý zadaný vstup. Nepříjemné, že? Odkliknutí všech odpovědí ve všech anketách sice nemá žádný praktický smysl, ale zkuste si představit, že by takhle někdo převezl třeba podmínku testující administrátorské heslo.

Této technice útoku se říká SQL injection a je to asi nevětší nebezpečí, s jakým se v běžném životě setkáme a kterému se musíme vyhnout.

V našem případě je řešení celkem jednoduché. Očekáváme číselný kód, ale text '1 OR 5=5' má k číslu daleko, tak ho můžeme rovnou vyloučit. Kontrolu číselnosti zajistí funkce is_numeric, která vrací true v případě, že se parametr skládá pouze z číslic, případně s nějakým tím znaménkem nebo desetinnou tečkou:

if (is_numeric($_GET['hlasujpro']))
      mysql_query("UPDATE... atd., viz výše);
 else die('Co to na mě zkoušíš, podvodníku?');

Jiným způsobem zabezpečení je tzv. escapování, kdy se před všechny mezery, středníky, apostrofy, uvozovky a podobné potenciálně nebezpečné znaky předřadí znak \ (zpětné lomítko). Ale to má smysl především pro texty a podrobně si ho probereme někdy příště.

Tohle nám ovšem ještě na plnohodnotnou anketu nestačí, pokračujte prosím tudy.


 

  Aktivity (1)

Článek pro vás napsal Mircosoft
Avatar
Autor je amatérský pascalista, assemblerista a bastlíř. Profesionálně psal nebo píše v HLASM, Rexxu, Cobolu, ST, LAD, FBD, PHP, SQL, JS, Basicu a pár dalších jazycích, které kupodivu stále existují a používají se :-).

Jak se ti líbí článek?
Celkem (4 hlasů) :
55555


 


Miniatura
Předchozí článek
Počítadlo přístupů
Miniatura
Všechny články v sekci
Databáze v PHP pro začátečníky
Miniatura
Následující článek
Anketa - Vylepšujeme anketu

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!