Komplex példa - Validálás - 2. rész

Attila | 2022. 07. 17. 19:17 | Olvasási idő: 5 perc

Címkék: #Blade #Érvényesítés (Validation) #HTML #JavaScript #Laravel #Nézet (View) #Űrlap (Form)

Maradunk a kliens oldali validációnál, de most már megnézzük, hogy milyen lehetőségeink vannak, ha a Javascript-et is használjuk. Megvizsgáljuk továbbá a Constraint Validation API lehetőségeit is. Mindezeket egy-egy gyakorlati példán keresztül ki is próbáljuk. Ezután felsorolok néhány validáció-specifikus Javascript keretrendszert, amelyek érdemesek a részletesebb megismerésre...
javascript_form_validation

Bevezetés

Továbbra is az űrlapokat vizsgáljuk, hiszen ezeken keresztül tudunk a legkönnyebben kapcsolatba kerülni a felhasználóinkkal. Én most külön Javascript oktatást nem szeretnék tartani a blogomban, de több egyetemi kollégám készített a nyelvről és alkalmazásáról egy elég jó kezdeti összefoglalót, amit szívesen megosztok itt veletek: http://webprogramozas.inf.elte.hu/tananyag/kliens/ és ez szintén hasznos lehet: http://webprogramozas.inf.elte.hu/tananyag/weaf1/

A HTML5-ös űrlap bemeneti elemeknél végzett megszorítások segítenek a felhasználóknak megfelelően kitölteni egy mezőt, azonban nem védenek a támadások ellen. Induljunk ki onnan, hogy valaki "megpróbálja feltörni" a HTML5-ös validációnkat, tehát mint ahogy az előző bejegyzésben is írtam, élőben belenyúl a kódba és kitörli mondjuk a "required" attribútumot a HTML tag-ünkből, majd úgy küldi el az űrlapot, hogy nem tölti ki a kötelezően kitöltendő mezőt. Mit tehetünk még ekkor a validációnk sikerességéért?

Javascript-et fogunk használni, ami persze még közel sem jelent majd számunkra tökéletes védelmet (olyan nincs is), de még egy fokkal nehezebbé tesszük az alkalmazásunk feltörését.


Ellenőrzés Javascript-tel

Ismét a komplex példánkat fogjuk elővenni és már a gyakorlatban használni fogjuk a Javascript validációs képességeit. Legelőször a Légitársaságokat (airlines) létrehozó űrlapon fogunk tevékenykedni, ehhez azonban biztosítsuk a helyet a script-jeinknek. A layout.blade.php fájlban a body záró tag-je elé helyezzük el ezt:

@stack('scripts')

Magyarázat és tipp: Nagyon hasonlóan a @yield - @section Blade direktíva pároshoz, itt a @stack - @push lesz a segítségünkre abban, hogy Javascript kódokat helyezzünk el az egyes gyermekoldalakon (a layout szülő nézetünket így nem kell teleszemetelnünk). A különbség a két páros között, hogy amíg egy gyerekoldalon egy yield-be csak egyszer tudunk kódot írni a section-nel, addig egy stack-be bármennyiszer szúrhatunk be (push-olhatunk a verembe) akár újabb és újabb kódokat, amennyit csak szeretnénk. Míg a yield-section párost inkább a HTML kódokhoz és a CSS/Javascript csomagok, osztálykönyvtárak importálására használjuk, addig a stack-push párost a változó CSS és főleg Javascript kódjaink beszúrására alkalmazzuk. A saját Javascript kódjainknak fenntartott stack-et pedig azért helyezzük el a body záró tag-je elé, mert a kódjainkat gyakran használjuk az oldalon megjelenített (render-elt) HTML kód manipulálására és ha akkor szeretnénk megváltoztatni valamelyik HTML elemet, amikor még nem került megjelenítésére, akkor hibát kapnánk a Javascript Console-ban, hogy még nem talált ilyen elemet a DOM fában... hmm... érzem azért, hogy használtam ebben a bekezdésben olyan kifejezéseket, amelyek nem biztos, hogy mindenki számára ismertek. Emiatt tényleg azt javaslom, hogy ha valami nem világos ezek közül, akkor érdemes megnézni ezt az oldalt és többminden is egyből világossá válik.

