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 kolačić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)) {
    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

Zamislimo pojednostavljenu slastičarnu: kod svake narudžbe kolača od strane korisnika, isti će biti odabran slučajno kako ne bismo morali implementirati odabir kolača. Pritom želimo pamtiti koji kolač je korisnik ranije “odabrao” kako bismo ga jednom mogli upitati kako mu se svidio taj kolač i vremenom uvesti ocjenjivanje kolača od strane korisnika.

Želimo da web poslužitelj na putanju /kolaci prima zahtjeve metodom GET i metodom POST. Zahtjev metodom GET daje korisniku informacije o tome koji je kolač naručio u prethodnom koraku. Zahtjev metodom POST naručuje novi kolač.

Odaberimo par kolača s Wikipedijinog popisa i ponudimo ih. Gruba struktura programa je:

<?php

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis prethodno naručenog kolača
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

Unos podataka i postavljanje kolačića

Pohrana podataka o korisnicima

Podsjetimo se da je HTTP protokol koji ne održava stanje (engl. stateless) pa se svaki zahtjev obrađuje neovisno o prethodnima.

Kako bismo pohranili kolače koje su korisnici naručili, svakako će nam trebati datoteka u koju ćemo pohraniti serijalizirane podatke pohraniti funkcijom file_put_contents(). Ponovno ćemo iskoristiti serijalizaciju u oblik JSON funkcijom json_encode() te 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(). Datoteku nazovimo orders.json pa imamo kod oblika:

<?php

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

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis prethodno naručenog kolača
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

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

Prikaz prethodno naručenog kolača

Kako bismo korisniku prezentirali informaciju o tome koji je kolač ranije naručio, iskoristit ćemo podatak user_id iz kolačića koji on pošalje za dohvaćanje identifikatora korisnika u polju $orders. Ako korisnik nije poslao kolačić, ili je poslao kolačić koji nema podatak user_id, ili je poslao kolačić čiji user_id ne postoji u polju, poslužitelj će mu vratiti odgovor da dosad nije naručio nijedan kolač sa statusnim kodom 404 Not Found.

<?php

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

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $prethodni_kolac = $orders[$user_id];
            echo "<p>Ranije ste naručili " . $prethodni_kolac . ".</p>\n";
        } else {
            http_response_code(404);
            echo "<p>Dosad niste naručili nijedan kolač.</p>\n";
        }
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

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

Naručivanje kolača slučajnim odabirom

Kod naručivanja, odabir kolača ćemo izvesti slučajno među ponuđenima funkcijom array_rand() (dokumentacija).

Ako je korisnik poslao kolačić u kojemu je sadržan podatak user_id i taj identifikator postoji u polju u polju $orders, zamijenit ćemo zapis o prethodno naručenom kolaču novim (to činimo ovdje radi jednostavnosti; u praksi web aplikacije uglavnom dopunjavaju podatke, a vrlo rijetko brišu išta). Slično kao kod dohvaćanja podataka, ako korisnik nije poslao kolačić, ili je poslao kolačić koji nema podatak user_id, ili je poslao kolačić čiji user_id ne postoji u polju, poslužitelj će u polju $orders stvoriti podatke pod novim indeksom pa taj indeks taj indeks poslati korisniku u kolačiću pod user_id.

<?php

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

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $prethodni_kolac = $orders[$user_id];
            echo "<p>Ranije ste naručili " . $prethodni_kolac . ".</p>\n";
        } else {
            http_response_code(404);
            echo "<p>Dosad niste naručili nijedan kolač.</p>\n";
        }
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $kolac = array_rand($kolaci);
        echo "<p>Naručili ste " . $kolac ".</p>\n";
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $orders[$user_id] = $kolac;
        } else {
            $orders[] = $kolac;
            $user_id = array_key_last($orders);
            setcookie("user_id", $user_id);
        }
    }
}

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

Isprobajmo navedeni kod TODO

$ 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 orders.json koja sadrži listu:

["Bugness lyonaess"]

Warning

Iz sigurnosne perspektive, korištenje kratkih, predvidljivih i nepromjenjivih identifikatora korisnika u kolačićima kao što su redom brojevi 0, 1, 2, … je loš pristup jer otvara puno prostora za napad. U praksi bi postavljanje kolačića bilo nešto složenije, ali ovakav pristup je procesu učenja sasvim dovoljan za ilustraciju načina postavljanja i dohvaćanja 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