RFC – Container::setDefaults() opravit chybu + BaseControl::setDisabled() vylepšit funkci

m.brecher
Generous Backer | 765
+
0
-

Ahoj,

mám návrh na vylepšení funkce Nette formulářů ve dvou místech, které spolu úzce souvisí.

Mizející $value v disablovaném prvku po submitu formuláře

Problém řeším cca 2 roky v tomto vlákně zde: https://forum.nette.org/…ba-framework

Dnes se mě podařilo chybu přesně lokalizovat a ve zdrojovém kódu Nette opravit a opravu vyzkoušet.

Chyba spočívá v tom, že pokud do formulářového prvku který je disablován setDisabled() pošleme defaultní data setDefaults() po anchor události formuláře, tak po submitu formuláře data zmizí, což je nežádoucí chování. Pokud použijeme metodu setDefaultValue() data nemizí a je to OK.

Ukázka testu ve formuláři:

$form->addText('disabled', 'Disablovaný')
    ->setDisabled()->setOmitted(false);

$form->addSubmit('send', 'Odeslat')
    ->onClick[] = $form->handleSend(...);

$form['disabled']->setDefaultValue('abcd');         //  OK
$form->setDefaults(['disabled' => 'abcd']);         //    OK

$form->onRender[] = fn() => $form['disabled']->setDefaultValue('abcd');     // OK
$form->onRender[] = fn() => $form->setDefaults(['disabled' => 'abcd']);       //  NOT OK

$form->onValidate[] = fn() => $form->addError('is_submitted');

return $form;

Při pohledu do zdrojového kódu je chyba zřejmá. Zatímco metoda BaseControl:setDefaultValue() používá pro nastavení dat podmínku:

$this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()

metoda Container::setDefaults používá jinou podmínku:

!$form || !$form->isAnchored() || !$form->isSubmitted()

PR je zde: https://github.com/…rms/pull/326

Nevhodné mazání dat z prvku při volání setDisabled()

V dokumentaci k disablování prvku je uvedeno https://doc.nette.org/…rms/controls#…, že pokud chceme zobrazit v disablovaném prvku data, musíme dodržet předepsané pořadí volání setDisabled() a setDefaultValue(), jinak se předaná data smažou. Tento požadavek je nepříjemný a matoucí, protože není úplně logický a na pořadí volání by záležet nemělo.

Proč setDisabed() maže data v prvku ? Protože to tak je ve zdrojovém kódu napsáno a v době, kdy Nette formuláře vznikaly k tomu byl nějaký důvod.

Dnes jsem se na otázku zda by se data v disablovaném prvku mazat měly nebo neměly podíval detailně. Norma HTML definuje input s html atributem disabled jako prvek, který nelze editovat, data která obsahuje zobrazuje, ale neodesílá je zpět. Tedy žádné mazání již vložených dat, data, která v prvku jsou se nemažou a zobrazí. To je žádoucí chování.

OK tak tedy mazání dat z metody setDisabled() odstraníme. Potom můžeme metody setDisabled() a setDefaultValue() volat v libovolném pořadí a funkce se nezmění. A nevznikne nám nějaký problém tím, že jsou v prvku nějaká data, která se nesmažou ? Protože když setDisabled() vznikalo tak tam nějaký důvod byl!! Data se do prvku mohou dostat dvěmi cestami:

a) setDefaults() + setDefaultValue()
b) po submitu formuláře od uživatele

V případě a) data do prvku posílá vývojář a ví proč, protože data chce zobrazit – tyto data by se mazat neměly, naopak jejich smazání vadí.

V případě b) data odesílá uživatel z formuláře – to znamená, že prvek ve fázi vykreslení formuláře (nesubmitnutý) nebyl disablován, jinak by uživatel data nemohl poslat a vývojář prvek disabluje až po submitu s použitím nějaké podmínky. Pokud vývojář nechal v první fázi prvek nedisablovaný a nechal uživatele vyplnit nějaká data, která po submitu chce dodatečně vymazat a prvek disablovat nedává to moc smysl. V takovém případě je přece logické prvek disablovat od počátku a nenechat tam data vyplnit. Pokud tato data metoda setDisabled() nesmaže ale ponechá, žádný problém tím nevznikne.

