netteForms a morální dilema

David Grudl
Nette Core | 8227
+
0
-

Přemýšlím nad zajímavým rozhodnutím, jak se má chovat javascriptová obsluha formulářů netteForms.js. O co jde? Nejprve rekapitulace.

Formuláře mají celkem důmyslně nastavená validační pravidla. Základní validace přichází s volbou samotného prvku:

// validuje, že vstup je číslo
$form->addInteger('age', 'Věk');

a dále ji doplníme pravidly:

// validuje, že vstup je číslo
$form->addInteger('age', 'Věk')
	->setRequired() // musí být vyplněné
	->addRule($form::MIN, null, 18); // alespoň 18

U Nette formulářů získáváme zdarma validaci i na straně klienta, kterou provádí právě skript netteForms.js.

Nicméně mohu si vytvořit i běžný textový prvek a doplnit mu validační HTML atributy:

$form->addText('age', 'Věk')
	->setHtmlAttribute('required')
	->setHtmlAttribute('maxlength', '3');

Vliv atributů jako je required, min nebo třeba pattern a podobně, tak ten netteForms.js zcela vyignoruje. Pro něj je směroplatná pouze validace definovaná v PHP. Pouze validace provedená na serveru hraje roli. Co se definuje na straně klienta je pouhá kosmetika, kterou lze snadno obejít útočníkem.

Speciální případ nastává, když vytvoříme políčko pro vstup s číslem:

$form->addText('age', 'Věk')
	->setHtmlAttribute('type', 'number');

Opět, je to jen kosmetika. Nicméně v tomto případě netteForms.js kontroluje platnost vloženého čísla. Vždy, bez toho, že by jakákoliv validace byla specifikována v PHP kódu.

Důvod je jednoduchý: pokud by uživatel zadal neplatnou hodnotu, prohlížeč ji na server neodešle.

Nette se tedy nesnaží kontrolovat, zda číslo je opravdu číslo, ale snaží se zabránit tomu, aby neplatná hodnota byla tiše smazána. Neplatnou hodnotou je třeba i 18. napsané s tečkou na konci.

Sešly se mi tu dvě protichůdné issues:

Já bych to rozšířil ještě o možnou třetí cestu, a sice že by tohle chování zůstalo, ale respektovalo by nastavený validation scope. Tedy implicitně by se platnost prvků v prohlížeči kontrolovala, ale šlo by to na straně PHP explicitně vypnout. Ale vůbec nevím, jestli by to tak bylo lepší.

A teď babo raď. Jak se chovat k type=number, pokud není podložené validačním pravidlem na straně PHP? Budu rád za názory, ale prosím, zkuste se nad tím alespoň na pět minut zamyslet, není to triviální.

Marek Bartoš
Nette Blogger | 1274
+
+1
-

Já bych se html atributy nikdy neřídil. Jde je vkládat přes setHtmlAttribute(), přes manuální render, přes javascript. Na serveru o nich tedy nebudeš vědět s jistotou nikdy. A nejde tedy o spolehlivou validaci. Validace musí být vždy na serveru a pokud probíhá i v prohlížeči, tak je to bonus. Pokud probíhá v prohlížeči a na serveru už ne, je to chyba.

Naopak, pokud by programátor definoval validační html atribut, který neodpovídá tomu, co má Nette nastavené ve vlastních pravidlech, tak bych ho na to v konzoli prohlížeče upozornil.
Hodnota se odešle na server, na serveru validace spadne. Došla validace neúspěšně na server? Tak to se nám v prohlížeči chová jinak. Otevřeme konzoli a v ní už je upozornění kde je problém. A víme, že místo nastavení html atributu musí programátor najít řešení přes validační interface v Nette.

A pokud by bylo možné rozlišit nastavení html atributů přes setHtmlAttribute volané interně uvnitř třídy s formulářovým prvkem a externě při přidání prvku do formuláře programátorem, tak bude možné na přidání – na serveru nefunkční – validace upozornit ve většině případů už na serveru.