Megvan tehát a szülő layout nézetünkben a Javascript kódjaink helye, nincs más hátra, mint az utód nézet fájlba beillesszünk saját kódokat, amivel ellenőrizzük az űrlapunkat.

name="legitarsasagLetrehozo"
onSubmit="return validateForm()"

A form nyitó tag-jébe helyezzük el az iménti két új attribútumot. Az egyikkel nevet adunk az űrlapnak, míg az onsubmit attribútummal jelezzük majd az űrlapnak, hogy amikor a felhasználó rákattint a Mentés (Küldés) gombra, akkor milyen metódusnak kell végrehajtódnia a tényleges adatelküldés előtt. Hozzuk is létre és szúrjuk be a stack-be az új függvényünket (bár nincs jelentősége, én a @endsection után helyezem el a kódomat, de megtehetném, hogy a @section elé tenném be...):

@push('scripts')
<script>
  function validateForm() {
    let x = document.forms["legitarsasagLetrehozo"]["legitarsasagneve"].value;
    if (x == "") {
      alert("A nevet kötelező kitölteni");
      return false;
    }
  }
</script>
@endpush

Teszteljünk! Azonban egy kicsit könnyítsük meg a dolgunkat és ne csak "hackeljünk" azzal, hogy valós időben átszerkesztjük a form elemeit és kivesszük az input mezők required attribútumát. A tesztelés meggyorsítása miatt a create nézetet szerkesztve vegyük ki ezt a required attribútumot mindkét bemeneti elem nyitótag-jéből.

Ez így egész jó kiindulásnak. Most még azt csináljuk meg, hogy ne lehessen létrehozni olyan légitársaságot, aminek nincsen telephelye... legalább 1 telephelye minden új légitársaságnak lennie kell, amit így szabályként meg is fogalmaztunk.

if (document.forms["legitarsasagLetrehozo"]["cities[]"].selectedOptions.length < 1) {
  alert("Legalább 1 telephelyet állítson be");
  return false;
}

Adjuk hozzá az iménti feltételvizsgálatot a validateForm() függvényünkhöz. Itt a feltételvizsgálat csak akkor lesz igaz, hogy ha kevesebb mint 1 kiválasztott elem van a listában, persze ha azt szeretném megkövetelni, hogy több kiválasztott elemnek is lennie kell, mondjuk minimum 3 telephellyel kell rendelkeznie az új légitársaságnak, akkor azt is beállíthatnánk itt, minden csak tőlünk és a meghatározó szabályainktól függ. Egy oldalfrissítés után pedig teszteljük is le:

Látható, hogy a nevet megadtuk, de nem választottunk ki egy telephelyet sem, emiatt vissza is kaptuk a figyelmeztetést és az űrlap nem lett elküldve a szervernek. Ez persze nem a legszebb megoldás, pusztán csak a működést szerettem volna ábrázolni így.


Constraint Validation API

A legtöbb böngésző már támogatja a címben írt API (Application Programming Interface). Így például a gombokhoz, bemeneti mezőkhöz (input, textarea), select-hez van már egy olyan felület, amit alapértelmezetten használhatunk validációra és nem kell különböző egyéb Javascript osztályokat importálnunk. Minden imént felsorolt elem automatikusan rendelkezik azzal a lehetőséggel, hogy validáljuk őket (ellenőrizhetjük a következőket: illeszkedik-e a bemenet egy mintára, túl rövid/hosszú-e a bemenet, túllép/nem ér el valamilyen limitet a szám bemenet értéke, típusprobléma e-mailek vagy URL-ek kapcsán, hiányzó bemenet, vagy egyszerűen csak valamilyen egyéb, általunk definiált szabálynak nem felel meg a bemenet). Ezeket tehát mind támogatja az API, illetve ezek ellenőrzését. Az API még azt is lehetővé teszi, hogy az imént felsorolt problémák ellenőrzésénél különböző hibaüzeneteket állíthassunk be a setCustomValidity(message) metódus segítségével. Az itt felsoroltakat kipróbáljuk a gyakorlatban is és megnézzük, hogy hogyan működnek, de előtte csinálunk a validateForm() függvényünkbe egy sima kiíratást.