Myslím, že odstraněním mazání dat po zavolání setDisabled() žádný problém nevznikne, byť je to BC break. A použití této metody bude bez záludností a funkce 100%. Dnes je totiž stav takový, že metodu setDisabled() můžeme použít jak pro zobrazení needitovatelných dat, tak pro smazání dat z prvku – kterou funkci framework použije záleží na zmíněném pořadí volání metod. To není dobře navržené API, to by se mělo opravit.

PR je zde https://github.com/…rms/pull/327

Prosím o komentáře a připomínky k navrženým úpravám.

Editoval m.brecher (21. 4. 2:10)

David Grudl
Nette Core | 8157
+
+5
-

Prohlížeče disablované prvky neodesílají. Proto je nelze přijímat. Pokud prvek nějaká data zvenčí přijal v době, kdy ještě nevěděl, že je disablovaný, musí je smazat. Tady přece není o čem diskutovat.

V případě b) data odesílá uživatel z formuláře – to znamená, že prvek ve fázi vykreslení formuláře (nesubmitnutý) nebyl disablován, jinak by uživatel data nemohl poslat

Ten, kdo posílá data, je ve všech úvahách v první řadě útočník. Takhle je Nette postavené. Každá úvaha musí vycházet z nejhoršího možného scénáře. Formuláře musí být v první řadě neprůstřelné. Featury jsou až v druhé řadě. Když tady občas čtu, jak si někdo posílá data v hidden fieldech nebo staví na atributu „readonly“, tak facepalm. Ty data může kdokoliv změnit. Těmto nápadům zabránit nedokážu, ale aspoň můžu mazat data disablovaných prvků.

Pokud vývojář nechal v první fázi prvek nedisablovaný a nechal uživatele vyplnit nějaká data, která po submitu chce dodatečně vymazat a prvek disablovat nedává to moc smysl.

To je úplně normální stav. Měj odeslaný formulář, který již je připojený k presenteru. A zavolej tento kód:

$form->addText('foo') // nyní vznikl nedisablovaný prvek, který natáhl data z HTTP requestu
	->setDisabled(); // teď se teprve dozvídáme, že je disablovaný, tak podvržená data mažu

Tyjo, za 25 let co dělám weby jsem ještě ani jednou nepoužil disabled prvek, ale je to snad nejprobíranější téma kolem Nette formulářů :-) Fakt vůbec nechápu proč, kašlat na ně, šak ve vrácených datech nejsou. A rozhodně nechci kvůli nim překopávat nebo narušovat leta prověřenou architekturu Nettích formulářů.

m.brecher
Generous Backer | 765
+
0
-

@DavidGrudl

Když tady občas čtu, jak si někdo posílá data v hidden fieldech nebo staví na atributu „readonly“, tak facepalm.

V dokumentaci k disablovaným prvkům není o zabezpečení proti útoku žádná zmínka, takže jsem si to včera otestoval a zabezpečení je kvalitní, nejen mazání dat přímo v metodě setDisabled(). Jsou tam další ochranné mechanismy a blokuje se javascriptem odstraněné disablování submit buttonu. Samozřejmě mazání dat musí v setDisabled() zůstat, to jsem nedomyslel fatální důsledky.

Nicméně je škoda, že dokumentace zabezpečení disabled/hidden/readonly prvků vůbec nezmiňuje. Pak nováčci uvažují nad použitím html readonly nebo důvěřují hidden prvkům. Přitom hidden prvek by se dal elegantně zabezpečit plánovaným setReadonly(). Jakmile by se přidala do formulářů metoda setReadonly() mohli bychom dokumentaci v tomto směru doplnit.

David Grudl
Nette Core | 8157
+
+1
-

Na tohle dokumentace žádného webového frameworku neupozorňuje. Programátor prostě nesmí spoléhat na data zvenčí. Ale já tu větu u hidden fieldu přidal.

U disabled prvků žádné riziko není, tam se data zvenčí dostat nemůžou, protože je nette maže.

m.brecher
Generous Backer | 765
+
0
-

@DavidGrudl

U disabled prvků žádné riziko není, tam se data zvenčí dostat nemůžou, protože je nette maže.

Záleží na správném použití:

$form->onRender[] = fn() => $form['field']->setDisabled(); // dangerous !!

Na tohle dokumentace žádného webového frameworku neupozorňuje.