Editoval Marek Bartoš (5. 5. 15:12)

m.brecher
Generous Backer | 871
+
0
-

Ahoj,

Prostudoval jsem si oba PR, které požadují opačné chování a oba dva jsou svým způsobem argumentačně podložené. Nelze oběma současně vyhovět.

Příčina je v tom, že Nette umožňuje vnitřně rozporné nastavení:

$form->addText('age', 'Věk')
    ->setHtmlAttribute('type', 'number');

Jak to má Nette interpretovat??

jako:

$form->addInteger('age', 'Věk')      // chci <input type="nubmer"...>

nebo jako:

$form->addText('age', 'Věk', 2, 2)	// chci <input type="text"...>
    ->addRule($form::Integer);      // s přidanou validací Integer

Vývojář totiž může chtít jedno nebo druhé, ale zvolil nejasný zápis.

Řešením by bylo vyloučit vnitřně rozporné zápisy, kdy zvolím typ prvku a poté jej pomocí setHtmlAttribute(‚type‘, ‚something‘) měním a donutit vývojáře specifikovat typ prvku předepsaným způsobem. V chybové hlášce by mohlo být navedení správným směrem např:

$form->addText('age', 'Věk')
    ->setHtmlAttribute('type', 'number');   // Exception
HtmlInputTypeMismatchException
Change type of input TextInput is illegal, use appropriate form control or validation rule

V Nette formulářích je ale ještě jiný problém, který s diskutovaným souvisí. Uvedu příklad. Chtěl bych mít textové políčko o délce dva znaky s přidanou validací Integer, ale nechci prvek <input type=„number“ …> protože ten mě nevyhovuje. Napíšu korektní kód:

$form->addText('age', 'Věk', 2, 2)	// chci <input type="text"...>
    ->addRule($form::Integer);      // s přidanou validací Integer

ale Nette udělá něco jiného – automaticky změní typ prvku na <input type=„number“ …>. Co jiného můžu dělat než zkusit vnitřně rozporný zápis?

$form->addText('age', 'Věk')
    ->setHtmlAttribute('type', 'number');

Ten ale vede na zřejmě neřešitelné dilema o kterém je toto vlákno.

Závěr

Mnou navrhovaná změna je bohužel BC break, ale odstraňuje nekonzistentní chování formulářů co se týče typu prvku. Tím se vyřeší dilema a zpřehlední kód.

a) nikdy neměnit typ prvku zvolený v kódu $form->addText() ⇒ vždycky <input type=„text“…>
b) vyloučit dodatečnou změnu typu ->setHtmlAttribute(‚type‘, ‚???‘)
c) umožnit přidání atypického validačního pravidla $form::Integer do jiných než number prvků – má to svoje použití

Poznámka: validátor $form::Integer je v nápovědě IDE přeškrtnutý – plánuje se jeho vyřazení ?? To by byla chyba, protože má praktické použití. Doporučuji validátor $form::Integer ve formulářích ponechat.

Editoval m.brecher (5. 5. 15:38)

David Grudl
Nette Core | 8227
+
+1
-

Ano, napsal jsi to dobře, jde o vnitřně rozporné nastavení.

Ale do detekcí povolených kombinací typů se pouštět nechci. Sám používám addText() v kombinaci se změnou typu na search nebo addInteger() se změnou typu na text nebo třeba range a je to úplně ok.

ps. přeškrtnutá by měly být jen SCREAMING_SNAKE_CASE varianta pravidel.

m.brecher
Generous Backer | 871
+
0
-

@DavidGrudl

Souhlasím s @MarekBartoš, že nejlepším řešením v Nette javascriptové validaci je ručně nastavené html atributy ignorovat. Atribut type=number prvku input má přece vestavěnou html validaci a maskování vstupu, které neumožní zadat nenumerickou hodnotu.

