Facebook RSS feed

11. díl - Anketa - Vylepšujeme anketu

Zpět do sekce Databáze v PHP pro začátečníky

(pokračování tohohle článku)

Pojistka proti vícenásobnému hlasování

Anketa by nám teď měla fungovat, ale má jednu nevýhodu: kdokoli do ní může naklikat libovolný počet jakýchkoli hlasů. To se nám ale nelíbí, chtěli bychom, aby každý mohl hlasovat jenom jednou. Jak to udělat?

Máme v podstatě dvě možnosti: buď si zapamatovat IP adresu hlasujícího počítače, nebo v něm nechat sušenku (cookie). Cookies mají výhodu v jednoznačnosti (nastaví se opravdu jenom na ten jeden počítač, ze kterého se hlasovalo, a nezablokuje anketu jiným), ale nevýhodu v tom, že si je uživatel může kdykoli smazat a hlasovat znovu. IP adresy sice nemusí být jednoznačné (víc počítačů může mít stejnou) ani trvalé (na stejném počítači se může měnit), ale zase jsou jednodušší na obsluhu a tak nějak blbuvzdornější. Proto si první ochranu založíme právě na IP adresách.

Postup bude celkem jednoduchý: vytvoříme si další tabulku, do které budeme ukládat IP adresy počítačů, ze kterých se hlasovalo, spolu s kódem ankety, aby nám odhlasování v jedné nezablokovalo všechny ostatní:

CREATE TABLE ipadresy
(
ip VARCHAR(15),
kod TINYINT
)

Položka ip je vlastní IP adresa. Od serveru ji dostaneme dostaneme v podobě textového řetězce obsahujícího čtyři čísla v rozsahu 0..255, oddělená tečkami (např. '123.255.20.1'). Varchar(15) jsem si zvolil proto, že na víc než 15 znaků to vyjít nemůže; teoreticky by stejně dobře posloužil třeba Tinytext.

Položka kod odpovídá kódu ankety. Záměrně jsem ji nepojmenoval KodAnkety, protože tuhle tabulku můžeme zároveň využít třeba pro počítadla přístupů (viz minule), stačí jim přidělit nějaké kódy, které se nebudou tlouct s existujícími anketami. Ale to už nechám na vás.

Aktuální IP adresu návštěvníka najdeme v superglobálním poli $_SERVER, konkrétně pod názvem $_SERVER['REMO­TE_ADDR'] (pozor, všechna písmena jsou velká!). To je všechno, co potřebujeme, můžeme se pustit do práce. Po odhlasování uložíme adresu do tabulky následujícím způsobem:

mysql_query("INSERT INTO ipadresy VALUES ('".$_SERVER['REMOTE_ADDR']."',$cislo)",$spojeni);

Povšimněte si apostrofů připravených okolo místa, do kterého vkládáme adresu - bez nich by to nešlo, je to text. Vzhledem ke složitosti zápisu téhle proměnné jsem ji radši připojil pomocí teček, automatickému rozbalování zas až tak moc nevěřím :-].

$cislo na konci je kód ankety. Ovšem pozor - kde jsme ho vlastně vzali? V tuhle chvíli máme po ruce jenom id odeslané odpovědi, proměnnou $_GET['hlasujpro']. Nezbývá, než si ho ještě před pokusem o uložení ajpiny v příslušné tabulce najít:

$vysledek=mysql_query("SELECT kodankety FROM odpovedi WHERE id=".$_GET['hlasujpro'],$spojeni);
if ($radek=mysql_fetch_row($vysledek)) $cislo=$radek[0];
  else die('Tuhle odpověď nemáme v databázi!');

Prvním příkazem jsme získali buď tabulku 1×1 obsahující hledaný kód, nebo nic, pokud odpověď s daným id nebyla v databázi nalezena (to se může stát snad jenom při ručním hraní s adresním řádkem). Samozřejmě předpokládám, že proměnnou $_GET['hlasujpro'] už touhle dobou máme důkladně zkontrolovanou a víme, že je to platné číslo. Jestli ne, ať vás ani nenapadne strkat ji do SQL!

Druhý příkaz se pokusí z načtené jednobuňkové minitabulky vytáhnout hledaný kód. Kdyby se mu to nepovedlo, zahlásí chybu a ukončí skript (to samozřejmě není jediná možnost, jak se s chybou vyrovnat).

