Ciasteczka w JavaScript, cz. 2

Prawie dwa lata temu rozpisywałem się nieco na temat ciasteczek w JavaScript w artykule „JS a cookies”. Od tego czasu napisałem sporo skryptów i eksperymentowałem na wiele różnych sposobów. Eksperymenty dotyczyły również ciasteczek. W tym artykule postanowiłem podzielić się wynikami tych doświadczeń.

Odczyt ciasteczek raz jeszcze

W ostatnim artykule, po poprawkach zauważonych błędów, skończyliśmy na funkcji odczytu ciasteczek w postaci:

function getCookie(N){
   if(N=(new RegExp(';\\s*'+Escape(N)+'=([^;]*)')).exec(';'+document.cookie+';'))
      return N[1]
}

gdzie Escape jest funkcją, która powoduje, że pewne znaki specjalne są traktowanie dosłownie a nie jako meta-znak wyrażenia regularnego.

function Escape(N){return N.replace(/([()[\]{}\-.*+?^$|\/\\])/g,'\\$1')}

Nie spodobała mi się ostateczna wersja funkcji, ponieważ przez przymus korzystania z funkcji Escape wykonujemy zbędne operacje, nie związane z ciasteczkami.

Postanowiłem pokombinować i napisać na nowo funkcję odczytu cookies.

Bazując na założeniu, że nazwa każdego ciasteczka poprzedzona jest średnikiem i jedną spacją, a po wartości ciasteczka znajdziemy średnik, napisałem funkcję, która wykorzystuje tylko metody obiektu String.

function getCookie(N, t, s){
   s='; '+document.cookie+';'
   if((t=s.indexOf('; '+N+'='))>-1)
      return s.slice(t+=3+N.length,s.indexOf(';',t))
}

Jeśli ktoś chciałby uciąć jeszcze kilka znaków z kodu może pokusić się o zapis z instrukcją with, tylko ostrzegam, że wydajność funkcji spadnie.

function getCookie(N, t){
   with('; '+document.cookie+';')
      if((t=indexOf('; '+N+'='))>-1)
         return slice(t+=3+N.length,indexOf(';',t))
}

Komentarz do funkcji?

Ogólnie rzecz ujmując, nowa funkcja cieszy o tyle, że wykorzystuje tylko podstawowe metody obiektu String. Jeśli chodzi o szybkość działania w porównaniu z pierwszą funkcją, to oczywiście nowa funkcja jest szybsza. Z krótkich testów, które przeprowadziłem pod silnikiem Gecko, nowa funkcja okazała się 1,9 razy szybsza niż wersja wykorzystująca wyrażenia regularne. Ale jak to testy, jest to rzecz subiektywna, dlatego każdy może testować kod własnymi sposobami.

Wracając do pierwszej funkcji, można zauważyć, że wspomniane escape’owanie meta-znaków kosztuje sporo czasu, pominięcie tej operacji, chociaż niesie pewne „ryzyko”, poprawia wydajność funkcji.

Kombinowania ciąg dalszy

W ostatniej publikacji przedstawiłem też sposób implementacji obiektu document.cookies, który znacznie upraszczał dostęp do poszczególnych ciasteczek. Niestety, działał tylko w przeglądarkach z obsługą Getterów w JS. Obsługą Getterów może się pochwalić Firefox, Opera, Safari oraz Chrome (który w zasadzie bazuje na tym samym silniku co Safari). Kulą u nogi znowu jest Internet Explorer. Przeglądając dokumentację JScriptu na stronach Microsoftu natrafiłem na zdarzenie propertychange. Zdarzenie to wywoływane jest w momencie, gdy zmieni się dowolna właściwość obserwowanego obiektu. Dlaczego więc nie zatrudnić tego zdarzenia do obserwowania własności cookie obiektu document?

Bez zbędnego rozpisywania się przechodzimy do implementacji.

;(function(){
   function parse(ret){
      var t={};
      eval((document.cookie+';').replace(/([\w%]+)=?([^;]*);/g,"t['$1']='$2';"));
      if(ret)return t; document.cookies=t;
   }
   if(document.__defineGetter__)
      document.__defineGetter__('cookies',function(){return parse(true)});
   else if(/*@cc_on @if (@_jscript)!@end @*/false){
      parse();
      document.attachEvent('onpropertychange',function(e){
         if((e=window.event)&&e.propertyName=='cookie')
            parse()
      });
   } else
      throw new Error("This script doesn't support your browser! Sorry.");
})();

Krótko o powyższym kodzie.

