Jako zákusek si dáme koláčky

Jiří Kosek ml.

Dnes nás čeká poslední díl seriálu, jehož cílem bylo seznámit vás se základy tvorby internetových aplikací na straně serveru. Na závěr se budeme věnovat koláčkům -- v originále cookies -- což je velice zajímavá technologie, která dala název i dnešnímu článku. Zopakujme si nejprve stručně na jakém principu cookies pracují.

Samotný protokol HTTP je bezstavový -- pro stažení každé stránky se musí vytvořit nové spojení, po kterém se obsah stránky přenese. Server tedy neví, zda danou stránku posílá podesáté jednomu uživateli anebo zda stránku desetkrát poskytl různým uživatelům. Přitom na mnoha stránkách by šlo tuto informaci využít. Server by monitoroval pohyb uživatele po stránkách a na základě jeho vyhodnocení by uživateli poskytoval personalizované stránky -- např. se specifickou reklamou či informacemi.

Výše zmíněného nedostatku si byla vědoma firma Netscape a vytvořila proto cookies, které brzy začaly podporovat i všechny další významné firmy ve svých prohlížečích. Cookies jsou rozšířením protokolu HTTP -- přidávají do něj dvě nové hlavičky.

První hlavičku Set-Cookie používá server při odesílání odpovědi prohlížeči. V této hlavičce může server prohlížeči poslat informace. Získané informace si prohlížeč uloží do souboru. Pokud pak uživatel v budoucnu přistupuje ke stejnému serveru, prohlížeč uložené informace pošle zpět serveru v hlavičce Cookie. Server na základě obdržených informací v cookies zjistí, že uživatel se k serveru připojil již dříve a může na to patřičně zareagovat.

Každá cookie má své jméno a můžeme do ní uložit hodnotu. Kromě toho lze u každé cookie nastavit dobu její platnosti a další atributy.

Práce s cookies je v PHP velice snadná. K odeslání cookie klientovi slouží funkce SetCookie(). Tuto funkci musíme volat ještě předtím, než náš skript generuje nějaký výstup v HTML, protože cookies jsou součástí HTTP-hlaviček.

Naopak cookies, které v požadavku na náš skript odeslal prohlížeč máme přístupné v proměnných odpovídajícího jména podobně jako pole formuláře.

Použití cookies si ukážeme na jednoduchém příkladě -- vytvoříme stránku, která každému uživateli zobrazí, kolikrát ji navštívil:

<?
  if (!IsSet($pocetPristupu)) 
      $pocetPristupu = 0;
  $pocetPristupu++;
  SetCookie("pocetPristupu", $pocetPristupu);
?>
<HTML>
<HEAD>
<TITLE>Vítejte na serveru s podporou koláčků</TITLE>
</HEAD>
<BODY>
<?
  if ($pocetPristupu==1):
?>
    <H1>Vítejte nový uživateli</H1>
<?else:?>
    <H1>Ahoj starý brachu -- my už se známe</H1>
    Na našem serveru jste již 
    po <?echo $pocetPristupu?>.
<?endif?>
</BODY>
</HTML>
Skript nejprve testuje, zda mu prohlížeč poslal cookie s názvem pocetPristupu. Pokud ne, zinicializuje proměnnou $pocetPristupu. Následně počet přístupů zaktualizujeme a odešleme zpět klientovi. Zobrazená stránka se liší podle toho, zda uživatel ke stránce přistupuje poprvé nebo již poněkolikáté (viz obr. 1 a 2).

Obr. 1: První přístup na stránku s cookies
První přístup na stránku s cookies

Obr. 2: Další přístupy na stránku s cookies
Další přístupy na stránku s cookies

Funkce SetCookie má i několik nepovinných parametrů. Celkem můžeme použít šest následujících parametrů:

SetCookie(jméno, hodnota, platnost,
          cesta, doména, zabezpečení)