Shrneme to: podle čísla odpovědi jsme si našli kód ankety a spolu s IP adresou jsme ho uložili do databáze. Tím máme hotovou poslední fázi zpracování hlasu. Ještě se ale musíme vrátit na začátek, do okamžiku, kdy jsme dostali číslo odpovědi, zkontrolovali ho a teď se rozmýšlíme, jestli tenhle hlas započítáme nebo jestli už má daná adresa odhlasováno. Bude to chtít kontrolu, jestli už máme tuhle adresu uloženou a hlas zahodíme, nebo jestli ji ještě nemáme, takže ji uložíme a hlas započítáme:

$vysledek=mysql_query("SELECT * FROM ipadresy WHERE ip='".$_SERVER['REMOTE_ADDR']."' AND kod=".$cislo,$spojeni);
if (mysql_num_rows($vysledek)==0) ...tenhle tu ještě nebyl, jeho hlas uložíme...
  else ...už ho tu máme uloženého, další hlasy od něj ignorujeme...

První příkaz zkusí z tabulky IP adres vytáhnout kombinaci zpracovávané ankety a aktuální adresy návštěvníka. Pokud tam taková kombinace není (tj. ještě nebylo hlasováno), vrátí prázdnou tabulku s nulovým počtem řádků. Pokud tam je, vrátí nám jednořádkovou tabulku. Dovnitř do ní vůbec nemusíme koukat (stejně víme, co tam je - přesně to, co jsme zadali do té podmínky). Pozor, nestačí ověřit $vysledek na true nebo false - true dostaneme vždycky, i když se nevrátí žádná data! False by se objevilo jenom při chybě.

Pozn.: teoreticky samozřejmě můžeme místo té hvězdičky napsat jméno některého sloupce, ale výsledný efekt by byl stejný: buď dostaneme něco nebo nic.

Poslední drobnost, ke které se uložené adresy dají použít, je to, že anketu, ve které už se nedá hlasovat, vykreslíme rovnou bez klikatelných odkazů, aby to návštěvníky nemátlo - takovéhle věci je lepší se dozvědět na první pohled a ne až po zdlouhavém kliknutí. To bude záležitost toho kousku, kde vypisujeme jednotlivé odpovědi: prostě v takovém případě vynecháme <a>...</a>.

Stop, mám v tom guláš! Jak to všecho patří dohromady?

Pravda, zatím jsme probírali jednotlivé detaily a celek se ztrácí kdesi v nedohlednu. Tak tedy: všechno máme v jednom skriptu, který dělá tohle:

  1. Přijetí hlasu ($_GET['hlasuj­pro']). Pokud přišel, zkontrolujeme jeho platnost (is_numeric), najdeme k němu kód ankety a obě čísla si uložíme do nějakých vhodných proměnných. Pokud nepřišel nebo není platný, přeskočíme jeho zpracování.
  2. Kontrola, jestli tenhle návštěvník ve zvolené anketě může hlasovat.
  3. Zpracování hlasu (pokud přišel, je platný a může se hlasovat):
    1. Zvyš počet hlasů u zvolené odpovědi.
    2. Návštěvníkovu IP adresu ulož do tabulky adres.
  4. Cyklus pro každou anketu, kterou na téhle stránce máme:
    1. Zkontroluj, jestli tenhle návštěvník v téhle anketě může hlasovat.
    2. Pokud může, vypiš anketu s odpověďmi ve formě aktivních odkazů. Pokud ne, vypiš odpovědi jako neaktivní text.

Jak vidíte, kontrolovat přítomnost adresy v tabulce musíme nejméně dvakrát: jednou na začátku skriptu při zpracovávání hlasu a potom jednou pro každou vykreslovanou anketu.

Kosmetické detaily

Počty hlasů v anketách se obvykle znázorňují graficky pomocí různých sloupečků nebo barevných žížalek. To by v tom byl čert, abychom to nedokázali taky!

Teoreticky by se vodorovný pruh asi dal vyrobit i pomocí vhodně ostylovaných HTML elementů hr nebo div, ale nejmodernější CSS není moje silná stránka a navíc by to nebylo zrovna nejpružnější, takže použijeme klasické obrázky - element img. Využijeme toho, že se obrázku dají nastavit libovolné rozměry a nemusí se dodržovat ani poměr stran. To znamená, že můžeme mít fyzicky uložený jenom krátký úsek (jestli nepotřebujeme barevné přechody mezi levým a pravým koncem, může to být třeba jenom jednopixelová nudle) a roztáhneme si ho podle potřeby. Pokud máme konce proužku nějak tvarově odlišené (kulaté, stínované, plastické apod.), roztažení by jim uškodilo, takže je musíme uložit zvlášť. Měli bychom tedy celkem tři malé obrázky: pevný levý konec, roztažitelný střed a pevný pravý konec. Dejme tomu, že je máme na serveru uložené pod jmény levykonec.gif, stred.gif a pravykonec.gif a že jsou všechny 10 pixelů vysoké.

