Nákupní košík v cookie

Školení, která pořádám

Před třemi lety jsem psal, že obsah nákupního košíku je nejlepší ukládat do session proměnné. Ve skutečnosti je ale ve většině případů lepší ho ukládat do cookie hned ze dvou důvodů:

  1. Cookie můžeme nastavit libovolnou dobu platnosti, takže obsah košíku nezmizí po zavření prohlížeče. Jde to sice zajistit i u session proměnných, ale jejich obsah potom překáží na serveru, takže je lepší session proměnné používat skutečně jen pro aktuální sezení.
  2. Cookie se dá nastavit na straně klienta, takže pro přidání zboží do košíku nemusíme komunikovat se serverem.

Do cookies lze uložit pouze řetězec, proto obsah košíku budeme ukládat jako seznam ID oddělených čárkou.

<script type="text/javascript">
function kosik_pridat(id) {
	var match = /(^|;)kosik=([^;]+)/.exec(document.cookie);
	document.cookie = 'kosik=' + (match ? match[2] + ',' : '') + id + ';max-age=' + (60*60*24*30);
	document.getElementById('pocet').innerHTML = +document.getElementById('pocet').innerHTML + 1;
	alert('Položka byla přidána do košíku.');
	return true;
}
</script>
Položek v košíku: <span id="pocet"><?php echo ($_COOKIE["kosik"] ? substr_count($_COOKIE["kosik"], ",") + 1 : 0); ?></span>
<form action="" method="post" onsubmit="return !kosik_pridat(<?php echo $id; ?>);">
<p><input type="hidden" name="kosik" value="1" /><input type="submit" value="Přidat do košíku" /></p>
</form>

Na straně serveru ještě ošetříme případ, kdy má uživatel vypnutý JavaScript:

<?php
if ($_POST["kosik"]) {
    $_COOKIE["kosik"] = ($_COOKIE["kosik"] ? "$_COOKIE[kosik]," : "") . $id;
    setcookie("kosik", $_COOKIE["kosik"], strtotime("+30 day"));
}
?>

Součástí formuláře a skriptu by měla být obrana proti CSRF.

Pokud chceme vypsat obsah košíku, nesmíme zapomenout na ošetření dat a nejednotkový počet kusů:

<?php
if (preg_match('~^[0-9]+(,[0-9]+)*$~', $_COOKIE["kosik"])) {
    $kosik = array_count_values(explode(",", $_COOKIE["kosik"]));
    $result = mysql_query("SELECT id, nazev FROM zbozi WHERE id IN ($_COOKIE[kosik])");
    while ($row = mysql_fetch_assoc($result)) {
        echo htmlspecialchars($row["nazev"]) . " (" . $kosik[$row["id"]] . ")<br />\n";
    }
    mysql_free_result($result);
}
?>

Pokud by hrozilo, že položek do košíku budeme ukládat větší množství, bylo by vhodnější formát cookie upravit např. na id:počet.

Jakub Vrána, Řešení problému, 22.10.2008, diskuse: 26 (nové: 0)

Diskuse

Petr 'PePa' Pavel:

S argumenty souhlasím, ale člověk si musí dát pozor, aby nepřešvihnul limity cookie:
* celkový počet 300 cookies
* 4096 bytů na jednu cookie
* 20 cookies na doménové jméno

RFC to definuje jako minimální počty, ale už se mi podařilo cookie přeplnit, takže v praci je třeba to brát opravdu jako maximální hranice:
http://www.ietf.org/rfc/rfc2109.txt

David:

Z mého pohledu je toto řešení dobré jen v minimech případů.

Pokud máte registrovaného zákazníka, neukládal bych nákupní košík do cookie v žádném případě. Data bych uchoval na straně aplikace, tedy serveru, tedy nějak vhodně v databázi.
A proč ne do cookie?
Protože zákazník takového obchodu může používat víc počítačů. Řekněme, že si naplní košík v práci v čase obědové pauzy a z domova pak bude chtít nákup dokončit. Nebo ještě běžnější případ, jde o počítač, který používá více uživatelů najednou pod jedním systémovým účtem.