Když se vývojář rozhodne nepoužít Nette validační prostředky a místo nich použije html, nechme validaci na něm. Předejde se tím situacím, kdy dobře míněná přídavná nette javascriptová validace v některých kombinacích způsobí problém.

Já bych to rozšířil ještě o možnou třetí cestu, a sice že by tohle chování zůstalo, ale respektovalo by nastavený validation scope. Tedy implicitně by se platnost prvků v prohlížeči kontrolovala, ale šlo by to na straně PHP explicitně vypnout. Ale vůbec nevím, jestli by to tak bylo lepší.

Je to komplikované a asi to stejně nikdo využívat nebude. Proč vývojáři “přetypovávají” input text na number ? Použití addInteger() přece poskytne stejnou funkci a navíc zabezpečenou. Myslím, že je to neznalost, nikoliv promyšlený záměr. Pochybuji, že by někdo explicitní vypínání javascriptové validace použil.

dokumentace

Přetypování typu inputu přináší v některých případech bezpečnostní rizika, v některých ne. Začínající vývojář se v problematice nemusí plně orientovat. Dokumentace v odstavci o nastavení Html atributů zmiňuje možnost nastavit type jako běžnou funkci aniž by zmiňovala případná rizika: https://doc.nette.org/…ms/rendering#…

citace z dokumentace:

Nastavení typu:

$form->addText('tel', 'Váš telefon:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'napište telefon');

Co kdyby se do dokumentace doplnil nový odstavec Nastavení Html typu, kde by se problematika trochu nastínila, protože některá nastavení odlišných typů jsou logická a bezpečná, některá jsou nebezpečná ?? Třeba před přetypováním text na number bych varoval a doporučil použití addInteger().

Editoval m.brecher (5. 5. 20:09)

David Grudl
Nette Core | 8227
+
0
-

Tak bych řekl, že jsme se vůbec, ale vůbec, nepochopili :-)

m.brecher
Generous Backer | 871
+
0
-

Tak bych řekl, že jsme se vůbec, ale vůbec, nepochopili :-)

Pokusil jsem se pochopit správně první citované issue https://github.com/…s/issues/289, kde autor dodal vzorek testovacího kódu a popsal chybu. Pointa byla v tom, že pokud se do prvku number vyplní nečíselná hodnota + další podmínky, formulář po odeslání nezobrazí žádnou chybu, ale měl by.

Testovací kód:

$form = new Nette\Application\UI\Form();

$dep = $form->addCheckbox('dep', 'Num required');

$num = $form->addText('num', 'Number');
$num->setHtmlAttribute('type', 'number');
$num->addConditionOn($dep, $form::Equal, true)->setRequired('pole Number je povinné');

$form->addText('required', 'Required')->setRequired('pole Required je povinné');

$form->addSubmit('submit', 'done')
    ->onClick[] = $this->handleTestNumber(...);

return $form;

V Google Chrome vůbec nejde vložit do pole number nečíselnou hodnotu, ale jde to např. ve Firefoxu. Ve Firefoxu jsem podle popisu reprodukce chyby:

  • zaškrtnul checkbox
  • vyplnil do pole num neplatnou hodnotu xx
  • pole required jsem ponechal nevyplněné
  • odeslal formulář

Formulář se neodeslal, html 5 validace zachytila chybu v poli num a hlásí „Zadejte prosím platné číslo“

Píšeš, že nette javascript kontroluje platnost čísla:

Nicméně v tomto případě netteForms.js kontroluje platnost vloženého čísla.

V uvedeném vzorku validuje chybu number html 5 a netteForms.js se nijak neaktivují.

Závěr

Chybu uvedenou v issue se mě nepodařilo reprodukovat a dle mnou provedeného testu ve Firefoxu se aktivuje html5 validace, která je zdá se dostatečnou zábranou před odesláním neplatné hodnoty.

Proto mě přijde zbytečné k funkční html5 validaci inputu number přidávat javascriptové vylepšování.

