JS a cookies

W tej notce chciałem nieco napisać o obsłudze cookies w JavaScript. Zanim jednak zajmiemy się JavaScriptem, opiszemy jak sprawa wygląda w PHP.

PHP a ciasteczka

Wszystkie cookies wysłane przez przeglądarkę są w PHP udostępniane w tablicy $_COOKIE. Jeśli chcemy odczytać wartość cookie o nazwie cookie1 wystarczy kod:

echo $_COOKIE['cookie1'];

Czyli prosto i wygodnie.

Ciasteczka zapisujemy za pomocą funkcji setcookie:

int setcookie(string nazwa [, string wartość [, int data_ważności [, string ścieżka [, string domena [, int bezpieczne]]]]])

(wg. pl.php.net/setcookie)

JS a ciasteczka (odczyt)

Jak pisałem wyżej, PHP oferuje wygodny dostęp do ciasteczek zapisanych dla danej witryny. Niestety tego samego nie możemy powiedzieć o JavaScriptcie. W JS wszystkie informacje o ciasteczkach udostępniane są we własności document.cookie. Próbując odczytać tę właściwość uzyskamy poniższy wynik (dla 3 ciasteczek):

cookie1=wartosc1; cookie2=wartosc2; cookie3=wartosc3

Czyli krótko mówiąc wszystkie ciasteczka mamy w jednym Stringu. Jestem bardzo ciekawy, dlaczego zdecydowano się właśnie na taki format zapisu? Teraz chcąc odczytać wartość dowolnego ciasteczka trzeba odpowiednio manipulować tymi danymi. Analizując ten String zauważymy, że każde ciasteczko oddzielone jest od poprzedniego znakiem ;, a pomiędzy nazwą a wartością ciasteczka znajduje się znak =.

Zanim napiszemy odpowiednią funkcję, rozpatrzymy jeden z przypadków specjalnych - zapiszemy ciasteczko bez wartości.
Przeglądarki oparte o silnik Gecko oraz Opera zwrócą String postaci

cookie1=wartosc1; cookie2=wartosc2; cookie3=wartosc3; cookie4=

zaś Internet Explorer zwróci

cookie1=wartosc1; cookie2=wartosc2; cookie3=wartosc3; cookie4

Jak widać, znak równości nie zawsze może być dla nas punktem odniesienia, ponieważ przy ciasteczkach bez wartości IE nie dodaje tego znaku.

Odpowiednio wykorzystując wyżej podane informacje dojdziemy do oto takiej funkcji:

function getCookie(N){
	if(N=(new RegExp(N+'=([^;]*)')).exec(document.cookie+';'))return N[1]
}
W powyższej funkcji znalazł się błąd. Poniżej prezentuję zaktualizowaną wersję
function getCookie(N){
	if(N=(new RegExp(';\\s*'+N+'=([^;]*)')).exec(';'+document.cookie+';'))return N[1]
}

Została jeszcze jedna kwestia do rozwiązania. Jeśli jako nazwę ciasteczka podany zostanie ciąg zawierający jeden z meta-znaków wykorzystywanych w wyrażeniach regularnych (np. kropka, nawias kwadratowy, nawias okrągły, itp.), wtedy funkcja może zwrócić inną wartość niż oczekiwaliśmy. Poprawka może wyglądać w sposób, jaki prezentuję poniżej.

function getCookie(N){
	if(N=(new RegExp(';\\s*'+(''+N).replace(/([()[\]{}\-.*+?^$|\/\\])/g,'\\$1')+'=([^;]*)')).exec(';'+document.cookie+';'))return N[1]
}

Funkcja oczekuje jednego argumentu - nazwy ciasteczka. Zwróci jego wartość, gdy ciasteczko istnieje, lub zwróci undefined, gdy ciasteczko o podanej nazwie nie istnieje.

W swoich kombinacjach postanowiłem zrobić coś jeszcze - dlaczego nie zdefiniować czegoś na wzór tablicy $_COOKIE z PHP? Nie zagłębiając się w szczegóły, podaję mój kod:

$_COOKIE={};eval((document.cookie+';').replace(/([\w%]+)=?([^;]*);/g,"$$_COOKIE['$1']='$2';"));

JS a ciasteczka (zapis)

Niewiele dobrego można powiedzieć o odczycie cookies w JS. Niestety ich zapis też nie wygląda za ciekawie. Główną niedogodnością jest konieczność zapisu daty wygaśnięcia (ang. expire date) w postaci GMT-Stringu, czyli w mniej więcej takiej:

dlatego napisanie takiej funkcji jest po prostu koniecznością

function setCookie(name, value, expires, path, domain, secure){
	document.cookie=name+'='+escape(value||'')+
		(expires?';expires='+new Date(+new Date()+expires*864e5).toGMTString():'')+
		(path?';path='+path:'')+
		(domain?';domain='+domain:'')+
		(secure?';secure':'');
}

Odczyt jeszcze raz

Wyżej napisany skrypt utworzy obiekt $_COOKIE przy starcie strony. Można spróbować pokusić się o automatyczną aktualizację w momencie zapisania nowych ciasteczek. Jednym z podejść może być modyfikacja funkcji zapisującej ciasteczko, żeby zapisała też odpowiednią wartość do obiektu.

function setCookie(name, value, expires, path, domain, secure){
	document.cookie=name+'='+escape(value||'')+
		(expires?';expires='+new Date(+new Date()+expires*864e5).toGMTString():'')+
		(path?';path='+path:'')+
		(domain?';domain='+domain:'')+
		(secure?';secure':'');
	if(window.$_COOKIE)$_COOKIE[name]=escape(value||'');
}

Powyższa metoda ma kilka podstawowych wad. Po pierwsze jeśli ciasteczko zostanie odrzucone przez przeglądarkę, w obiekcie $_COOKIE i tak znajdziemy zapis o nowym ciasteczku. Po drugie jesteśmy zmuszeni do korzystania z tej funkcji (co oczywiście nie musi być problemem).

Spróbujmy jednak pozbyć się pierwszej wady. Zmodyfikujemy funkcję

function setCookie(name, value, expires, path, domain, secure){
	document.cookie=name+'='+escape(value||'')+
		(expires?';expires='+new Date(+new Date()+expires*864e5).toGMTString():'')+
		(path?';path='+path:'')+
		(domain?';domain='+domain:'')+
		(secure?';secure':'');
	if(window.$_COOKIE&&navigator.cookieEnabled&&(N=(new RegExp(name+'=([^;]*)')).exec(document.cookie+';')))$_COOKIE[name]=N[1]
}

Po zapisie ciasteczka dokonujemy jego odczytu i zapisujemy zwróconą wartość do obiektu $_COOKIE. Warto jeszcze sprawdzić, czy przeglądarka akceptuje cookies. Niestety, nawet jeśli przeglądarka ma włączone akceptowania cookies, nie mamy gwarancji, że ciasteczko zostanie zapisane.

Spróbujmy jednak pozbyć się drugiego ograniczenia. Przeglądarki oparte o silnik Gecko udostępniają nam metodę watch, którą postaramy się tutaj wykorzystać.

Składnia tej metody prezentuje się następująco:

Object.watch(wlasnosc,monitor);
  • wlasnosc - Nazwa obserwowanej własności obiektu (u nas będzie to własność cookie obiektu document).
  • monitor - Funkcja, która ma zostać wywołana przy zmianie własności. W tym miejscu przekazujemy referencję do funkcji, która ma zostać wykonana w momencie próby zmiany wartości danej własności. Do funkcji zostaną przekazane 3 argumenty, tj. wlasnosc, stara_wartosc, nowa_wartosc. Obserwowana własność przyjmie wartość taką, jaką zwróci ta funkcja.
document.watch('cookie',function(prop,old_val,new_val,t){
	if(window.$_COOKIE&&navigator.cookieEnabled){
		if((t=/^([\w%]+)(?:\=)([^;]*);/.exec(new_val))&&t[1]&&t[2])$_COOKIE[t[1]]=t[2];
	}
	return new_val;
})

Jak wyżej wspomniałem, metoda watch istnieje AFAIK tylko w przeglądarkach z Gecko, dlatego dla pozostałych przeglądarek trzeba wykombinować coś innego. Jedyną metodą, jaką udało mi się wymyślić jest okresowe sprawdzanie własności document.cookie.

setInterval(function(t,i){
	t={};eval((document.cookie+';').replace(/([\w%]+)=?([^;]*);/g,"t['$1']='$2';"));
	for(i in t)if($_COOKIE[i]!=t[i])$_COOKIE[i]=t[i];
},100);

Wadą tego rozwiązania jest, że wartości w obiekcie $_COOKIE będą się pojawiać z pewnym opóźnieniem, co może być problematyczne, dlatego IMO metoda ta jest nie do zaakceptowania.

