Section author: Vedran Miletić

Baratanje HTTP kolačićima u jeziku PHP

HTTP kolačić (engl. HTTP cookie, Wikipedia, više detalja o kolačićima na MDN-u) je maleni dio podataka koji korisnički agent pohranjuje na računalu korisnika kod pregledavanja web sjedišta. Na temelju pohranjenih podataka web sjedište pamti stanje koje je korisnik stvorio svojim pregledavanjem, npr. predmete dodane u košaricu za kupnju, prijavu na zatvoreni dio sjedišta upotrebom određenog korisničkog računa ili tekst prethodnih pretraga arhive audiovizualnih datoteka.

Način rada kolačića

HTTP zaglavlje Set-Cookie (više detalja o HTTP zaglavlju Set-Cookie na MDN-u) je dio odgovora na zahtjev i koristi se za postavljanje kolačića koji se pohranjuju na klijentskoj strani (npr. u tekstualnu datoteku). Kod slanja idućeg zahtjeva korisnički agent šalje pohranjene kolačiće (npr. učitava ih iz tekstualne datoteke) u HTTP zaglavlju Cookie (više detalja o HTTP zaglavlju Cookie na MDN-u).

Postavljanje kolačića

Interpreter PHP-a podržava postavljanje kolačića funkcijom setcookie() (dokumentacija) na način:

<?php

setcookie("kolacic", "Bugnes lyonnaises");

Kod korištenja cURL-a primljeni kolačići u odgovoru se pohranjuju u staklenku (engl. cookie jar) korištenjem parametra --cookie-jar, odnosno -c na način:

$ curl -v -c cookies.txt http://localhost:8000/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 15:04:22 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
* Added cookie kolacic="Bugnes%20lyonnaises" for domain localhost, path /, expire 0
< Set-Cookie: kolacic=Bugnes%20lyonnaises
< Content-type: text/html; charset=UTF-8
<
* Closing connection 0

Uočimo da u odgovoru postoji zaglavlje Set-Cookie koje postavlja kolačić pa sadrži njegov naziv i vrijednost. Ispišimo sadržaj stvorene staklenke cookies.txt:

$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       kolacic Bugnes%20lyonnaises

Primanje kolačića

Polje $_COOKIE (dokumentacija) sadrži sve keksiće primljene od strane klijenta. U tom polju ključevi su nazivi kolačića, a vrijednosti upravo njihove vrijednosti. Za ilustraciju, provjerimo funkcijom array_key_exists() (dokumentacija) postoji li u tom polju kolačić pod nazivom kolacic, a zatim, ako postoji, dohvatimo njegovu vrijednost i ispišimo je:

<?php

if (array_key_exists("kolacic", $_COOKIE["kolacic"]) {
    echo $_COOKIE["kolacic"];
}

U cURL-u se staklenka kolačića šalje u zahtjevu parametrom --cookie, odnosno -b. Iskoristimo ga na način:

$ curl -v -b cookies.txt http://localhost:8000/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Cookie: kolacic=Bugnes%20lyonnaises
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 15:09:00 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-type: text/html; charset=UTF-8
<
Bugnes lyonnaises
* Closing connection 0

Uočimo u zahtjevu dodatno zaglavlje Cookie koje sadrži naziv i vrijednost poslanog kolačića.

Primjena kolačića

Želimo da web poslužitelj na putanju /profil-studenta prima zahtjeve metodom GET i metodom POST. Zahtjev metodom GET pozdravlja studenta imenom i prezimenom ako su podaci prethodno uneseni ili poruku da se radi o studentu koji još nije unio svoje podatke. Zahtjev metodom POST omogućuje unos podataka (ime, prezime, studij i JMBAG) te javlja grešku ako neki od podataka nije unesen.

Gruba struktura programa je:

<?php

if ($_SERVER["REQUEST_URI"] == "/profil-studenta") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis
    }
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // unos
    }
}

Unos podataka i postavljanje kolačića

Napravimo prvo unos podataka na način koji smo vidjeli ranije, a zatim dodajmo unesene podatke u studentu u polje $studenti na prvi sljedeći ključ. U našem slučaju polje je prazno pa će prvi dodijeljeni ključ biti 0.

<?php

$studenti = [];