David Grudl
Nette Core | 8227
+
0
-

m.brecher napsal(a):

Pokusil jsem se pochopit správně první citované issue https://github.com/…s/issues/289, kde autor dodal vzorek testovacího kódu a popsal chybu. Pointa byla v tom, že pokud se do prvku number vyplní nečíselná hodnota + další podmínky, formulář po odeslání nezobrazí žádnou chybu, ale měl by.

Ano. Vlastně žádné další podmínky nejsou potřeba.

V Google Chrome vůbec nejde vložit do pole number nečíselnou hodnotu, ale jde to např. ve Firefoxu. Ve Firefoxu jsem podle popisu reprodukce chyby:

Jde to i v Chrome, stačí za číslo dát tečku.

Formulář se neodeslal, html 5 validace zachytila chybu v poli num a hlásí „Zadejte prosím platné číslo“

V uvedeném vzorku validuje chybu number html 5 a netteForms.js se nijak neaktivují.

Pokud je aktivní netteForms, tak to dělá on, ale to není zas tak podstatné, uživatel se o chybě dozví.

Proto mě přijde zbytečné k funkční html5 validaci inputu number přidávat javascriptové vylepšování.

Tak otázka nestála. Je tu ta druhá issue, která požaduje úpravu, tedy aby se chybová hláška nezobrazovala, když tam není validační pravidlo.


Ale to je jedno, zůstanu u současného řešení, nemusíme to dál rozebírat.

m.brecher
Generous Backer | 871
+
0
-

Ověření issue číslo 2

Otestoval jsem si ve Firefoxu funkci vzorového kódu předloženého v druhém issue https://github.com/…s/issues/328

$form = new Nette\Application\UI\Form();

$checkbox = $form->addCheckbox('checkbox', 'Need a number?');

$form->addInteger('number1', 'Number1')
    ->addConditionOn($checkbox, $form::Equal, true)
    ->setRequired('required number1');

$form->addText('number2', 'Number2')
    ->setHtmlType('number')
    ->addConditionOn($checkbox, $form::Equal, true)
    ->setRequired('required number2')
    ->addRule($form::Integer, 'Need a number');

$form->addSubmit('submit', 'done')
    ->onClick[] = $this->handleTestNumber(...);

return $form;

Autor popisuje chybu takto „When the input type number is used, it is always validated for the correct number format, even if it is not required“. Což není chyba, ale správná funkce. Pokud se použije input number, potom html5 validuje formát zadaného vstupu a vyhodí chybu při nečíselné hodnotě.

Přesně takto funguje vzorek kódu. Html5 validace hlásí v obou inputech „Zadejte prosím platné číslo“.

Autor dále popisuje tuto chybu: „When you input non-numerical text foo (e.g. in Firefox) into the inputs, the form cannot be submitted because of JS validation and Nette.invalidNumberMessage is shown even with unchecked checkbox.“

Při mém testu ve Firefoxu a poslední verzi nette/forms se nette javascriptová validace vůbec neaktivovala a formulář validoval pomocí html5 validace.