Platnost udává časový okamžik, do kdy je obsah cookie platný. Čas se udává jako počet sekund od začátku 1. ledna 1970. S výhodou můžeme použít funkci Time(), který vrací aktuální čas v tomto formátu. K nastavení cookie s platností jedna hodina (3600 sekund) můžeme použít volání:
SetCookie("Kategorie", "obchod", Time()+3600)
Pomocí parametrů cesta a doména můžeme rozšířit platnost cookie. Normálně je cookie platná -- je zasílána prohlížečem zpět serveru -- pouze pokud se shoduje doména a úvodní část cesty ke skriptu, který cookie odeslal prohlížeči. Pokud chceme rozšířit platnost cookie na celý server, použijeme jako parametr cesta lomítko '/'. Pomocí parametru doména můžeme rozšířit platnost cookie na celou doménu. Pokud jako hodnotu uvedeme např. 'firma.cz', bude cookie platná pro všechny servery v doméně firma.cz.

Pokud jako hodnotu posledního parametru zabezpečení, uvedeme true, bude cookie zaslána pouze, pokud je mezi serverem a klientem vytvořeno bezpečné spojení pomocí SSL (Secure Socket Layer).

Cookies můžeme z prohlížeče smazat dvěma způsoby -- buď jako hodnotu cookie pošleme prázdný řetězec nebo platnost cookie nastavíme do minulosti. V obou dvou případech je nepotřebná cookie odstraněna z prohlížeče a nezabírá zbytečné místo na disku uživatele.

Použití cookies si nyní ukážeme na složitějším příkladě. Naším úkolem bude vytvořit server, který si od každého uživatele při vstupu na hlavní stránku vyžádá zodpovězení jedné z otázek. Otázka bude pro uživatele vybrána náhodně, ale zároveň bychom jednomu uživateli neměli vícekrát pokládat tutéž otázku.

Budeme dále předpokládat, že jednotlivé otázky máme uloženy v databázové tabulce Dotaznik. U každé otázky máme uloženo její identifikační číslo, znění a počet hlasů pro a proti.

V řešení našeho úkolu nám výborně pomohou cookies, protože právě pomocí nich si budeme u každého uživatele evidovat, na které otázky již odpověděl. Z databáze při přihlášení k serveru vybereme vždy dosud nepokládanou otázku -- to zamezí zbytečnému zkreslení výsledků tím, že někdo vícekrát odpoví na jednu otázku.

Protože pro každého uživatele potřebujeme evidovat větší počet zodpovězených otázek, využijeme toho, že více cookies může mít stejné jméno a různou hodnotu -- v PHP s nimi pak pracujeme jako s polem. My si cookie pojmenujeme zodpovezeneDotazy. Následující skript z tabulky náhodně vybere jeden dosud nepoložený dotaz a zobrazí jej:

<HTML>
<HEAD>
<TITLE>NÁZORY.CZ</TITLE>
</HEAD>
<BODY>
<?
  $sql = "select * from Dotaznik";
  if (IsSet($zodpovezeneDotazy))
      $sql .= " where ID not in (".
              Implode($zodpovezeneDotazy, ",").
              ")";
  do {              
    MySQL_Connect("localhost");
    $vysledek = MySQL("test", $sql);
    $pocet = MySQL_NumRows($vysledek);
    if ($pocet==0) break;
    SRand((double)MicroTime()*1e6);
    $aktualni = Rand() % $pocet;
    $id = MySQL_Result($vysledek, $aktualni, "ID");
    $dotaz = MySQL_Result($vysledek, $aktualni, "Otazka");
  } while (false);

  if (IsSet($id)): ?>
  
  <H1>Pro vstup na server odpovězte na následující otázku:</H1>
  <FORM ACTION="17-03.php3">
  <?echo $dotaz?><BR><BR>
  <INPUT TYPE=Submit NAME=Odpoved VALUE="Ano">
  <INPUT TYPE=Submit NAME=Odpoved VALUE="Ne">
  <INPUT TYPE=Hidden NAME=id VALUE="<?echo $id?>">
  </FORM>
  
<?else:?>  

  <H1>Dnes to bude bez otázky</H1>
  <A HREF="17-03.php3">Vstupte na náš server</A>
<?endif?>
</BODY>
</HTML>
Pochopení skriptu nechám na laskavém čtenáři, zmíním se jen o pár funkcích PHP, se kterými jsme se dosud nesetkali. Funkce IsSet() zjišťuje, zda daná proměnná obsahuje nějakou hodnotu. Funkce Implode() vezme jednotlivé prvky pole a navzájem je spojí do jednoho řetězce -- k oddělení prvků pole v řetězci se používá znak předaný jako druhý parametr. Voláním Implode($zodpovezeneDotazy, ",") tedy získáme seznam identifikačních čísel již zodpovězených dotazů, který s výhodou použijeme při zadávání SQL-dotazu, který vybírá pouze dosud nepoložené otázky.