console.log(document.forms["legitarsasagLetrehozo"]["cities[]"]);

Ekkor tudjuk ellenőrizni az általam leírtakat a böngésző Javascript Console-jában:

A validationMessage tartalmazza a hibaüzenetet, ha nem stimmel valamilyen ellenőrzés. A validity attribútum tartalmaz egy ValidityState objektumot, aminek látható a felsorolásában azok a dolgok, amiket az imént én is felsoroltam részletesen, amilyen problémák előfordulhatnak egy ilyen select-tel. A willValidate beállítás pedig azt jelzi nekünk, hogy ellenőrzésre fog-e kerülni a vizsgált (kiíratott) űrlapelem.

A további gyakorláshoz térjünk vissza az utasok létrehozó űrlapjához, mert ott az előző bejegyzésben sok szabályt állítottunk be, amelyeket most a Javascript és a Constraint Validation API segítségével is ellenőrizni tudunk majd. Bővítsük a passengers / create.blade.php -t az @endsection után:

@push('scripts')
    <script>
        // -------------- utasneve --------------
        const utasneve = document.getElementById("utasneve");

        utasneve.addEventListener("input", function(event) {
            if (utasneve.validity.tooLong || utasneve.validity.tooShort) {
                utasneve.setCustomValidity("A névnek legalább 10 és legfeljebb 50 karakteresnek kell lennie!");
                utasneve.reportValidity();
            } else {
                utasneve.setCustomValidity("");
            }
        });

        // -------------- age --------------
        const age = document.getElementById("age");

        age.addEventListener("input", function(event) {
            if (age.validity.rangeOverflow || age.validity.rangeUnderflow) {
                age.setCustomValidity("Legalább 6 és legfeljebb 99 éves lehet az utas!");
                age.reportValidity();
            } else {
                age.setCustomValidity("");
            }
        });

        // -------------- email --------------
        const email = document.getElementById("email");

        email.addEventListener("input", function(event) {
            if (email.validity.typeMismatch) {
                email.setCustomValidity("Ide egy helyes e-mail címet várok!");
                email.reportValidity();
            } else {
                email.setCustomValidity("");
            }
        });

        // -------------- phone --------------
        const phone = document.getElementById("phone");

        phone.addEventListener("input", function(event) {
            if (phone.validity.patternMismatch) {
                phone.setCustomValidity("Kövesse a mintát!");
                phone.reportValidity();
            } else {
                phone.setCustomValidity("");
            }
        });

        // -------------- repulojarata --------------
        const repulojarata = document.getElementById("repulojarata");

        repulojarata.addEventListener("input", function(event) {
            if (repulojarata.validity.valueMissing) {
                repulojarata.setCustomValidity("Válasszon ki egyet!");
                repulojarata.reportValidity();
            } else {
                repulojarata.setCustomValidity("");
            }
        });
    </script>
@endpush

Magyarázat: Ahogy az látható is a kódsorozatban, mindenféle ellenőrzést végrehajtunk: maxlength és minlength értékeket a tooLong és tooShort párossal, max és min értékeket a rangerOverflow és rangerUnderflow párossal, email típusú bemeneti mezőt typeMismatch -csel, a telefonszám mezőnél a megadott mintát (patternMismatch -csel) ellenőrizzük, míg végül a repülőjáratnál a hiányzó érték (valueMissing) esetén írjuk ki, hogy ki kell választani egyet. Teszteljük is le!

Minden billentyűlenyomásnál megjelenik a Javascript-tel ellenőrzött mezőnél a saját szövegezésű hibaüzenetünk, amíg nem lesz érvényes a bemeneti mező.


Támadás a Javascript-es validáció ellen

Hogyan lehetne ezt megtámadni? Hát ez egy jó kérdés, de induljunk ki abból, hogy a támadó hogyan tudja megnézni a Javascript kódunkat, ami ellenőrzi a bemeneti mezőinket. Firefox-ban a Debugger fülön tudja megnézni a kódot, így:

Innentől kezdve ő már tudja, hogy a getElementById-val kérjük le a mezőt, tehát meg tudja támadni a kódunkat, például így:

  1. Átírja a "megtámadni kívánt" mező id attribútumát, ahogy az előző bejegyzésben már láttuk, és onnantól kezdve már nem vonatkozik rá a javascript-es ellenőrzés
  2. Mondjuk elküldi (elmenti) úgy az űrlapot, hogy megad mondjuk egy 200 éves kort magának, amit mi ugye nem szerettünk volna, hogy előfordulhasson...

Ez persze csak egy "butácska példa", de annyi látható belőle, hogy ha már ismeri a kódunkat, ami kliens oldalon van, tehát a támadó böngészőjében, akkor onnantól ő már, ha elég dörzsölt, akkor ki tudja cselezni a mi kliens oldali validációs szabályainkat...


Validáció-specifikus Javascript keretrendszerek, osztálykönyvtárak

Ahogy a CSS kapcsán megismerhettük, hogy lehet, sőt érdemes keretrendszereket használni, úgy mint a Bootstrap vagy a Tailwind rendszereket, itt is erről van szó. Javascript-es osztálykönyvtárakkal és keretrendszerekkel igazából Dunát lehetne rekeszteni, kicsit talán nehéz is eligazodni köztük. Én most kigyűjtöttem néhányat, amik űrlapok ellenőrzésére szolgálnak leginkább. Abban az ügyben, hogy melyik a jobb / rosszabb, én biztosan nem foglalnék állást, érdemes kipróbálni 1-2-t közülük és ami jobban "kézre áll", azt lehet utána alkalmazni. Általában elmondható, hogy az alap funkcionalitásokra, ellenőrzésekre mindegyik alkalmas, ha pedig valamelyik fejlesztői kitalálnak valami egyedit, jót, akkor azt utána gyorsan lemásolják a többi fejlesztői is. Itt egy lista, direkt nem számozott sorrendben, mert inkább utána sorolok fel szempontokat, hogy mi szerint érdemes választani ezek vagy éppen más hasonlók közül:

Néhány szempont, ami alapján érdemes választani közülük:

  1. Mivel egy nagy alkalmazásnál biztosan sok JS csomagot használunk, érdemes figyelni a validációs csomag méretére (minél kisebb legyen).
  2. Mikor frissítették utoljára: ha már régebben érkezett hozzá frissítés, akkor nem biztos, hogy érdemes ezt használni, mert esetleg a fejlesztői már nem gondozzák tovább, ami problémás működéseket szülhet.
  3. Nézzünk meg példákat, próbáljuk ki a "szimpatikusakat", hogy mennyire áll nekünk kézre az alkalmazásuk, mert hogy ha nem logikus számunkra, vagy nehezen használható, akkor annak az alkalmazása a későbbiekben csak nyűggel jár majd.
  4. Van-e hozzá weboldal: dokumentáció, mintapéldák stb.
  5. Mennyi és milyen további függőségeket tartalmaz még. Nyilván minél kevesebb egyéb függőséget tartalmaz, annál inkább jobb lehet nekünk (kisebb eséllyel lesz problémás a használata egyéb függő csomagok miatt).

A következő bejegyzésben ezek közül kiválasztok egyet-kettőt és megmutatom, hogy hogyan is kell használni őket a gyakorlatban.

Miután végrehajtottuk a Javascript-es teszteléseinket, érdemes visszatenni a kódjainkba a HTML5-ös attribútumokat, amelyek az ellenőrzést segítették, így újra - bár csak kliens oldalon vagyunk továbbra is -, de kétlépcsős lesz az űrlapunk ellenőrzése.

Ennek a bejegyzésnek a Github commit-je itt érhető el. Tipp: mivel a kódokat formáztam is a VSCode-ban az ALT + SHIFT + f billentyűparanccsal, ezért a kódokban az egyes tag-ek attribútumait új sorba kezdte, ez egy formázási beállítás miatt van, de amikor sok attribútuma van már egy tag-nek, akkor így jobban át is lehet látni a dolgokat.

Most megcsináltuk az airlines és a passengers mappában lévő két create nézeteket. Gyakorlásként a hozzájuk kapcsolódó két edit nézetet is érdemes ugyanígy megvalósítani.