A ještě drobnost, měl byste čtenáře upozornit, že výše uvedený kód umožňuje útok na takovou aplikaci pomocí SQL injection.
(Samozřejmě chápu, že pro názornost je kód zjednodušený, ale minimálně bych na tuto skutečnost upozornil. Kód mohou číst začátečníci, kterým tato skutečnost nemusí na první pohled dojít.)

David:

Aha, omlouvám se, SQL injection beru zpět. To by měl pobrat preg_match. Moje chyba, nevšiml jsem si. Ještě jednou se omlouvám.

Leoš Ondra:

"A proč ne do cookie?
Protože zákazník takového obchodu může používat víc počítačů. Řekněme, že si naplní košík v práci v čase obědové pauzy a z domova pak bude chtít nákup dokončit."

Pak mu nepomuzou ani session, ty jsou vazane na cookie se session id a cookies zase na konkretniho http klienta. Navic session muze mezitim vyexpirovat. Takze byste to musel strkat do trvalejsiho uloziste nez je session promenna, a to je databaze - otazka je, jestli to neni pro nakupni kosik trosku overkill jak rikaji ti, co pouzivaji cizi termity.

David:

Udělal jste podobnou chybu jako já (nevšiml jsem si ochrany proti sql injection).
Četl jste nepozorně, nemluvil jsem o ukládání do session, ale například do databáze. A mluvil jsem o registrovaném, neboli přihlášeném uživateli.

Pokud se bavíme o uživateli, který se neregistroval, nepřihlásil, respektive ho nemohu identifikovat, předpokládám, že je k tomu nějaký důvod.
A data, která po něm někde mohou zůstat, považuji za škodlivá, mohou být totiž zneužita. V ten moment nechráním, lépe řečeno, nerespektuji jeho soukromí.

Toť můj skromný názor.

Leoš Ondra:

Pravda, prehledl jsem tu databazi :-)

Keff:

Overkill? Rozhodně ne! Pokud mám eshop, je pro mě uživatel který si dal něco do košíku tím nejdůležitějším člověkem na světě - a pokud mu košík vysypu, je pro něj nulová námaha znovu si stejný výrobek najít u konkurence (zatímco plný košík už se leckomu nechce plnit znova jinde kvůli pár desetikorunám na pár tisíc ceny).

Kamarád si teď takhle vybírá PC - už měsíc má v košíku naházené díly, čte recenze, přihazuje a odebírá, a až bude se sestavou spokojený, nakoupí. Když mu někdo vysype košík, přišel právě o zisk ze slušného PC.

Keff:

(a navíc, vsadím se, že profesionální eshopy moc zajímá které věci si lidé strkají do košíku, a rozhodně takovou informaci nenechají klientově browseru)

ikona Jakub Vrána OpenID:

Ukládat obsah košíku do databáze považuji za rozumné u přihlášených uživatelů. Používat košík by ale měli mít možnost i nepřihlášení - rozumné je u přihlášených používat jeden způsob a u nepřihlášených druhý (spolu se zohledněnmí toho, když se nepřihlášený s naplněným košíkem přihlásí).

gogloid:

No SQL injection třeba problém není, ale je to náchylné na přidávání položek z jiné stránky (něco jako CSFR ale bez requestu). Útočník jednoduše na své stránce může třeba přidat do košíku další položky, vymazat je, atd. To považuji za největší problém.

ikona Jakub Vrána OpenID:

Ano, ochrana proti CSRF by byla nedílnou součástí serverové části skriptu. Zde na blogu je to myslím dostatečně popsané (http://php.vrana.cz/cross-site-request-forgery.php), do ukázky jsem to v zájmu názornosti nedával.

gogloid:

No nevím, asi bych se tohoto řešení bál - člověk nikdy neví, co má uživatel v cookies :-) Pokud bych to osobně implementoval, tak bych nejspíš šifrovat obsah cookies klíčem uloženým s údaji uživatele, takže by šlo správně změnit hodnotu cookie jen velmi těžko. Ale to je možná už příliš velká paranoia :-)

P.S.: Děkuji za výbornou přednášku na WebExpu

ikona v6ak:

No pak ztrácí cookies smysl, ne?

ikona v6ak:

Pak je vhodné na tento fakt upozornit přímo v článku.