Dokumentace by měla popsat funkci. U disablovaných prvků není v aktuální dokumentaci ani zmínka o nějakém zabezpečení ze strany frameworku, takže třeba já jsem 3 roky předpokládal, že si ochranu musím udělat sám. Až včera jsem si to otestoval a byl překvapen, že tam ochrana je a kvalitní. Tohle bych tam doplnil + upozornění, že disablovat prvek po zpracování submitu představuje bezpečnostní riziko.

Hidden: https://doc.nette.org/…rms/controls#…

Napsal Jsi to tam výborně, pokud přidáme metodu setReadonly() tak tato metoda by se dala použít pro zabezpečení hidden prvků jednoduše a elegantně a to by se tam mohlo jednou větou zmínit jako tip/doporučení.

U setReadonly() může pro nováčka v Nette být matoucí že jakoby „odesílá“ needitovatelná data na server viz tento nedávný příspěvek na fóru: https://forum.nette.org/…-basecontrol#… takže bych tam dvě tři věty o tom jak setReadonly() alis k setDisabled()->setOmitted() reálně funguje + že předpokládá naplnění setDefaultValue() dal. Ne každý si to domyslí.

David Grudl
Nette Core | 8157
+
0
-

Za mě je ta docka pro disbled prvky dostatečná, nevím co bych tam měl doplnit

David Grudl
Nette Core | 8157
+
0
-

Ještě jedna poznámka. Ten název případné metody setReadonly() jsme už myslím probírali – není dobrej, protože hodně lidí bude očekávat, že to nastaví HTML atribut readonly. Vhodnější by bylo třeba preserveDefaultValue() nebo něco takového.

Mimochodem, jeden prvek umí – hidden field. V konstruktoru se dá nastavit, že má persistentní hodnotu.

m.brecher
Generous Backer | 765
+
0
-

@DavidGrudl

setReadonly() jsme už myslím probírali – není dobrej, protože hodně lidí bude očekávat, že to nastaví HTML atribut readonly

To máš asi pravdu

Vhodnější by bylo třeba preserveDefaultValue() nebo něco takového.

a co setPreserved() je to kratší a zapadá to do názvosloví metod, které modifikují chování prvku, lépe by se to pamatovalo:

  • setDisabled()
  • setOmitted()
  • setPreserved()
Kamil Valenta
Člen | 764
+
+3
-

m.brecher napsal(a):

Záleží na správném použití:

$form->onRender[] = fn() => $form['field']->setDisabled(); // dangerous !!

Tohle už je podobné situaci, kdy programátor v šabloně vypíše GET s noescape a bude si stěžovat, že je ve frameworku díra…
Je nějaký relevantní důvod, proč se o disabled dozvědět až onRender? Skutečně o tom neví už jeho Factory?
Zatím jsem nepotkal situaci, která by se řešila jinak, než že se formulář vytvoří a teprve pak se do něj nalejou default data…

m.brecher
Generous Backer | 765
+
0
-

@DavidGrudl

Ještě jedna poznámka. Ten název případné metody setReadonly() jsme už myslím probírali – není dobrej, protože hodně lidí bude očekávat, že to nastaví HTML atribut readonly. Vhodnější by bylo třeba preserveDefaultValue() nebo něco takového.

Smyslem mého RFC návrhu bylo existující syntax setDisabled()->setOmitted(false) nahradit jednoduchým, dobře zapamatovatelným a čitelným názvem. Navržená setReadonly() nabízí přesně to samé jako html atribut readonly s těmito rozdíly:

  • je zabezpečená proti podvržení
  • dá se použít i na všech prvcích
  • prvky nemají focus – to je dobře
  • pro správnou funkci vyžaduje nastavení setDefaultValue()

setReadonly() je bezpečnou variantou html readonly s vylepšenou funkcí (focus, všechny prvky) a když by se do dokumentace jasně napsalo, že nepoužívá atribut readonly a je vyžaduje použít setDefaultValue(), tak by s tím nikdo neměl mít problém, aby si to pletl s html readonly.

Měli bychom dvojici:

  • html readonly – nezabezpečené
  • Nette setReadonly() – zabezpečené

stejně jako nyní máme:

  • html disabled – nezabezpečené
  • Nette setDisabled() – zabezpečené

Šlo by i použít nějaké kombinace třeba:

  • setPreservedReadonly()
  • setSafeReadonly()