V minulém díle tohoto seriálu jsme se naučili, jak vytvořit databázi a jak do ní přidat tabulky a data z nich zobrazit na stránce. Dnes se naučíme záznamy do tabulek přidávat a ukážeme si to na velice jednoduchém diskusním fóru, které napíšeme. Webové aplikace mohou být nebezpečné, pokud jsou špatně napsané. Ukážeme si i dva možné útoky na tuto aplikaci a také způsob, jak jim zabránit. ASP.NET nám v tom vydatně pomůže, takže nebudeme mít téměř žádnou práci.
Začínáme
Vytvořte si tedy novou webovou aplikaci (ASP.NET Web Site) v jazyce, který je vám bližší. Přidejte si do aplikace jednu databázi, pokud nevíte jak, podívejte se do minulého dílu. Nyní do databáze vytvoříme jednoduchou tabulku, kde budeme "skladovat" všechny příspěvky uživatelů do diskuse. Tabulka to bude velmi jednoduchá, stačí uchovávat jenom pár údajů. Přidejte tedy do databáze sloupce podle obrázku.
Nezapoměňte kliknout na první řádek pravým tlačítkem a vybrat volbu , aby se první sloupec tabulky nastavil jako primární klíč. Navíc mu dole v okně vlastností nastavte Identity Specification na hodnotu Yes takto:
Tím zajistíme, že pokud přidáme do tabulky záznam, nastaví se do sloupce PostId automaticky unikátní hodnota. První záznam tedy dostane jedničku, druhý dvojku, třetí trojku atd. Pokud tyto záznamy smažeme, další záznam dostane číslo čtyři. Očíslování tedy nebude souvislé a nepoznáme z něj počet záznamů, ale to ani není cílem. Cílem je, aby každý záznam měl určitě číslo jiné. A to splněno bude.
Nyní stiskněte klávesy Ctrl+S, aby se tabulka vytvořila v databázi. Visual Studio se nás ještě zeptá na název tabulky, zadejte Posts a potvrďte.
Malá odbočka aneb pojmenovávání sloupců
Záměrně pojmenovávám sloupce v tabulce anglicky. Každý programátor by měl anglicky umět a pokud to neumí, měl by se to rozhodně co nejdříve naučit. Angličtina je ve světě programování naprosto nezbytná. Protože tohle je tutoriál pro začátečníky, vysvětlím, co názvy sloupců říkají, abyste se neztratili.
Název tabulky Posts znamená česky něco jako příspěvky. Jeden příspěvek je post, z čehož dostáváme název prvního sloupce - PostId - ID příspěvku. Dále máme sloupce Title, což znamená název (v našem případě název příspěvku), pak následuje Message (česky zpráva), dále pak Author (autor, v našem případě jeho jméno nebo přezdívka) a PostDate (datum příspěvku). Každý má na pojmenování databází jiné zásady, je to věc zvyku, ale je dobré mít všude jednotný systém.
V tuto chvíli byste měli tedy sami umět vysvětlit, jaká data budou uchovávána v každém sloupci a z jejich datových typů byste měli umět odvodit, jestli to bude datum, text (a jak maximálně dlouhý) atd. Zkuste si to, pokud váháte, mrkněte do článku Úvod do jazyka SQL.
Vypsání příspěvků a nastavení ConnectionStringu
Nyní budeme chtít vypsat všechny příspěvky z tabulky do stránky. Jelikož příspěvky v diskusním fóru většinou nejsou data vhodná k zobrazení v tabulce, nepoužijeme komponentu GridView, ale komponentu DataList. Ta umí zobrazit více záznamů a navíc si můžeme přesně zvolit, jak budou záznamy vypadat. Přidejte tedy do stránky komponenty SqlDataSource a DataList. Z miulého dílu již víte, jak nastavit SqlDataSource a jak vytvořit ConnectionString, tedy jak nastavit připojení do databáze. Nastavte tedy do SqlDataSource naši databázi a při nastavování dotazu vyberte všechny sloupce z tabulky Posts. Většinu věcí zvládnete sami, na dvou obrázcích ukážu, kde začít a kam se máte dostat.
Ještě než kliknete na tlačítko Next, nastavíme třídění záznamů podle data přidání sestupně. Nahoře tedy bude příspěvek nejnovější a dole ten nejstarší. Klikněte na tlačítko Order By a dialogové okno nastavte podle obrázku:
Nyní můžete pokračovat dále a odklikat OK a Next až do konce. Budeme mít nastavený SqlDataSource. Teď si trochu pohrajeme s komponentou DataList.
Základní nastavení komponenty DataList
Klikněte na komponentu DataList a v podokně úloh nastavte jako zdroj dat náš nastavený SqlDataSource1. Komponenta si ihned vytáhne z datového zdroje popis struktury a zobrazí nějaká vzorová data, jak asi budou vypadat na stránce.
Vzhled výpisu příspěvků zatím necháme tak, jak je, a vrhneme se na přidávání diskusních příspěvků a přidávání záznamů do databáze. Ještě před tím si ale povíme o dvou možných způsobech útoku na webové aplikace, které nás mohou potkat právě pokud necháme uživatele zadávat údaje a následně je někde vypisujeme.
Trocha bezpečnosti
Jak všichni jistě víme, existují určití zlí lidé, kteří se snaží upozornit na sebe tím, že nám všem škodí. Například tím, že kradou, zabíjí, jezdí na silnici příliš pomalu nebo zase příliš rychle, anebo třeba útočí na webové aplikace, nejčastěji za účelem získání dat, ke kterým by neměli mít přístup. Pokud uchováváte v databázi osobní údaje uživatelů, nesmíte připustit, aby se k nim někdo nepovolaný dostal, jinak vám hrozí žaloba a trest. Zajištění bezpečnosti webové aplikace napojené na databázi je opravdu nutné.
Útok SQL injection
Představme si, že na webové stránce máme textová pole, do kterých má uživatel napsat své uživatelské jméno a heslo. Jak tedy bude vypadat SQL příkaz, který ověří, zda je uživatel v databázi a jestli má správné heslo? Většina začátečníků to udělá nějak takto (předpokládejme proměnné jmeno a heslo s hodnotami z textových polí):
sql = "SELECT * FROM uzivatele WHERE jmeno='" + jmeno + "' AND heslo='" + heslo + "'"
Pokud uživatel tedy napíše jméno tom a heslo jerry, výsledný SQL dotaz bude vypadat takto: SELECT * FROM uzivatele WHERE jmeno='tom' AND heslo='jerry'. To vypadá dobře. Má to ovšem jeden zásadní problém - když přijde na web hacker, zkusí jako jméno napsat třeba toto: tom' --. A co se nestane? Naše SQL nyní bude vypadat takto: SELECT * FROM uzivatele WHERE jmeno='tom' --' AND heslo='cokoliv to je úplně fuk'. Jenže --, tedy 2 pomlčky, v SQL znamenají komentář, tedy to co je za nimi, se již neprovede. A bez komentáře to je jen SELECT * FROM uzivatele WHERE jmeno='tom'. Díky tomu se hacker dostane dovnitř, aniž by znal heslo, to se totiž nikde netestuje. To je ta lepší varianta, pokud napíše dostatečně sofistikovaný dotaz, zkopíruje si databázi k sobě a vám ji na serveru třeba smaže.
Jak z toho ven? Možná si řeknete, že stačí odebrat apostrofy z textu, ale existují i metody, které obejdou i toto (ne vždy a všude fungují, ale občas ano). Problém je v tom, že když jeden TextBox ošetřit zapomenete, máte bezpečnostní díru. ASP.NET, ale i jiné technologie, nabízí tzv. SQL parametry. V zásadě to znamená to, že do SQL dotazu místo dosazování konkrétních hodnot zapíšete jen název proměnné a hodnoty proměnných pošlete zvlášť, až po samotném dotazu. ASP.NET nás tlačí k používání parametrů, pokud si na ně zvyknete, tento typ útoku se vám nemůže stát, ať útočník napíše cokoliv. Hlavně nikdy neskládejte SQL příkaz, to téměř vždycky zavání bezpečnostní dírou. Pokud důsledně vždy použijete parametry, tento útok vám nehrozí, pokud neuděláte jinou botu.
SQL dotaz s parametry vypadá nějak takto: SELECT * FROM uzivatele WHERE jmeno = @jmeno AND heslo = @heslo. Hodnoty parametrů se nastavují zvlášť a nikdy se do dotazu nedosazují. Tím pádem jsme z nebezpečí venku, tento způsob zatím nebyl nijak prolomen.
Ještě jedna poznámka k uvedenému příkladu - ukládat hesla do databáze jen tak je naprostý nesmysl - pokud někdo už databázi stáhne, dozví se i hesla. A vzhledem k tomu, že 99% uživatelů používá všude stejné jméno a heslo, je to dost velké riziko. Proto se používá hash (čti "heš"). Hash je funkce, které nasypete nějaká data (v našem případě heslo) a ona je zakóduje tak, že se z výsledku již původní heslo dostat nedá. Důležité je, že dvě stejné hodnoty dají vždycky stejný hash a je velmi těžké (neřku-li nemožné ani s nejlepšími superpočítači) vymyslet dva vstupy tak, aby hash vyšel stejně. Do databáze tedy můžeme uložit jen hash našeho hesla. Při testování spočítáme hash toho, co napsal uživatel, a obě hodnoty porovnáme. Když jsou stejné, heslo je správné. V praxi se ještě pro každého uživatele vygenerujea uloží náhodný řetězec (tzv. sůl), která se před hashováním přidá za heslo, a to celé se teprve zahashuje. Stejný postup se provede i při ověřování - heslo se osolí (přidá se za něj příslušná sůl) a pak se teprve spočítá hash. Má to tu výhodu, že pokud více uživatelů má stejné heslo, nejde to poznat, protože každý má jinou sůl, a tím pádem i úplně jiný výsledný hash. Ale to je věc jiná, o uživatelích se budeme bavit až v příštím díle.
Útok Script injection
Tento typ útoku není tak nebezpečný, ale dokáže znepříjemnit život mnoha webovým vývojářům. Pokud máme hloupé diskusní fórum, stačí napsat jako text diskusního příspěvku třeba toto:
<script type="text/javascript">location.href='http://www.vbnet.cz';</script>
Všichni bychom měli vědět, co to udělá. Pokud příspěvek vypíšete do stránky, připravte se na to, že při jejím načtení bude hned prohlížeč přesměrován na náš server VbNet.cz. Javascript se tedy spustí, což určitě nechceme. Tady je nejlepším řešením použít metodu HtmlEncode, tedy převod speciálních znaků na HTML entity. Znak < se tedy převede na < a znak > na >. Prohlížeč nyní bude vědět, že to nejsou HTML značky, a vypíše je jako text. To by mělo k odražení tohoto útoku stačit.
Hodí se poznamenat, že ASP.NET automaticky hlídá, zda-li uživatel neposílá kusy HTML. Pokud to udělá, nastane výjimka a zobrazí se chybová stránka. To ale většinou nechceme, takže to musíme ručně vypnout. Pak si ale musíme tyto situace ošteřit ručně a zakódovat příslušné texty, jinak nám hrozí reálné nebezpečí.
Jak na přidávání příspěvků
Přidejte do stránky komponentu FormView. Ta umí pracovat s jedním záznamem, což zde právě potřebujeme. FormView má tři režimy zobrazení - Insert, Edit a ReadOnly. První režim přidává záznamy, druhý je upravuje, a třetí pouze zobrazuje. My chceme pouze přidávat, proto nastavte hodnotu vlastnosti DefaultMode na hodnotu Insert. Nastavte této komponentě také datový zdroj SqlDataSource1, úplně stejně, jako jsme to udělali komponentě DataList. Nyní by měl FormView vypadat asi takto:
To se nám rozhodně nelíbí, takže bychom vzhled měli upravit. Navíc datum se bude vyplňovat automaticky. Přepněte se tedy do režimu Source View a podívejme se na kód komponenty FormView, který vypadá takto:
<asp:FormView ID="FormView1" runat="server" DataKeyNames="PostId" DataSourceID="SqlDataSource1"
DefaultMode="Insert">
<EditItemTemplate>
PostId:
<asp:Label ID="PostIdLabel1" runat="server" Text='<%# Eval("PostId") %>'></asp:Label><br />
Title:
<asp:TextBox ID="TitleTextBox" runat="server" Text='<%# Bind("Title") %>'> </asp:TextBox><br />
Message:
<asp:TextBox ID="MessageTextBox" runat="server" Text='<%# Bind("Message") %>'> </asp:TextBox><br />
Author:
<asp:TextBox ID="AuthorTextBox" runat="server" Text='<%# Bind("Author") %>'> </asp:TextBox><br />
PostDate:
<asp:TextBox ID="PostDateTextBox" runat="server" Text='<%# Bind("PostDate") %>'> </asp:TextBox><br />
<asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update"
Text="Update"> </asp:LinkButton>
<asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"
Text="Cancel"> </asp:LinkButton>
</EditItemTemplate>
<InsertItemTemplate>
Title:
<asp:TextBox ID="TitleTextBox" runat="server" Text='<%# Bind("Title") %>'> </asp:TextBox><br />
Message:
<asp:TextBox ID="MessageTextBox" runat="server" Text='<%# Bind("Message") %>'> </asp:TextBox><br />
Author:
<asp:TextBox ID="AuthorTextBox" runat="server" Text='<%# Bind("Author") %>'> </asp:TextBox><br />
PostDate:
<asp:TextBox ID="PostDateTextBox" runat="server" Text='<%# Bind("PostDate") %>'> </asp:TextBox><br />
<asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert"
Text="Insert"> </asp:LinkButton>
<asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"
Text="Cancel"> </asp:LinkButton>
</InsertItemTemplate>
<ItemTemplate>
PostId:
<asp:Label ID="PostIdLabel" runat="server" Text='<%# Eval("PostId") %>'></asp:Label><br />
Title:
<asp:Label ID="TitleLabel" runat="server" Text='<%# Bind("Title") %>'></asp:Label><br />
Message:
<asp:Label ID="MessageLabel" runat="server" Text='<%# Bind("Message") %>'></asp:Label><br />
Author:
<asp:Label ID="AuthorLabel" runat="server" Text='<%# Bind("Author") %>'></asp:Label><br />
PostDate:
<asp:Label ID="PostDateLabel" runat="server" Text='<%# Bind("PostDate") %>'></asp:Label><br />
</ItemTemplate>
</asp:FormView>
Vzhledem k tomu, že budeme záznamy zatím jen přidávat, smažte celé sekce ItemTemplate a EditItemTemplate. Nyní by kód měl vypadat asi takto:
<asp:FormView ID="FormView1" runat="server" DataKeyNames="PostId" DataSourceID="SqlDataSource1"
DefaultMode="Insert">
<InsertItemTemplate>
Title:
<asp:TextBox ID="TitleTextBox" runat="server" Text='<%# Bind("Title") %>'> </asp:TextBox><br />
Message:
<asp:TextBox ID="MessageTextBox" runat="server" Text='<%# Bind("Message") %>'> </asp:TextBox><br />
Author:
<asp:TextBox ID="AuthorTextBox" runat="server" Text='<%# Bind("Author") %>'> </asp:TextBox><br />
PostDate:
<asp:TextBox ID="PostDateTextBox" runat="server" Text='<%# Bind("PostDate") %>'> </asp:TextBox><br />
<asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert"
Text="Insert"> </asp:LinkButton>
<asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"
Text="Cancel"> </asp:LinkButton>
</InsertItemTemplate>
</asp:FormView>
To, co je uvnitř značky InsertItemTemplate je tzv. šablona. Šablony se v ASP.NET používají velmi často k nadefinování vzhledu a struktury záznamů. Zde vidíme, že se vypíše nějaký text Title: a za ním je komponenta TextBox, obdobně se vypíší ostatní pole a nakonec tam máme dvě tlačítka. Jediné, co nám asi bude divné, jsou záhadné hodnoty vlastnosti Text, totiž <%# Bind("Title") %>. Říká se jim tzv. binding expressions a jsou to vlastně kusy kódu, které vypisují data z databáze. Funkce Bind má jako parametr název sloupce, jehož hodnotu má použít. Místo tohoto záhadného kousku kódu se tedy do textového pole vypíše hodnota sloupce z databáze (v tomto konkrétním případě se nevypíše nic, protože záznam vytváříme a v databázi ještě není). Co je však důležitější, při přidání záznamu se hodnota této vlastnosti vezme a uloží se do databáze. Tomuto procesu se často říká Data Binding, což by se dalo česky přeložit jako "provázání s daty". Aby přidávání záznamu vypadalo hezky, změňte kód komponenty FormView takto:
<asp:FormView ID="FormView1" runat="server" DataKeyNames="PostId" DataSourceID="SqlDataSource1"
DefaultMode="Insert">
<InsertItemTemplate>
<h2>Přidat příspěvek do fóra</h2>
<table>
<tr>
<td>Předmět:</td>
<td>
<asp:TextBox ID="TitleTextBox" runat="server" Text='<%# Bind("Title") %>' Width="400px"
MaxLength="50"></asp:TextBox>
</td>
</tr>
<tr>
<td>Autor:</td>
<td>
<asp:TextBox ID="AuthorTextBox" runat="server" Text='<%# Bind("Author") %>' Width="400px"
MaxLength="50"></asp:TextBox>
</td>
</tr>
<tr>
<td>Zpráva:</td>
<td>
<asp:TextBox ID="MessageTextBox" runat="server" Text='<%# Bind("Message") %>' Width="400px"
Height="100px" TextMode="MultiLine" MaxLength="2000"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center">
<asp:Button ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert" Text="Přidat příspěvek" />
<asp:Button ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Storno" />
</td>
</tr>
</table>
</InsertItemTemplate>
</asp:FormView>
Provedl jsem v šabloně několik změn - textová pole jsem umístil do tabulky, aby se úhledně zarovnala, nastavil jsem ručně šířku polí a u pole pro zprávu jsem nastavil TextMode na hodnotu MultiLine, takže se textové pole chová jako textarea. A konečně komponenty LinkButton jsem nahradil komponentami Button. To kvůli tomu, aby stránka fungovala i bez zapnutého javascriptu. Kompomenta LinkButton totiž odesílá formulář pomocí javascriptu, tento typ tlačítka totiž vypadá jako odkaz. Odeslat formulář odkazem je ale nemožné bez použití javascriptu, proto raději použijeme Button, což je obyčejné input type=submit. To funguje samozřejmě i bez klientských skriptů.
Pokud se podíváme, jak nyní komponenta pro přidání příspěvku vypadá, myslím, že můžeme být spokojeni, je to rozhodně lepší. Pokud si ještě pohrajeme s CSS styly, bude to vypadat ještě o několik řádů lépe.
Nastavení SqlDataSource pro podporu přidávání záznamů
Samotná komponenta nám ale ještě fungovat nebude, nezadali jsme do datového zdroje SQL příkaz pro přidávání záznamů a nevytvořili parametry. To musíme napravit. Najděte si tedy v okně kódu komponentu SqlDataSource1, její kód by měl vypadat takto:
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
SelectCommand="SELECT [PostId], [Title], [Message], [Author], [PostDate] FROM [Posts] ORDER BY [PostDate] DESC"></asp:SqlDataSource>
Pokud chceme zobrazovat záznamy, máme nastavený SelectCommand. Pokud chceme záznamy přidávat, nastavíme InsertCommand. Místo hodnot, které chceme dosadit, použijeme parametry. Doporučuji je pojmenovat stejně jako příslušné sloupce, ať je jasné, co k čemu patří. Nastavte tedy InsertCommand, kód by pak měl vypadat takto:
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
SelectCommand="SELECT [PostId], [Title], [Message], [Author], [PostDate] FROM [Posts] ORDER BY [PostDate] DESC"
InsertCommand="INSERT INTO [Posts] ([Title], [Message], [Author], [PostDate]) VALUES (@Title, @Message, @Author, GETDATE())"></asp:SqlDataSource>
Jako hodnotu do sloupce PostDate jsme nedali parametr, ale funkci GETDATE(), která vrátí aktuální datum a čas v době provádění příkazu. Tím se automaticky vloží aktuální datum a čas a nemusíme se tedy o něj starat. Nyní ještě musíme nadefinovat parametry dotazu a říci, jaké budou mít datové typy. Upravte tedy kód komponenty takto:
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
SelectCommand="SELECT [PostId], [Title], [Message], [Author], [PostDate] FROM [Posts] ORDER BY [PostDate] DESC"
InsertCommand="INSERT INTO [Posts] ([Title], [Message], [Author], [PostDate]) VALUES (@Title, @Message, @Author, GETDATE())">
<InsertParameters>
<asp:Parameter Name="Title" Type="String" Size="50" />
<asp:Parameter Name="Author" Type="String" Size="50" />
<asp:Parameter Name="Message" Type="String" />
</InsertParameters>
</asp:SqlDataSource>
Celý tento postup jsme si mohli odpustit a "naklikat" to přes designer. Stačilo by kliknout na odkaz Configure Data Source v panelu úkolů pro SqlDataSource v Design Mode a na okně, kde se vybírá SQL příkaz rozkliknout tlačítko Advanced kde bychom zaškrtnuli Generate INSERT, UPDATE and DELETE commands. Tento postup by nám ovšem vygeneroval příkazy pro úpravy záznamů a pro jejich mazání, které nepotřebujeme. Chci ale, abyste viděli i kód a rozuměli, co dělá.
Jak funguje FormView a samotné přidávání záznamů
To, co jsme teď udělali, již umí přidat záznam do databáze. Bez jediného řádku programového kódu. Pokud si stránku spustíme, umožní nám FormView zadat nový příspěvek do diskusního fóra. Jakmile vyplníme potřebná pole, máme k dispozici dvě tlačítka. Možná jste si všimli jejich vlastností CommandName, tlačítko pro přidávání má hodnotu této vlastnosti Insert a tlačítko pro zrušení má hodnotu Cancel. A to je celé kouzlo - jakmile klikneme na tlačítko, FormView si sám zachytí událost ItemCommand, která nastane, když odešleme formulář kterýmkoliv tlačítkem či komponentou zevnitř. Pokud FormView zachytí příkaz Insert, automaticky vezme hodnoty všech polí, které mají nastaven Data Binding pomocí funkce Bind a předá je do hodnot parametrů příslušné komponenty SqlDataSource. Pak na této komponentě zavolá metodu Insert, aby se příkaz provedl. Podobně funguje úprava záznamů, opět to jde bez jediného řádku kódu.
Úprava komponenty DataList
Nyní je čas na úpravu vzhledu komponenty DataList, opravte tedy kód, aby vypadal takto:
<asp:DataList ID="DataList1" runat="server" DataKeyField="PostId" DataSourceID="SqlDataSource1">
<ItemTemplate>
<div class="post">
<h3><asp:Literal ID="TitleLiteral" runat="server" Text='<%# Eval("Title") %>' Mode="Encode"></asp:Literal></h3>
<p><asp:Literal ID="MessageLiteral" runat="server" Text='<%# Eval("Message") %>' Mode="Encode"></asp:Literal></p>
<p>
<small>Vložil <strong>
<asp:Literal ID="AuthorLiteral" runat="server" Text='<%# Eval("Author") %>' Mode="Encode"></asp:Literal></strong>,
<em>
<asp:Literal ID="PostDateLiteral" runat="server" Text='<%# Eval("PostDate", "{0:d. MMMM yyyy H:mm}") %>'></asp:Literal></em>.
</small>
</p>
</div>
</ItemTemplate>
</asp:DataList>
Máme zde opět binging expressions, tentokrát jen Eval, protože potřebujeme data vytáhnout z databáze. Zpět je již nevracíme. Rozdíl mezi Bind a Eval je právě v tom, jestli data při dalším odeslání budeme vracet zpátky. Pokud ano, musíme použít Bind, jinak nám stačí Eval. V posledním Eval na konci ještě specifikujeme formát, v jakém se má vypsat datum a čas publikování. Všimněte si, že vše vypisuji do komponenty Literal. Je to jednoduchý způsob, jak vypsat do stránky text. Pokud navíc komponentám Literal nastavím vlastnost Mode na hodnotu Encode, provede se automaticky převod speciálních znaků na HTML entity, a tím pádem zabráníme útoku script injection.
Ochrana proti útokům nebo hlouposti uživatele
ASP.NET se snaží chránit webové aplikace proti záškodníkům, co nejvíce to jde. Pokud se pokusím do fóra poslat jakýkoliv příspěvek, který bude obsahovat HTML značku, dostaneme výjimku.
Aplikace je sice ochráněna proti tomuto útoku, chybovou stránku samozřejmě můžeme přesměrovat na něco ve stylu V aplikaci došlo k chybě, opakujte akci znovu. Co ale dělat v případě, že píšeme např. fórum o webdesignu, kde chceme, aby nám uživatelé posílali své HTML, abychom jim mohli vysvětlit, proč nefungují a co je na nich špatně. Musíme tedy kontrolu vypnout, a to přímo v direktivě Page. Přidejte tedy do prvního řádku ve stránce parametr ValidateRequest="false". Tím se kontrola vypne a požadavek se provede s tím, co uživatel poslal. Znamená to, že si ale musíme všechny vstupy zkontrolovat a všude provádět HTML endocing. První řádek by měl vypadat takto:
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" ValidateRequest="false" %>
Vzhledem k tomu, že používáme parametry v SQL příkazu, vyhnuli jsme se útoku SQL injection, a díky tomuto opatření a použití komponent Literal se zapnutým HTML encodingem jsme zabránili i útoku script injection. Aplikace by tedy měla být zabezpečená proti útokům.
Validace vstupů
Proti útokům již aplikaci zabezpečenou máme, nikolivěk proti hlouposti nebo nepozornosti uživatele. Pokud uživatel nevyplní předmět, nastane chyba, protože se budeme snažit vložit prázdnou hodnotu do sloupce, kde být nemá (nepovolovali jsme hodnoty NULL v databázi nikde). Chce to tedy zobrazit červenou hvězdičku vedle textového pole, pokud není vyplněno.
Validace je v ASP.NET vyřešena velice elegantně - máme k dispozici sadu validátorů, tedy komponent, které kontrolují vstup (jednak hned na klientovi poocí javascriptu, pokud to jde, ale hlavně i na serveru). Pokud je vstup špatný, komponenta FormView data do databáze nepošle. Stránka se vrátí klientovi zpátky a zobrazí se validátory. Najděte si tedy šablonu komponenty FormView a přidejte za tetové pole pro předmět příspěvku komponentu RequiredFieldValidator. Tento validátor zaručí, že se neprovede žádná akce, pokud nebude komponenta vyplněná. Hodnotu vlastnosti ErrorMessage nastavte na znak *, je to to, co se zobrazí, pokud je vstup špatně. Přidejte také vlastnost ControlToValidate="TitleTextBox", kde její hodnota je ID komponenty, kterou kontrolujeme. Celý kód validátor by měl vypadat takto:
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ErrorMessage="*" ControlToValidate="TitleTextBox"></asp:RequiredFieldValidator>
Sami si zkuste přidat validátory i pro další dvě textová pole, mělo by to vypadat takto:
<asp:FormView ID="FormView1" runat="server" DataKeyNames="PostId" DataSourceID="SqlDataSource1"
DefaultMode="Insert">
<InsertItemTemplate>
<h2>
Přidat příspěvek do fóra</h2>
<table>
<tr>
<td>Předmět:</td>
<td>
<asp:TextBox ID="TitleTextBox" runat="server" Text='<%# Bind("Title") %>' Width="400px" MaxLength="50"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ErrorMessage="*" ControlToValidate="TitleTextBox"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>Autor:</td>
<td>
<asp:TextBox ID="AuthorTextBox" runat="server" Text='<%# Bind("Author") %>' Width="400px" MaxLength="50"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ErrorMessage="*" ControlToValidate="AuthorTextBox"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>Zpráva:</td>
<td>
<asp:TextBox ID="MessageTextBox" runat="server" Text='<%# Bind("Message") %>' Width="400px" Height="100px" TextMode="MultiLine" MaxLength="2000"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator3" runat="server" ErrorMessage="*" ControlToValidate="MessageTextBox"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center">
<asp:Button ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert" Text="Přidat příspěvek" />
<asp:Button ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Storno" />
</td>
</tr>
</table>
</InsertItemTemplate>
</asp:FormView>
Možná jste si všimli vlastností CausesValidation u tlačítek - tlačítko Storno má tuto vlastnost nastavenou na hodnotu false, což znamená, že pokud na něj klikneme, validace se provádět nebude. Zrušit přidávání totiž můžeme i když jsme některé z polí nevyplnili. Byl by nesmysl nutit uživatele vyplnit všechna pole, pokud chce přidávání zrušit. Ve většině případů možná tlačítko Storno vymažete úplně, ani v tomto případě nemá moc význam.
Pokročilejší validace
Pokud je některá stránka složitější, potřebujeme často validovat některé komponenty při kliknutí na jedno tlačítko, a validovat jiné komponenty při kliknutí na tlačítko jiné. I to je možné - stačí všem tlačítkům, textovám polím a validátorům nastavit vlastnost ValidationGroup="něco", kde něco je libovolný název skupiny (je jedno jaký, ale komponenty, které se mají validovat najednou, jej musí mít stejný). Pokud kliknete na tlačítko a má tuto vlastnost vyplněnou, zkontrolují se jen ty komponenty, které mají vlastnost ValidationGroup nastavenou na stejnou hodnotu jako toto tlačítko. Takto můžete jedněm komponentám přidat ValidationGroup="prvni" a druhým ValidationGroup="druha" a tím je rozdělíte na dvě skupiny. Jedno tlačítko bude validovat jen první skupinu, druhé jen tu druhou.
Vylepšení vzhledu fóra
Nyní můžeme přidat do stránky opět nějaký CSS soubor, aby fórum vypadalo lépe. Nejsem designér ani grafik, takže nemám estetické cítění, ale mohlo by to vypadat třeba nějak takto:
To je pro tento díl vše. Pokud chcete vědět, jak jsem udělal střídání barev, vězte, že stačí zkopírovat v komponentě DataList šablonu ItemTemplate a přidat ji do komponenty ještě jednou jako AlternatingItemTemplete. Pak jen v jedné nastavíte elementu div jinou hodnotu atributu class a ostylujete zvlášť. Komponenta DataList použije pro renderování každé sudé položky šablonu AlternatingItemTemplate, pokud je tato šablona vyplněna.
Příště si ukážeme úpravy a mazání záznamů a v některém z příštích dílů se podíváme na správu uživatelů. Doufám, že se vám seriál líbí, omlouvám se, že jsem si dal s tímto dílem na čas, a doufám, že další díl vyjde již brzy. Jakékoliv připomínky či náměty pište do fóra.