if ($_SERVER["REQUEST_URI"] == "/profil-studenta") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis
    }
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $ime = $_POST["ime"];
        $prezime = $_POST["prezime"];
        $studij = $_POST["studij"];
        $jmbag = $_POST["jmbag"];
        if (isset($ime) && isset($prezime) && isset($studij) && isset($jmbag)) {
            $student = [
                "ime" => $ime,
                "prezime" => $prezime,
                "studij" => $studij,
                "jmbag" => $jmbag
            ];
            $studenti[] = $student;

            echo "<p>Uspješno pohranjen unos: $ime $prezime ($jmbag), $studij.</p>\n";
        } else {
            http_response_code(400);
            echo "<p>Neispravan zahtjev.</p>\n";
        }
    }
}

Kako je HTTP protokol koji ne održava stanje (engl. stateless), svaki zahtjev se obrađuje kodom danim iznad neovisno o prethodnima. Čak nakon pohrane podataka iz varijable $student u polje $studenti, to polje i time svi podaci u njemu prestaju postojati nakon slanja odgovora na primljeni zahtjev. Dakle, kod novog zahtjeva ponovno se izvršava isti kod ispočetka u kojem se inicijalizira prazno polje $studenti i prethodno pohranjene podatke ne možemo dohvatiti.

Zbog toga moramo podatke serijalizirane u nekom obliku trajno pohraniti u datoteku funkcijom file_put_contents(). Ponovno ćemo iskoristiti serijalizaciju u oblik JSON funkcijom json_encode() te po potrebi deserializaciju iz JSON-a u podatke funkcijom json_decode(). Kod pokretanja ćemo funkcijom file_exists() (dokumentacija) provjeriti ako postoji datoteka sa spremljenim podacima od ranije te ih učitati funkcijom file_get_contents(). Kod je oblika:

<?php

$datoteka = "studenti.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $studenti = json_decode($j, true);
} else {
    $studenti = [];
}

if ($_SERVER["REQUEST_URI"] == "/profil-studenta") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis
    }
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $ime = $_POST["ime"];
        $prezime = $_POST["prezime"];
        $studij = $_POST["studij"];
        $jmbag = $_POST["jmbag"];
        if (isset($ime) && isset($prezime) && isset($studij) && isset($jmbag)) {
            $student = [
                "ime" => $ime,
                "prezime" => $prezime,
                "studij" => $studij,
                "jmbag" => $jmbag
            ];
            $studenti[] = $student;

            $j = json_encode($studenti);
            file_put_contents($datoteka, $j);

            echo "<p>Uspješan unos: $ime $prezime ($jmbag), $studij.</p>\n";
        } else {
            http_response_code(400);
            echo "<p>Neispravan zahtjev.</p>\n";
        }
    }
}

Ovaj kod možemo isprobati zahtjevom:

$ curl -v -X POST -d ime=Ivan -d prezime=Horvat -d studij=informatika -d jmbag=0123456789 http://localhost:8000/profil-studenta
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /profil-studenta HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Content-Length: 50
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 50 out of 50 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 16:47:54 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-type: text/html; charset=UTF-8
<
<p>Uspješan unos: Ivan Horvat (0123456789), informatika.</p>
* Closing connection 0

Možemo se uvjeriti da je stvorena datoteka studenti.json sadržaja:

[{"ime":"Ivan","prezime":"Horvat","studij":"informatika","jmbag":"0123456789"}]

Warning

Iz sigurnosne perspektive, spremanje podataka u datoteku koja se nalazi na mjestu s kojeg je klijenti mogu dohvatiti HTTP zahtjevima (u ovom slučaju naredbom curl http://localhost:8000/studenti.json) je katastrofalno loša praksa. Ovdje datoteku pohranjujemo na tom mjestu samo radi jednostavnosti.

Ostaje nam još pohraniti kolačić. Pohranit ćemo u njemu redni broj pod kojim se u polju $studenti nalazi traženi unos. Kako ti brojevi idu redom počevši od 0, možemo lako vidjeti da će ključ elementa koji ćemo spremiti upravo biti jednak broju elemenata u polju prije njega pa taj broj možemo dohvatiti funkcijom count() (dokumentacija) na način:

<?php

$datoteka = "studenti.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $studenti = json_decode($j, true);
} else {
    $studenti = [];
}