Megaloman:

To je nesmysl. Každý, kdo píše nějaký skript, který je (bude) veřejně přístupný, musí brát ohled na bezpečnost a u drtivé většiny to jsou následující:

PHP INJECTION, SQL INJECTION, XSS (CSS), XSRF (CSRF), atd.

Není možné na začátku každého kousku kódu uvádět seznam hrozeb, ty si musí pohlídat každý sám, navíc u takovéhoto kousku kódu, který není ready-to-use, ale pouze inspirativní. (I když neustálé opakování do zblbnutí by svůj smysl mělo.)

Navíc CSRF se týká každé stránky s formulářem bez CAPTCHA.

ikona Jakub Vrána OpenID:

Máš pravdu, doplnil jsem to. Ono to může být řešené i automaticky, popíšu to v samostatném článku.

Marek Soldát:

Je tu ještě jedno, podle mě systémovější řešení. Což ukládat do cookies pouze ID uživatele a zbylá data na základě toho ID uchovávat v databázi (a pokud se dotyčný zaregistruje, tak se mu obsah všeho zároveň dostane do profilu) ... ? Nějaké šifrování, nebo ochrana by se hodilo také, ale to už je individualita, u které bude lepší, když ji vyřeší každý sám...

ikona Michal Gebauer:

Není to to samé jako používat session?

ikona Jakub Hejda:

Není to to samé. Cookie zůstane uložené v počítači i po zavření a opetovném otevření prohlížeče. Session se ztratí (při standartním nastavení).

ikona david@grudl.com:

Zůstane obojí nebo se obojí ztratí, při stejném nastavení.

Leoš Ondra:

Jake ID?

sirio:

Potřeboval bych poradit - jakým způsobem můžu z košíku vyndat nějaké zboží? Zničením cookie to asi nepujde že? ... Děkuji

kosk:

cookie může obsahovat několik možných parametrů, některé jsou povinné, jiné fakultativní, zajímá más zde hlavně první, která určuje název cookie a má nějakou hodnotu. Třeba zde nazev kosik a může mít třeba hodnotu 1:2,56:3,57:1 ....   což  může znamenat: zboží 1 v počtu 2, zboží 56 v poctu 3, zbozi 57 v počtu 1 .....

k hodnote lze pristupovat pomocí javascriptu, nebo PHP, viz výše a ta hodnota je textový řetězec. Nic Vám nebrání, krom přidání další hodnoty, což máte výše, kteroukoliv jinou část toho řetězce upravit podle potřeby. (třeba vymazat nějakou část označující zboží, nebo upravit počet ...  když výše uvedený řetězec "1:2,56:3,57:1 ..." upravíte na "1:2,57:8..." a uložíte zpět do toho cookies, tak jste vymazal (vyndal z košíku) zboží číslo 56, a u zboží číslo 57 jste zvýšil počet z 1 na 8.

http://sablony.hyps.cz/php-skripty/nastaveni-cookies.php
http://www.jakpsatweb.cz/enc/cookies.html

Petr:

Asi je opravdu lepší použít Cookie - ad. bod 1) díky  session.gc_ se nám zamete session, ad. bod 2. větší zátěž serveru se sessions.

Právě budu zanedlouho vlastní košík programovat. Přemýšlím nad řešením spojit cookie + databázi. Chtěl bych se zeptat, jestli je dobré řešení udělat košík takto:
1. Do Cookie (třeba s platností 3 měsíce) bych ukládal vlastní vygenerované ID.
2. Hodnoty košíku by se ukládaly do MySQL databáze, spojení uživatele s jeho košíkem by proběhlo právě pomocí ID.
3. Databáze by se nějak pravidelně promazávala, všechny starší záznamy než 3 měsíce. Samozřejmě by se odebral košík uživatele po odeslání objednávky (resp. by se většina obsahu přesunula z tabulky "košíky" do tabulky "objednávky")

Michal:

Jak pomocí js ověřit, zda už cookie hodnotu obsahuje?

ikona Jakub Vrána OpenID:

new RegExp('(^|; *)kosik=([^;]*,)?' + id + '(,|;|$)', '').test(document.cookie);

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2016 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.