Příkaz SRand((double)MicroTime()*1e6) zinicializuje generátor náhodných čísel. Funkce Rand() vrací náhodné číslo. Použitím operátoru modulo (zbytek po dělení) upravíme náhodné číslo na potřebný rozsah.

Obr. 3: Náhodně vybraná otázka
Náhodně vybraná otázka

Nyní nadešel pravý čas pro skript, který zpracuje odpověď uživatele. Skript 17-03.php3 má na starosti mnoho věcí -- předně musí klientovi odeslat cookie, která obsahuje číslo zodpovězené otázky. Poté musí v databázi aktualizovat počet odpovědí pro/proti u dané otázky. Nakonec skript vypíše přehled odpovědí na všechny otázky, abychom měli přehled.

<?
  if(IsSet($id))
    SetCookie("zodpovezeneDotazy[$id]", $id, Time()+2592000);
?>    
<HTML>
<HEAD>
<TITLE>NÁZORY.CZ</TITLE>
</HEAD>
<BODY>
<H1>Výsledky hlasování pro všechny dotazy</H1>
<TABLE CELLSPACING=0 BGCOLOR=BLACK>
<TR BGCOLOR=YELLOW><TH>Otázka<TH WIDTH=120>Ano<TH WIDTH=120>Ne</TR>
<?
  MySQL_Connect("localhost");
  $sql = "update Dotaznik";
  if ($Odpoved=="Ano") 
    $sql .= " set Ano = Ano + 1";
  else
    $sql .= " set Ne = Ne + 1"; 
  $sql .= " where ID = $id";
  $vysledek = MySQL("test", $sql);
  if (!$vysledek) 
    echo "Nepodařilo se zapsat vaši odpověď.";
  $sql = "select * from Dotaznik order by Otazka";
  $vysledek = MySQL("test", $sql);
  $pocet = MySQL_NumRows($vysledek);
  define("SirkaGrafu", 100);
  for ($i=0; $i<$pocet; $i++):
    $ano = MySQL_Result($vysledek, $i, "Ano");
    $ne = MySQL_Result($vysledek, $i, "Ne");
    echo 
      "<TR><TD COLSPAN=3></TR>".
      "<TR VALIGN=TOP BGCOLOR='#FFFF80'><TD>".
      MySQL_Result($vysledek, $i, "Otazka").
      "<TD ALIGN=RIGHT>$ano&nbsp;".
      ($ano ? 
        "<IMG SRC=bluedot.gif WIDTH=".
        Round(SirkaGrafu/($ano+$ne)*$ano).
        " HEIGHT=10>" : "").
      "<TD ALIGN=LEFT>". 
      ($ne ?  
        "<IMG SRC=reddot.gif WIDTH=".
        Round(SirkaGrafu/($ano+$ne)*$ne).
        " HEIGHT=10>" : "")."&nbsp;$ne".
      "</TR>";
  endfor;
?>
</TABLE>
</BODY>
</HTML>
U odesílané cookie jsme nastavili platnost na 30 dní -- nepředpokládáme, že jeden dotaz bude na serveru delší dobu a je zbytečné, aby byl prohlížeč uživatele zavalen nepotřebnými cookies. V hranatých závorkách za názvem cookie uvádíme jedinečný index -- PHP nám následně umožní s cookie pracovat jako s polem (to využíváme v prvním skriptu).

Obr. 4: Výsledky hlasování
Výsledky hlasování

Při psaní aplikací nesmíme však na cookies spoléhat. U většiny prohlížečů lze podporu cookies vypnout a někteří uživatelé dělají, protože se zcela neoprávněně bojí úniku osobních informací. Profesionální aplikace by si měla poradit i s touto situací a pracovat správně -- třeba jen s omezeným uživatelským komfortem.

Zákusek máme úspěšně za sebou. Končí i seriál Aplikace na Webu, se kterým jste na stránkách Computerworldu setkávali více než půl roku. Doufám, že získané poznatky vám byly k užitku a že se brzy společně setkáme u dalších článků věnovaných webovým technologiím. Nečekejte však s rukama složenýma v klíně a napište nám, se kterými tématy a technologiemi byste se nejraději seznámili.

© Jiří Kosek 1999