if ($_SERVER["REQUEST_URI"] == "/profil-studenta") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis
    }
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $ime = $_POST["ime"];
        $prezime = $_POST["prezime"];
        $studij = $_POST["studij"];
        $jmbag = $_POST["jmbag"];
        if (isset($ime) && isset($prezime) && isset($studij) && isset($jmbag)) {
            setcookie("student", count($studenti));

            $student = [
                "ime" => $ime,
                "prezime" => $prezime,
                "studij" => $studij,
                "jmbag" => $jmbag
            ];
            $studenti[] = $student;

            $j = json_encode($studenti);
            file_put_contents($datoteka, $j);

            echo "<p>Uspješan unos: $ime $prezime ($jmbag), $studij.</p>\n";
        } else {
            http_response_code(400);
            echo "<p>Neispravan zahtjev.</p>\n";
        }
    }
}

Warning

Iz sigurnosne perspektive, korištenje kratkih i nepromjenjivih identifikatora korisnika u kolačićima je loš pristup te bi u praksi postavljanje kolačića bilo nešto složenije, ali ovakav način rada je procesu učenja sasvim dovoljan za ilustraciju načina primjene kolačića.

Isprobajmo radi li spremanje kolačića:

$ curl -v -c keksici-profil-studenta.txt -X POST -d ime=Marko -d prezime=Horvat -d studij=matematika -d jmbag=9876543210 http://localhost:8000/profil-studenta
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /profil-studenta HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Content-Length: 50
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 50 out of 50 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 23:11:25 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
* Added cookie student="1" for domain localhost, path /, expire 0
< Set-Cookie: student=1
< Content-type: text/html; charset=UTF-8
<
<p>Uspješan unos: Marko Horvat (9876543210), matematika.</p>
* Closing connection 0

$ cat keksici-profil-studenta.txt
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       student 1

Lako se uvjerimo da je i ovaj podatak spremljen u datoteku studenti.json:

[{"ime":"Ivan","prezime":"Horvat","studij":"informatika","jmbag":"0123456789"},{"ime":"Marko","prezime":"Horvat","studij":"matematika","jmbag":"9876543210"}]

Prikaz podataka i primanje kolačića

Kod prikaza podataka prvo provjeravamo ako je primljen kolačić pod nazivom student, a zatim na temelju vrijednosti kolačića dohvaćamo broj studenta u popisu i pozdravljamo studenta. Ako kolačić nije primljen, javljamo da podaci nisu uneseni:

<?php

$datoteka = "studenti.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $studenti = json_decode($j, true);
} else {
    $studenti = [];
}

if ($_SERVER["REQUEST_URI"] == "/profil-studenta") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        if (array_key_exists("student", $_COOKIE)) {
            $broj = $_COOKIE["student"];
            $ime = $studenti[$broj]["ime"];
            $prezime = $studenti[$broj]["prezime"];
            $studij = $studenti[$broj]["studij"];
            $jmbag = $studenti[$broj]["jmbag"];
            echo "<p>Pozdrav, $ime $prezime. Vaš JMBAG je $jmbag. Kako ide studij $studij?</p>\n";
        } else {
            echo "<p>Podaci nisu uneseni.</p>\n";
        }
    }
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $ime = $_POST["ime"];
        $prezime = $_POST["prezime"];
        $studij = $_POST["studij"];
        $jmbag = $_POST["jmbag"];
        if (isset($ime) && isset($prezime) && isset($studij) && isset($jmbag)) {
            setcookie("student", count($studenti));

            $student = [
                "ime" => $ime,
                "prezime" => $prezime,
                "studij" => $studij,
                "jmbag" => $jmbag
            ];
            $studenti[] = $student;

            $j = json_encode($studenti);
            file_put_contents($datoteka, $j);

            echo "<p>Uspješan unos: $ime $prezime ($jmbag), $studij.</p>\n";
        } else {
            http_response_code(400);
            echo "<p>Neispravan zahtjev.</p>\n";
        }
    }
}

Isprobajmo radi li nam kod ispravno kad pošaljemo ranije spremljenu staklenku s kolačićima:

$ curl -v -b keksici-profil-studenta.txt http://localhost:8000/profil-studenta
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /profil-studenta HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Cookie: student=1
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 23:48:02 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-type: text/html; charset=UTF-8
<
<p>Pozdrav, Marko Horvat. Vaš JMBAG je 9876543210. Kako ide studij matematika?</p>
* Closing connection 0

Isprobajmo varijantu i bez slanja kolačića:

$ curl -v http://localhost:8000/profil-studenta
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /profil-studenta HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 23:48:16 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-type: text/html; charset=UTF-8
<
<p>Podaci nisu uneseni.</p>
* Closing connection 0