Funkcja parse została utworzona na podstawie kodu z poprzedniego artykułu. Działa w ten sposób, że dla każdego ciasteczka tworzy nowy indeks w obiekcie t zgodny z nazwą ciasteczka i wartością identyczną z danym ciasteczkiem. W zależności, czy argument ret przyjmie wartość true lub false, funkcja zwraca obiekt t lub nadpisuje obiekt document.cookies tym obiektem. Dlaczego w ten sposób, dowiemy się z dalszego opisu.

Dalej sprawdzamy, czy przeglądarka obsługuje Gettery dla obiektu document. Jeśli tak, to zwracamy to, co zwróci funkcja parse.

Dalej wykorzystujemy kompilację warunkową. Chcemy być pewni, że przeglądarka obsługuje JScript. Jeśli tak właśnie będzie, warunek przyjmie postać !false i zostaną wykonane instrukcje dla tego warunku. W tym momencie tworzymy obiekt document.cookies z wartością zwróconą przez funkcję parse i dodajemy nasłuchiwanie na zdarzenie propertychange. W funkcji obsługującej zdarzenie sprawdzamy, czy zmiana dotyczy własności cookie. Jeśli tak, to nadpisujemy obiekt document.cookies nowymi ciasteczkami.

Reszta przeglądarek dostanie wyjątek, że nie są obsługiwane. Można całą klauzulę else wyrzucić, bo przydatna będzie tylko programiście do testów.

Podsumowanie

Dzięki powyższemu skryptowi uzyskujemy wygodny odczyt ciasteczek w przeglądarkach Firefox, Opera, Safari, Chrome i Internet Explorer i innych opartych o silniki Gecko, Webkit i Trident, a to już ponad 99% rynku przeglądarek.


Kategorie: JavaScript 28 grudnia 2008, 14:34:23 13 komentarzy