První otázka je, jak dlouhé proužky udělat. Nabízí se triviální možnost 1 hlas = 1 pixel, ale to by mělo dvě nevýhody: zaprvé by při malých počtech hlasů byly proužky moc krátké a rozdíly v délkách by nebyly pouhým okem viditelné, zadruhé by se naopak při hodně vysokých počtech anketa neomezeně roztahovala. Potřebujeme tedy nějaký přepočet, který maximální délku omezí a zároveň zajistí, aby každý hlas byl vidět. Nejjednodušší je prohlásit, že odpověď s největším počtem hlasů bude odpovídat maximální šířce proužku, a všechny ostatní šířky trojčlenkou smrsknout nebo roztáhnout v příslušném poměru (tak to dělají například diskusní fóra PHPBB):

max. počet hlasů    nějaký jiný počet hlasů
------------------ = -----------------------
max. šířka proužku    hledaná šířka proužku

neboli po úpravě:

hledaná šířka proužku = nějaký jiný počet hlasů * max. šířka proužku / max. počet hlasů

Jediná výjimečná situace, na kterou si musíme dát pozor, je počáteční stav ankety, kdy jsou všechny počty a tedy i maximum nulové. Asi nemusím připomínat, že dělení nulou by nedopadlo dobře ;-).

Teď ještě jak to naprogramovat (v proměnné $cislo máme kód ankety):

$vysledek=mysql_query("SELECT MAX(pocethlasu) FROM odpovedi WHERE kodankety=$cislo",$spojeni);
if (!($radek=mysql_fetch_row($vysledek)) or (($maximum=$radek[0])==null]))
  die('Tuhle anketu nemáme na skladě.');

Tady se nám objevuje nová funkce: MAX(sloupec) nám dá největší hodnotu z daného sloupce, výsledek dostaneme ve formě jednobuňkové návratové tabulky. Pokud by dané podmínce (where) neodpovídaly žádné řádky a maximum tedy nebylo z čeho počítat, v návratové tabulce bude hodnota null (pozor, návratová tabulka by existovala a obsahovala by hodnotu, akorát že by ta hodnota byla null - nestačí otestovat fetch na true/false). Obdobně funguje MIN(), která dává minimum.

Další věc, která možná potřebuje trochu vysvětlit (aspoň pro ne-céčkaře), je ten divoký logický výraz v závorce ifu. Takže: nejdřív se do proměnné $radek načte obsah návratové tabulky. Kdyby byla prázdná, celý ten přiřazovací výraz by dal hodnotu false a po negaci true. Druhá část podmínky by se vůbec neuplatnila (na výsledku oru by stejně nic nezměnila), takže by se rovnou provedlo die(). Jestli prázdná nebyla, pokračuje se druhou podmínkou. V ní se naplní proměnná $maximum, výsledná hodnota tohoto přiřazovacího výrazu (která je rovná tomu, co bylo přiřazeno) se hned porovná s nullem a jestli souhlasí, vyjde logické true a voláme die.

Na hlavičku ankety se naše kosmetické úpravy nevztahují, tak se mrkneme rovnou na odpovědi. Dejme tomu, že proužek zobrazíme ve stejné buňce přímo nad otázkou. Zároveň také rovnou provedeme dříve zmíněné vynechání odkazů, pokud už se hlasovalo (to je ta proměnná $muze_hlasovat; předpokládám, že už ji máme připravenou z dřívějška):

$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>';
 //proužek:
 echo '<img src="levykonec.gif">';
 if ($maximum==0) $sirka=0; //pojistka proti dělení nulou
             else $sirka=round($radek['pocethlasu']*200/$maximum);
 echo '<img src="stred.gif" height="10" width="'.$sirka.'">';
 echo '<img src="pravykonec.gif"><br>';
 //otázka a počet hlasů:
 if ($muze_hlasovat) echo '<a href="anketa.php?hlasujpro='.$radek['id'].'">';
 echo $radek['odpoved'];
 if ($muze_hlasovat) echo '</a>';
 echo '</td><td>';
 echo $radek['pocethlasu'];
 echo '</td></tr>';
 };