Autor dále popisuje Expected Behavior takto: „At least for `number2', the number format should not be validated unless the checkbox is actually checked. The rule explicitly says that the number is only required if the checkbox is checked. If it is unchecked, the input is ignored.“

To není dobrý návrh. Funkce nette formulářů je v pořádku, pokud je input typu number, nelze validaci Integer dodatečně podmínit, ale musí být natvrdo nastavená.

Závěr

Po podrobném prozkoumání a otestování údajných chyb ve validaci nette formulářů v obou citovaných issue konstatuji, že chyby ve validaci uváděné v issue se buďto nepodařilo reprodukovat, nebo se nejedná o chyby ale o správné chování. Nepodařilo se mě ale ani v jednom případě aktivovat nette javascriptovou validaci o níž @DavidGrudl píše zde:

Opět, je to jen kosmetika. Nicméně v tomto případě netteForms.js kontroluje platnost vloženého čísla. Vždy, bez toho, že by jakákoliv validace byla specifikována v PHP kódu.

a zde:

Důvod je jednoduchý: pokud by uživatel zadal neplatnou hodnotu, prohlížeč ji na server neodešle.

Závěr mám ten, že v html5 formulářích, pokud si uživatel zvolí dobrovolně html atribut number místo addInteger() plně postačuje ochrana html5 validací a není nutné v tomto případě přidávat jakokoliv další ochranu javascriptem.

David Grudl
Nette Core | 8227
+
0
-

Jak je aktivní netteForms, tak přebírá kompletní řízení, žádná HTML5 validace se neprovádí.

Asi máš dnes odpoledne vydanou verzi netteForms, která tu chybu zobrazuje takovým tím prohlížečovým způsobem, takže tě to plete, ale stáhni si starší verzi a uvidíš hlášky v tom klasickém dialogu uprostřed stránky. Liší se jen forma toho, jak netteForms chybu vizualizuje.

mystik
Člen | 312
+
0
-

Za me oba pristupy davaji smysl. Ale ja se obecne ridim pravidlem ze kod by mel co nejmene veci delat automagicky. Protoze je programator neceka. IMHO je vudy lepsi vyzadovat explicitni zadani nez implicitne udelat neco neocekavaneho.

Ale v tomhle pripade je to dost seda zona protoze tim ze netteForms vypnout HTML5 validaci by se dalo cekat ze ji nahradi. Aby se to na klientovi chovalo podobne s i nez netteForms.

Kamil Valenta
Člen | 820
+
0
-

Validace a atribut type jsou dvě odlišné věci a nemyslím si, že by měly být jakkoli spojovány. Pokud nemám žádnou ->addRule(), nečekal bych, že mi netteForms nějakou validaci vykouzlí dle type. A jen někdy.

Pokud očekávám číslo, můžu si dát ->setHtmlType(‚number‘), nebo ->addInteger(), nebo prostý text a mít nad tím jakoukoliv vlastní nadstavbu. Ale stejně přeci budu mít ->addRule(Form::Integer)…

David Grudl
Nette Core | 8227
+
+1
-

@KamilValenta a očekával bys, že se ti občas nějaká vyplněna položka nečekaně ztratí?

Myslím, že to by nemělo nastat za žádných okolností. Takže raději tu nečekanou validaci zachovám

Kamil Valenta
Člen | 820
+
0
-

David Grudl napsal(a):

@KamilValenta a očekával bys, že se ti občas nějaká vyplněna položka nečekaně ztratí?

Samozřejmě neočekával, ale Tvá otázka mě do té situace tlačí :) Přeci pokud bude mít input validaci na int, protože „proč by ji neměl mít“, tak se nikdy nic neztratí, ať už dělám s type čachry jaké chci…

Není to trochu akademická debata? Řeší se problémový vstup, který ale není validací ošetřen, takže si autor o problémy sám říká…

Myslím, že to by nemělo nastat za žádných okolností. Takže raději tu nečekanou validaci zachovám

Chápu původ té myšlenky, kořen problému je, že prohlížeč nemá co mluvit do toho, zda data odešle nebo ne…
I tak to ale nebude „to by nemělo nastat za žádných okolností“, protože vypnutý JS zase narazí na chybějící server side validaci.

GEpic
Člen | 566
+
0
-

Já bych to chápal takto:

Validace na straně klienta (input number):

$form->addText('age', 'Věk')
    ->setHtmlAttribute('type', 'number');

Validace na serveru (input number)

$form->addInteger('age', 'Věk')

Validace na serveru (input text)

$form->addText('age', 'Věk', 2, 2)
    ->addRule($form::Integer);

Ale nikdy by mě nenapadlo první případ nikde používat. Formulářů už jsem napsal spoustu. Nette by mělo validovat pouze addInteger|Float|Email atp. a pravidla skrze addRule().