Komentarze dla notki “Ciasteczka w JavaScript, cz. 2”

  1. BTM - 28 grudnia 2008, 18:11:56

    Widzę, że zainspirowałem, ale z mistrzem JS to nie mam co zaczynać ;]

  2. Rafael - 28 grudnia 2008, 18:15:14

    @BTM: faktycznie, po przeczytaniu notki na Twoim blogu postanowiłem w końcu napisać artykuł dotyczący mojego skryptu. Nie mogłem pozwolić, żeby skrypt umarł nie będąc nigdzie opublikowanym ;-)

  3. Michał Górny - 28 grudnia 2008, 20:21:48

    To jeszcze dodać należy, że w XHTML-u ‘document.cookie’ nie istnieje, a każda sensowna przeglądarka powinna prędzej czy później dorobić się możliwości blokady tej funkcjonalności, domyślnie nastawionej na ‘on’.

  4. Rafael - 28 grudnia 2008, 21:02:05

    @Michał Górny: Mógłbyś bardziej rozwinąć? Chyba nie wszystko zrozumiałem.

    document.cookie istnieje jako własność obiektu HTMLDocument w dokumentacji DOM Level 2 HTML Dokumentacja ta dotyczy zarówno dokumentów HTMLowych (4.01) jak i dokumentów XHTML 1.0

    This specification defines the Document Object Model [...] of HTML 4.01 and XHTML 1.0 documents

    Czyli dla XHTML 1.0 Twoje stwierdzenie uważam za nieprawdziwe. Wiem, XHTML 1.0 jest kompatybilny wstecz. Pozostaje tylko kwestia XHTML 1.1, dlatego proszę o Twój komentarz w tej sprawie.

    W każdym bądź razie nie spodziewam się wyrzucenia funkcjonalności ciasteczek z implementacji JS, mimo, że dostajemy powoli wraz z „HTML5” mechanizm Storage i inne, które mogą tylko częściowo zastąpić mechanizm ciasteczek.

    a każda sensowna przeglądarka powinna prędzej czy później dorobić się możliwości blokady tej funkcjonalności, domyślnie nastawionej na ‘on’.

    Też nie za bardzo rozumiem. Ciasteczka można spokojnie wyłączyć już dzisiaj w przeglądarce. Chyba, że masz na myśli kompletne usunięcie własności cookie z dokumentu?!

  5. Michał Górny - 28 grudnia 2008, 21:05:01

    Rafael: Nie chodzi mi o wyłączenie ciasteczek, a jedynie dostępu do nich z JS. Wiąże się ono bowiem z poważnymi zagrożeniami, jeśli chodzi o bezpieczeństwo — i w wielu miejscach bez większego problemu można „wykraść” cudze ciastka.

  6. Rafael - 28 grudnia 2008, 21:11:31

    To fakt, że JS stanowi – na źle zabezpieczonych stronach – zagrożenie, ale dlatego proponuje się programistom przeglądarek wprowadzenie obsługi parametru HttpOnly, który webmasterzy mogliby ustawiać dla poufnych ciasteczek, który spowoduje, że ciasteczka z tą flagą nie będą widoczne z poziomu skryptów wykonywanych przez przeglądarkę. IE ma to od jakiegoś czasu.
    W każdym bądź razie, dopóki nie możemy opierać się na innym mechanizmie przechowywania danych przez skrypty JS, ciasteczka będą nadal miały „wzięcie”.

  7. BTM - 28 grudnia 2008, 21:27:25

    Nie widzę co prawda żadnego powiązania pomiędzy wersją XHTML a obsługą ciasteczek przez document.cookie – przecież to leży w gestii JS/DOM jak zaznaczył Rafael, a ta jest raczej niezależna od standardu dokumentu?

  8. Rafael - 28 grudnia 2008, 21:46:26

    @BTM: tak, ale sama część dokumentacji DOM jest już zależna od typu dokumentu. Jak cytowałem DOM Level 2 HTML zdefiniowany jest dla dokumentów HTML i XHTML 1.0. Ale jak Michał Górny napisał, głównie rozchodzi się tu o zabezpieczanie dostępu do ciasteczek. Od jakiegoś roku natknąłem się na sporą ilość dyskusji na temat ciasteczek (linków nie jestem w stanie podać), propozycji zmiany tego mechanizmu, żeby stał się mniej podatny na „ataki”/wykradanie ciastek. Niektóre propozycje – o ile dla mnie były absurdalne – cieszyły się jako takim zainteresowaniem wśród deweloperów, bo były relatywnie proste do wdrożenia i nie ingerowały w znacznym stopniu w dzisiejszy „porządek” (aktualne założenia).

  9. Bartosz "BTM" Szczec - 28 grudnia 2008, 21:49:06

    No tak, ale całkowite uniemożliwienie odczytu ciasteczek przez JS jest moim zdaniem rozwiązaniem błędnym. Szczególnie, jeżeli jedynym „za” jest uniknięcie ataków bazujacych na kradzieży ciastek

  10. Wasacz - 30 grudnia 2008, 11:41:22

    A ten tego, wie ktoś, kiedy mniej więcej Opera dostanie local/globalStorage?

  11. Rafael - 03 stycznia 2009, 12:09:30

    Niestety, na to pytanie nie jestem w stanie odpowiedzieć. O ile lubię obserwować rozwój przeglądarek to w przypadku Opery jestem trochę zniechęcony, bo cała wiedza na temat tej przeglądarki pochodzi ze szczątkowych informacji zespołu programistycznego i wydań tygodniowych. W ten sposób za bardzo nie znamy przyszłości tylko stan aktualny.

  12. Karol - 07 marca 2009, 23:06:47

    (Komentarz zmodyfikowany 15.03.2009 o 18:45)

    Mam dwa pytania:
    co robi srednik na samym poczatku? czy to jakis hack na konkretna przegladarke?
    skad decyzja zeby uzyc eval do tworzenia obiektu? nie lepiej

    
    var t={};
    (document.cookie+’;’).replace(/([\w%]+)=?([^;]*);/g,function (m,a,b) {t[a]=b});
    
  13. Rafael - 15 marca 2009, 16:45:31

    co robi srednik na samym poczatku? czy to jakis hack na konkretna przegladarke?

    Nie, ten średnik nie jest żadnym hackiem na przeglądarki. W zasadzie piszę go z przyzwyczajenia. Daję go dla pewności, że interpreter uzna tę funkcję anonimową, wraz z otaczającymi ją nawiasami za początek nowej instrukcji a nie część wcześniej zadeklarowanej zmiennej czy funkcji. Taka abstrakcyjna sytuacja, ale możliwa, gdy zdarza nam się nie wstawiać średników na końcu instrukcji, tak jak czasami mi się zdarza.

    Co do użycia eval, to po prostu w momencie pisania artykułu nie wpadło mi do głowy, żeby w ten sposób użyć replace ;-P Dzięki za zwrócenie uwagi, odnotuję to w powyższym skrypcie.

    Przepraszam, że tyle czekałeś na odpowiedź, ale info o nowym komentarzu nie dotarło do mnie na jabbera.

Pozostaw komentarz

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