Informování uživatele o výsledku operace
Školení, která pořádám
Pokud sledujete mé články delší dobu, tak víte, že stejně jako třeba dgx nemám rád používání session proměnných pro ukládání stavu aplikace (nastavení jazyka, vybraná položka, …). Tyto informace by se zásadně měly přenášet v URL. Proč vlastně?
Když jsem se před časem ucházel v jedné firmě o pozici programátora, v posledním kroku jsem se dozvěděl, že jsem se přihlásil na pozici viceprezidenta. Jak se to stalo? Do panelů prohlížeče jsem si otevřel všechny pozice a potom je zase zavíral. Aplikace používala pro uložení informace o vybrané pozici session proměnnou a protože jsem viceprezidenta otevřel jako posledního, tak si aplikace myslela, že se hlásím na jeho pozici, i když jsem formulář vyplňoval na jasně nadepsané stránce s pozicí programátora. Kdyby bylo ID pozice součástí URL, tak k tomu nikdy nedojde.
Z tohoto důvodu třeba informaci o tom, jak dopadla nějaká operace, posílám také v URL:
<?php
if (mysql_query("UPDATE ...")) {
header("Location: vysledek.php?update=1");
}
// vysledek.php
if ($_GET["update"]) {
echo "<p>Aktualizace proběhla v pořádku.</p>\n";
}
?>
Jaké jsou nevýhody tohoto řešení?
- Předně musíme v URL předávat nějaké parametry, což také zamořuje prostor URL (třeba v historii prohlížeče je potom zbytečně ta samá stránka víckrát).
- Dále je popis výsledku operace na úplně jiném místě, než operace samotná (obvykle v jiném souboru) – to by samozřejmě šlo vyřešit předáním hlášky přímo v URL, to je ale ošklivé a náchylné ke XSS (nejde jen o to, že hláška nemůže obsahovat HTML značky, ale také o to, že kdokoliv může na naše stránky tímto způsob dostat jakýkoliv, třeba hanlivý text tvářící se jako oficiální hláška).
Co kdybychom pro předání této informace použili session proměnné?
<?php
if (mysql_query("UPDATE ...")) {
$_SESSION["message"] = "Aktualizace proběhla v pořádku.";
header("Location: vysledek.php");
}
// vysledek.php
if (isset($_SESSION["message"])) {
echo "<p>$_SESSION[message]</p>\n";
unset($_SESSION["message"]);
}
?>
Obě popsané nevýhody zmizely, objevily se ale jiné:
- Když si dá uživatel obnovit stránku s výsledkem, hláška zmizí. To je ale možná spíš výhoda – tou dobou je hláška už nejspíš neaktuální.
- Teoreticky může dojít k tomu, že hlášku „slízne“ stránka v jiném panelu. Riziko je ale naštěstí malé, protože mezi nastavením hlášky a jejím zobrazením uplyne jen krátká doba jednoho přesměrování.
Co si o popsaných řešeních myslíte? Já si nejsem zcela jist, ale asi se pomalu začínám klonit spíše k druhému.
Diskuse
Honza:
Určitě druhá možnost. Že hláška zmizí je dobře, nechceme, aby si uživatel myslel, že data při obnovení stránky odeslal znovu. Pravděpodobnost, že bude uživatel zrovna načítat stránku s výsledkem v jiném panelu a trefí se, je minimální, už proto že na stránku s výsledkem by měla (mohla) vést jediná cesta přes to přesměrování. A pro tento případ se dá skript doplnit o zjištění vstupní stránky, tedy že se hláška zobrazí jen po přesměrování např. z formular.php
Uvedený příběh je zajímavý a napadá mě otázka: K čemu má firma viceprezidenta, když zřejmě nemá ani jednoho programátora? Programátor by údaj o pozici nikdy neuložil do session nebo cookie, ale do skrytého pole formuláře :-)
Kontrola bohužel nepomůže, protože ve dvou oknech může být otevřen ten samý skript, klidně i s tím samým záznamem.
Najímání zaměstnanců bylo outsourcované...
Ondra:
Hmm tak tento článek je pro mne jako na zavolanou. Právě si dělám svoje "cms". Nebo možná spíše pokus o něj :) A tenhle problém jsem nedávno řešil a pořád si lámal hlavu tím, jestli jsem udělal dobře, když informace o dokončené operaci posílám přes SESSION. Jak vidím, tak nejsem sám kdo si není uplně jitý tímto řešením. Ale asi opravdu není jiný lepší způsob než posílát tato data přes SESSION. Navíc čím složitější aplikace tím dříve u využití SESSION pro tyto účely člověk skončí.
Milan:
Vytvářel jsem rozsáhlejší webový dotazník o několika stránkách a nakaždé s mnoha vstupními poli. Představa posílání dat v URL si vynucuje můj úsměv. Řešil jsem přez SESSION.
ivan_d:
Tady jde o trochu jiný příklad - data v session je třeba uchovávat přes více requestů - nelze je po použití smáznout. Dotazníků se asi nebude vyplňovat více naráz. Jsou ale aplikace (jako zmíněný příklad v článku), kde je paralelní práce docela možná. Pak může uživatel nadělat v session dost nepořádek. Možná by mohlo pomoci něco ve stylu řešení, které jsem navrhl níž. Ale jak jsem zmínil, je to jen teoretický náčrt.
V tomto případě bych to taky necpal do URL. Ale spíše než session bych asi použil skryté pole.
ivan_d:
Jistě, na tento případ by to bylo asi nejlepší řešení
ivan_d:
Taky bych se klonil ke druhé možnosti. Url vidím jako 'pohled' na aplikaci. A to pohled trvalý - byť může být jeho výsledkem 404. Hláška je jednorázovka.
Podobný problém jsem řešil asi před měsícem a po sáhodlouhých úvahách, jak to udělat nejlíp, jsem dospěl k těm sessions :) .
ivan_d:
To slíznutí jiným tabem by možná šlo řešit. Co třeba označit si každý případ (každou nabízenou pozici) heší (pozice_ve_firme=viceprezident), tu přenášet v url a zářídit si podle toho session?
<?php
if (mysql_query("UPDATE ...")) {
$_SESSION[$_GET['pozice_ve_firme']]["message"] = "Aktualizace proběhla v pořádku.";
header("Location: vysledek.php?pozice_ve_firme=".$_GET['pozice_ve_firme']);
}
// vysledek.php
if (isset($_SESSION[$_GET['pozice_ve_firme']]["message"])) {
echo "<p>$_SESSION[$_GET['pozice_ve_firme']][message]</p>\n";
unset($_SESSION[$_GET['pozice_ve_firme']]["message"]);
}
?>
Zatím je to jen teoreticky, prakticky jsem nerealizoval, takže tady může být něco silně nedomyšleného. Má někdo lepší řešení?
Jak jsem psal - z obecného pohledu to nepomůže. Ve dvou oknech můžu pracovat s týmž záznamem.
ivan_d:
Asi jsem dobře nepochopil to slíznutí. Moje představa byla: tabulka záznamů, u každého tlačítko s akcí, já na ně v rychlém sledu klikám prostředním tlačítkem (otevírají se do nových tabů) a tady jsem myslel, že je problém s vyžíráním postů.
Pokud bychom se neomezovali PHP, tak by se taky dalo nějak sledovat spojení. Teď si nejsem jistý, ale mám dojem, že u HTTP/1.1 se při přesměrování na jiný path stejného serveru spojení zavírat nemusí. Pak by to ovšem bylo silně závislé na HTTP/1.1 a na tom, že to zavření nevynutí prohlížeč.
Asi bude nejlepší riskovat, že se hláška zobrazí v jiném tabu. Tak jako to dělají všichni ostatní.
Ještě by šlo spojit obě dvě možnosti dohromady. Při updatu vygenerovat hash a do session pro tento hash napsat příslušnou zprávu. Pak přesměrovat na na stránku a té předat příslušný hash jako parametr v GET a ona zobrazí výsledek akce.
ivan_d:
Je to trochu podobný přístup jako jsem o kousek výš navrhoval, nicméně - ten hash má být jednorázově pro onu zprávu? Tím odstraňujete problém s XSS u nevýhody číslo 2 z řešení číslo 1 , ale v url zůstává balast. A co pak udělá obnovení?
Jen takova rypava otazka: Nemel by jsi mit po tom header exit();?
Osobne bych spise volil reseni podle daneho druhu. Pokud to bude v administracni casti, poslal bych to klasicky pres GET.
Osobne bych i ve verejne casti volil to same.
"Dále je popis výsledku operace na úplně jiném místě, než operace samotná (obvykle v jiném souboru) – to by samozřejmě šlo vyřešit předáním hlášky přímo v URL, to je ale ošklivé a náchylné ke XSS (nejde jen o to, že hláška nemůže obsahovat HTML značky, ale také o to, že kdokoliv může na naše stránky tímto způsob dostat jakýkoliv, třeba hanlivý text tvářící se jako oficiální hláška)."
Ale tohle je prece vyhoda! Ve chvili, kdy umim rozdelit aplikacni logiku od prezentacni, mam mnohem snazsi zpusob, jak aplikaci globalne menit. Kdybych mel hlasku menit na desitkach mistech, tak se z toho z zblaznim. Podle meho by bylo nejlepsi poslat hash a ten pote dekodovat.
<?php
header("Location: vysledek.php?mess=". URLMessages::getHash(URLMessages::UPDATE));
if (isset($_GET["mess"])) {
echo URLMessages::getMessage($_GET["mess"]);
}
?>
Pote pri zachycovani si dokazi predstavit, ze budu mit opet moznost podle hashe dostat tu zpravnou hlasku.
Vyhodou je jiz zminena oddelenost aplikace. Dam priklad: Co kdyz se rozhodnu, po urcitem case, ze aplikace bude multijazycna? Budu muset vstupovat do kazde casti zvlast a hledat vsechny hlasky?
ivan_d:
'Nemel by jsi mit po tom header exit();?' - já to tedy rozhodně nechápal jako hotový skript, spíš je přístup..
'Co kdyz se rozhodnu, po urcitem case, ze aplikace bude multijazycna'
<?php
... _("Aktualizace proběhla v pořádku.") ...
?>
Pokud není opravdu ideální řešení, tak je podle mě nejlepší znát více přístupů, jejich nevýhody a pro konkrétní úkol se rozhodnout pro nejvhodnější. Přílišná generalizace bývá na škodu.
Ps: to není osobní, ale nevím proč na některé lidi reaguji častěji :)
Ondra:
"Co kdyz se rozhodnu, po urcitem case, ze aplikace bude multijazycna? Budu muset vstupovat do kazde casti zvlast a hledat vsechny hlasky?"
Samozřejmě tento problém lze snadno řešit tak jak to načrtl kolega nademnou. V případě správného použití cachování je to dobrá metoda. Navíc u rozsáhlejších projektů lze hlášky ukládat do DB.
SendiMyrkr:
A nebo třeba do konstant... já osobně rešim multijazykový verze tak, že mam několiko souborů se shodnými konstantami a překlady jejich obsahu a podle toho jakou verzi aktuálně používám nahraji potředný soubor...
Příklady zjednodušuji tak, aby v nich zůstala jen hlavní myšlenka. Za přesměrováním by mohl být exit stejně jako by zbytek skriptu mohl být v else větvi.
MK:
Myslím si, že každé řešení má něco do sebe. První řešení bych použil pokud bych chtěl nějak akci zaznamenat třeba jako goal v GA (např. zaregistrování newsletteru apod.), také má určitě výhodu, když uživatel má vypnuté cookies a mám zakázáno posílání PHPSESSID v url.
Dle vlastnich zkusenosti nespravne pouziti session pacha vice skody nez uzitku.
My od Microsoftu na to mame ViewState :)
My programujici ve swingu to neresime vubec :)
ivan_d:
.net neznám, ale setkal jsem se se stránkama psanými pravděpodobně v .netu, které se bránily otevřít odkazy v novém tabu (javascript) a celé to na mě působilo jako směřované pro 1 okno. Jako neznalý bych se chtěl zeptat - jak .net řeší problémy s více taby. Vím, že to není specifické pouze pro .net - mail na seznamu je podobně (pro mě nepříjemně) vyřešen - nemohu si rozklikat zprávy do víc tabů... Takže jak je na tom .net?
To je otazka na kapitolu v knize, ale zkracene bych rek, ze viewstate je v podstate session uchovavana v hiddenpolich stranky.
Dulezite je to, ze .net pro to ma podporu a programator z toho nemus nic resit.
ivan_d:
Princip znám, jde mi o to, jak se v tom pracuje s taby a jak to řeší nadnesené problémy - včetně toho, co popisuje dgx o kousek níž - obnovení stránky s informací o úspěšnosti akce.
Vývoj programátora pod .NET:
1) nadšení z ViewState
2) šíření nadšení z ViewState mezi ostatní (jó, to my máme ViewState, heč)
3) snaha redukovat velikost ViewState
4) hledání způsobů jak tvořit weby zcela bez ViewState
5) šíření nadšení, že to umíme bez ViewState
6) ...
:-)
ivan_d:
Ale řeší ten viewstate zmíněné problémy?
Je videt, ze si tim neprosel :)
Je to spis
1) wtf, ctj z kod v html?
2) on existuje viewstate?
3) dejte ho pryc, ja ho nechci.
4) ovladnuti viewstate / controlstate
5) ucelne pouzivani viewstate
Aneb tisice flamu po celym netu.
Michal:
Jelikož používám speciální třídu pro práci se sessions, tak mám k dispozici funkci set_flashdata. Tato funkce do session uloží libovolná data, která jsou platná pouze pro další stránku, pak se automaticky vymažou. Pokud chci, tak jejich platnost mohu prodloužit. Pokud se někdo shání po podobné třídě, tak se může kouknout na http://codeigniter.com/wiki/Native_session/ je to sice pro codeigniter framework, ale dá se použít i samostatně.
dgx:
Zkuste se zamyslet nad tím, kolikrát jste špatné načtení stránky, ať už to byla chybějící část textu nebo třeba jen nenačtené obrázky, řešili stisknem F5 - obnovit.
Pokud jste studenti, kteří vysedávají na vysokoškolských internetech, uvědomte si, že tak dobrý internet nikdo jiný nemá. A realita bezdrátů je onačejší - běžně chytnete ping 800ms, protože "je něco ve vzduchu".
Sám mačkám F5 s obavami, jestli na podobný stav programátor vůbec myslel, a třeba tím neodešlu formulář podruhé, nebo, což je tento případ, jestli se tak nepřipravím o důležitou informaci o výsledku operace.
ivan_d:
Je možné postavit aplikaci tak, že zpráva o úspěšné akci je jen redundance uklidňující uživatele: pokud se nezdaří dostanu se na jinou stránku (problémovou).
Rozumím problému, ale moc se mi (pocitově) nelíbí ta hláška v url - tam si může kdokoliv zapsat cokoliv. Ale asi to ničemu nevadí...
dgx:
Textový obsah hlášky samozřejmě nemá v URL co dělat. Hlášek je limitní počet, takže v URL bude pouze její ID. Nakonec skutečně plnohodnotné hlášení o chybě se nemůže ani do délky URL stlačit. Je vhodné uživatele nejen srozumitelně informovat o chybě, ale také navrhnout způsob řešení (byť by to bylo jen "zkuste později, administrátor byl o problému informován").
Leoš Ondra:
Ono krome tlacitka Obnovit existuje i tlacitko Zpet, a pokud je hlaseni o odeslani objednavky vazane na URL, pak se uzivateli pri prochazeni historie objevi znovu... Coz taky neni idealni stav, Leo
dgx:
Proč ne? K tomu oznámení historicky došlo, tedy má v historii své místo. Proč měnit historii?
Když už bych usoudil, že má skutečně smysl historii změnit, tak bych místo unset($_SESSION["message"]) použil něco jako:
<?php
if (mysql_query("UPDATE ...")) {
$_SESSION["message"] = "Aktualizace proběhla v pořádku.";
$_SESSION["message-expire"] = time() + ANY_CONST;
header("Location: vysledek.php");
}
// vysledek.php
if (isset($_SESSION["message-expire"]) && $_SESSION["message-expire"] < time()) {
unset($_SESSION["message"], $_SESSION["message-expire"]);
}
if (isset($_SESSION["message"])) {
echo "<p>$_SESSION[message]</p>\n";
}
?>
Leoš Ondra:
Ono je to vlastne cely blbost, protoze aby to fungovalo, tak by se muselo zakazat kesovani, ale to nema mit vliv na to, co uzivatel vidi pri prochazeni historie (tlac. Zpet/Vpred). takze v Opere, ktera to respektuje, tam bude stara verze viset porad. S URL/session (jen s cookie) to nesouvisi.
Moje chyba, L.
Jakub Vrána :
Je to tak - přesměrování dělám jen v případě úspěchu. V případě neúspěchu zůstanu na stejné stránce (i s napostovanými daty) a hlášku zobrazím rovnou tam.
dgx:
Má, ale tuším všechny používané prohlížeče tolerují i relativní cestu. Sám teda HTTP specifikaci nikdy neporušuju, a to z úcty, považuju ji za ojediněle výbornou.
Jakub Vrána :
Je to tak, už jsme se o tom tady bavili. Tohle považuji za jedinou slabinu protokolu :-).
LesTR:
napr. links ne : ) Oni ty RFCcka nejsou jen tak pro nic.
Jan Tichý:
"Má, ale tuším všechny používané prohlížeče tolerují i relativní cestu."
Což ovšem neznamená, že jakýkoliv současný či budoucí prohlížeč to může zpracovávat jakkoliv jinak. A proto je lepší se držet specifikace protokolu a do hlavičky psát výhradně úplné URL.
Bohdan:
Často řeším otázku, kdy je v takových případech nejlepší session spustit. Na nějakém webu jsou na různých místech formuláře, které přes session předávají informace o výsledku. Jsou asi dvě možnosti - buď session_start volat vždy na začátku každé stránky, nebo jen když je to potřeba, tj. při zpracovávání formuláře. A pak na začátek každé stránky dát něco jako
<?php
if(isset($_COOKIE['PHPSESSID']) || isset($_GET['SID']))
session_start().
?>
První možnost posílá cookie i všem 99.9% návštěvníků, kteří žádný formulář neodeslali. Druhou běžně používám, tak by mě zajímalo co si o tom myslíte :)
optik:
presne tak, ZF ma praci se session moc pekne vyresenou (Zend_Session_Namespace), vcetne toho flassmessengeru.
Ondrej Ivanic:
A vobec, skusali ste to pouzit presne v tejto situacii?
Ked som studoval zdrojaky, tak som nadobudol pocit, ze to ma presne tie iste problemy, lebo stale sa pouziva rovnaky session namespace.
Budem rad ak ma niekto presvedci o opaku
optik:
ja osobne pouzivam primo zend_session_namespace s unikatnim namespace per controller a expiraci na 1 hop, v podstate nevadi pouzivat jen default namespace a flassmessengera pokud vytvaris a vypisujes message vzdy v ramci jednoho controlleru. Jak jsem se koukal do manualu, muzes pro kazdy message nastavit, v jakem namespace ho chces v messangeru ukladat, takze na nej sve reseni muzu v klidu predelat.
Mordae:
A co v URL posílat id prováděné operace a do session ukládat data jako $_SESSION[$tamtoID]..? Potom každý form bude mít své id a nic se nesplete...
Axiss:
Informace s hláškami o chybě nebo úspěchu ukládám do session, zvlášť do pole s chybami nebo úspěchem (často se vypisuje více než jen jedna informace). V session mám jen ID hlášek, u nenáročných projektů, kdy lze použít smarty, potom text s hláškou přímo :)
RAY:
Podobně jako u většiny funkcí, je i sessions potřeba používat s rozmyslem. Ve vhodných případech totiž svojí funkci splní dokonale, v jiných naopak způsobí nemálo problémů.
Kupříkladu ono zmíněné nastavení jazyka v úvodu článku. Určitě bude vhodné ve většině případů jazyk přenášet v URL (za pomocí mod_rewite to může vypadat i velmi elegantně).
Představme si např. eshop, který má popis zboží uveden hned v několika jazycích - a takové případy nejsou především na zahraničním webu ojedinělé. Vložením jazyka do URL dosáhneme:
1) Indexace všech jazykových variant vyhledávači
2) Pošle-li např. nějaký Francouz odkaz na výrobek jinému Francouzovi zobrazí se i druhému popis ve francouzštině
Naopak nabízí-li eshop zobrazení cen v několika měnách (CZK/SKK), určitě nebude dobré toto nastavení přenášet v URL už proto, že bychom získali penalizaci za duplicitní obsah od vyhledávačů (a nebo bychom si museli pohrát s robots.txt).
V tomto případě tedy dokonale poslouží ony sessions.
JiFF:
Já osobně mám nejraději předání parametru v URL a následné napojení na array chyb... Co na tom, že si může uživatel "vylistovat" jakoukoli chybu? ^^ ať má radost =)
Navíc - daný chybový stav (nebo jiný stav) nastává globálně všem lidem, pokud se k němu dostanu, proč to tedy řešit pomocí session...
mhanny:
A je to tu zas :)
Tenhle problém už jsem několikrát řešil a řeším ho znovu.
Přesně ten příklad se session jsem chtěl použít, ale prostě mi nefunguje a nemůžu přijít na to proč.
Já když tu session odeberu hned v té stránce kde jsem chtěl tu session použít, tak jako bych ji vůbec neměl.
Tady je můj kód zkráceně:
if(IsSet($_SESSION['sent'])){
echo "V pořádku.";
UnSet($_SESSION['sent']);
}
Když tohle použiju tak se mi dané echo vůbec nevypíše, ale kdybych tu sesion neodebral tak ano. Ale pak se mi vlastně zobrazí až do doby, dokud ji neodeberu a to chci hned v té jedné stránce.
Prostě už mi to asi nemyslí, ale nemůžu přijít na to proč mi to nejde :(
Budu vděčný za každý názor.
Karel Dytrych:
Mate nastartovane spravne session? Podivejte se kde presne do $_SESSION['sent'] prirazujete...
mhanny:
zároveň teď musím přiznat, že kód který jsem zde napsal v prvním dotazu je nejspíš i funkční, ale psal jsem ho z hlavy a nefunkčním se stával až když jsem danou session definoval před přesměrováním
přiznávám, že jsem se nedostatečně vyjadřoval, protože jsem si nebyl jistý, kde přesně problém je
David:
Dobrý den, jak to tedy nakonec řešíte teď - po 3 letech ?
Jakub Vrána :
Používám druhé řešení. Ale když vytvářím aplikaci v Nette Frameworku, tak je to kombinace obojího. Nette si totiž hlášku uloží do session a v URL přenese její identifikátor. Hláška se pak nemůže zobrazit na jiné stránce. Nevýhoda je ale ta, že se tím vytváří nekanonická URL (i když jde o stejnou stránku, tak se v prohlížečích zobrazuje jako nenavštívená).
David:
Děkuji vám za odpověd.
Miloš Brecher:
Formulář by měl v sobě obsahovat celý balík dat pro ukládání do databáze - včetně pozice na kterou se uživatel chce přihlásit - buďto ve viditelném prvku - např. předvolený select na dané pozici, nebo input hidden. Do session ani do url bych to nedával. Zprávu o výsledku zpracování odeslaného formuláře dát do session proměnné tak, aby se zpráva zobrazila i po přesměrování po odeslání POST.
Diskuse je zrušena z důvodu spamu.