Zakončení tabulky a podobné formality už nechám na vás.

200 je požadovaná maximální šířka proužku v pixelech. Hodnotu jsem si zvolil, může to být samozřejmě i cokoli jiného. 10 je požadovaná výška proužku; zadat se musí, jinak by většina prohlížečů roztáhla obrázek i svisle, aby se zachoval poměr stran. Výška proužku by pochopitelně měla být stejná jako výška levého a pravého konce, na které má navazovat.

Použili jsme novou funkci: round(číslo), která dané číslo zaokrouhlí na celé. To je nutné, protože šířka obrázku musí být zadána v celých pixelech.


Poslední věc, která stojí za zmínku, je možnost zabalit obsluhu anket do funkcí (viz dříve). Pokud si napíšeme jednu funkci pro zpracování hlasu a druhou pro zobrazení jedné ankety (její kód by dostala v parametru), výrazně si zpřehledníme zápis v hlavní části skriptu. Navíc pokud se nám podaří po vyhodnocení hlasu skočit zpátky na původní stránku (napadá mě předat jméno cílové stránky jako další parametr zobrazovací funkce, ale možná existuje i něco pohodlnějšího), můžeme si anketní funkce schovat do samostatného souboru a ten pak podle potřeby includovat a využívat na všech ostatních stránkách.

Tím jsme s provozní částí anket víceméně hotovi. Zbývá sestavit si nějaký skript, který by nám usnadnil jejich tvorbu a úpravy. Pokračujte prosím tudy.


 

Článek pro vás napsal Mircosoft
Avatar
Autor se věnuje amatérskému tvoření v Turbo Pascalu, Assembleru a PHP a pár let se profesionálně šťoural v jiném Assembleru, Rexxu, Cobolu a podobných obskurnostech.

Jak se vám líbí článek?
Celkem (1 hlasů):
55555


 


Předchozí článek
Anketa - Tvoříme anketu
Další možnost využití databází. Vytvoříme si základ webové ankety a dozvíme se o nebezpečí zvaném SQL injection.
Všechny články v sekci
Databáze v PHP pro začátečníky
Sekce obsahuje tutoriály pro práci s nejen MySQL databází pro začátečníky. V seriálu vytvoříme jednoduchý redakční systém, knihu návštěv a anketu.
Další článek
Anketa - Administrační rozhraní
Abychom svoje ankety nemuseli spravovat ručně. Spousta kódu! Nové funkce! Může obsahovat stopy oříšků!


 

 

Vaše komentáře:

Avatar
Vojta Pšenák
Moderátor
Avatar
Vojta Pšenák:

Ahoj, nějak mi nejde ta kontrola IP. Nejde mi uspořádat kód , nemohl by jsi ho sem dát?

Odpovědět 11.5.2012
Prosím všechny, kdo klikají na i--;, aby mi napsali s čím nesouhlasí
Avatar
Mircosoft
Redaktor
Avatar
Odpovídá na Vojta Pšenák
Mircosoft:

Hotové ankety dostaneš na Blueboardu. Tohle je výukový seriál a uspořádání kódu jsi dostal za domácí úkol 8-) .

Ale že jsi to ty: http://mircosoft.mzf.cz/…load/php.zip (neručím za přehlednost, psal jsem to pro vlastní potřebu)

 
Odpovědět  +1 14.5.2012
Avatar
David Čápka
Moderátor
Avatar
Odpovídá na Mircosoft
David Čápka:

Klidně bych sem ty zdrojáky dal ke stažení, chytří se z nich přiučí a těm, kteří jim nerozumí, to stejně nepomůže :)

Jinak jsi mě pobavil tím "že jsi to ty" :D

Odpovědět 14.5.2012
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Vojta Pšenák
Moderátor
Avatar
Odpovědět 18.5.2012
Prosím všechny, kdo klikají na i--;, aby mi napsali s čím nesouhlasí
Avatar
oggymotslp
Člen
Avatar
oggymotslp:

Moc složité :D

Odpovědět 10.června
Proč to dělat jednoduše, když to jde i složitě :D

 

Zobrazeno 5 z 5 zpráv

Přidat novou zprávu

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řihlaš. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.