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.