Przeglądarki Gecko oferują jeszcze jedną ciekawą funkcjonalność. Umożliwiają one stosowanie setterów i getterów dla własności obiektów. Postaramy się to także wykorzystać do obsługi cookies.

document.__defineGetter__('cookies',function(t){
	t={},eval((document.cookie+';').replace(/([\w%]+)=?([^;]*);/g,"t['$1']='$2';"));
	return t
})

W tym wypadku definiujemy własność document.cookies, która przechowuje obiekt (patrz JSON) z wszystkimi ciasteczkami. Dla przykładu cookie o nazwie test odczytujemy.

document.cookies.test

lub

document.cookies['test']

Własność document.cookie jest analizowana za każdym razem, gdy dobieramy się do własności document.cookies, dlatego możemy mieć pewność, że zawsze mamy aktualne dane na temat ciasteczek.

Niestety settery i gettery na razie występuję na listach życzeń większości przeglądarek.

Zapis osobiście preferowałbym za pomocą zwykłej funkcji setCookie, ale jeśli ktoś uważa inaczej, to prezentuję metodę z setterami.

document.__defineSetter__('cookies',function(o){
	if(o&&o.name)document.cookie=o.name+'='+escape(o.value||'')+
		(o.expires?';expires='+new Date(+new Date()+o.expires*864e5).toGMTString():'')+
		(o.path?';path='+o.path:'')+
		(o.domain?';domain='+o.domain:'')+
		(o.secure?';secure':'');
}

Przykład:

document.cookies={
	name:'test',
	value:'wartosc',
	expires:31,
	path:'/',
	domain:'domena.pl',
	secure:true
}

Tylko name jest oczekiwany jako wymagana własność.

Parę uwag na koniec

Na koniec postanowiłem omówić jeszcze kilka aspektów związanych w ciasteczkami i JavaScriptem. Wszystkie skrypty do obsługi cookies w JS będą niepoprawne jeśli zapiszemy ciasteczka o treści dla przykładu

cookie1=wart;o=sc1
cookie2=wart=os;c2

Proszę zauważyć wyróżnione znaki. Otóż jak się okazuje, musimy sami zadbać o zakodowanie zapisywanych w ciasteczku danych, ponieważ przeglądarka tego za nas nie zrobi, a format ciasteczek w JavaScripcie uniemożliwia poprawny odczyt wybranych cookies w przypadku niezakodowanych wartości.


Kategorie: JavaScript 26 lutego 2007, 19:54:00 6 komentarzy

Komentarze dla notki “JS a cookies”

  1. Kate - 27 lutego 2007, 21:36:05

    No, no… całkiem pokaźna ta notka.

  2. Rafael - 27 lutego 2007, 23:01:36

    Tak jakoś wyszło ;-)
    Jak złapię wenę to nie wiem kiedy skończyć pisać ;-P

  3. Riddle - 27 lutego 2007, 23:43:23

    Bardzo dobry wpis, dzięki.
    Przy projektach, do których dodaję jQuery ciasteczkami zajmuje się odpowiedni plugin, ale warto wiedzieć co siedzi pod maską. :)

  4. blue - 19 sierpnia 2007, 21:01:24

    Dopatrzyłem się małego błędu – w funkcji getCookie pobierasz wartość dowolnego ciastka, którego nazwa kończy się ciągiem znaków podanym w parametrze:
    function getCookie(N){ if(N=(new RegExp(N+’=([^;]*)’)).exec(document.cookie+’;’))return N1
    }

    document.cookie = „tekst=a”;
    getCookie(‘t’); // zwróci „a”;

  5. Rafael - 22 lutego 2008, 14:49:55

    @blue: Dzięki za zwrócenie uwagi na ten błąd. Nie zauważyłem tego.

    PS. Lepiej późno niż wcale, ale jakoś Twój komentarz umknął mojemu wzrokowi.

    Poprawką na szybko może być:

    function getCookie(N){
    if(N=(new RegExp('; ?'+N+'=([^;]*)')).exec(';'+document.cookie+';'))return N[1]
    }
    
  6. Rafael - 23 lutego 2008, 15:55:31

    Powyższy kod nie jest ostateczny. Zmieniłem część ‘ ?’ na ‘\\s*’. Poza tym trzeba jeszcze wstawić znak ucieczki przed wszystkimi meta-znakami, które mogą się pojawić w zmiennej N. Nowy kod wstawię do notki w najbliższych dniach.

Pozostaw komentarz

Copyright © 2003-2011 Rafał Kukawski. Powered by Jogger | RSS Subskrybuj