PHP/Obsługa ciastek

< PHP
Poprzedni rozdział: Podstawy wyrażeń regularnych
Następny rozdział: Sesje

Obsługa ciastek

edytuj

Czym są nagłówki HTTP?

edytuj

Serwer w odpowiedzi na żądanie HTTP wysyła nie tylko kod HTML, lecz także zestaw nagłówków pomagających przeglądarce na zidentyfikowanie dostarczanych treści. Nagłówki precyzują typ MIME danych, np. text/html dla dokumentu HTML, kodowanie znaków, zachowanie się serwerów proxy, ustawienia cache'owania danych itd. PHP posiada funkcję header() pozwalającą skryptowi wysłać własne nagłówki. Muszą być one jednak zdefiniowane przed wysłaniem jakiegokolwiek kodu HTML (są to przecież NAGŁÓWKI). W przeciwnym wypadku dostaniemy niemiły komunikat Cannot add header information. Programiści często wykorzystują nagłówki do zdefiniowania typu oraz kodowania w nadsyłanym dokumencie:

<?php

    // Bedziemy wysylac dokument HTML z kodowaniem UTF-8
    header('Content-type: text/html;charset=utf-8');

?>

Ale nie tylko. Wiele stron udostępnia swe archiwa nie poprzez bezpośredni dostęp do katalogu, w którym są trzymane, ale poprzez specjalny skrypt, który wysyła ich zawartość do ściągnięcia. Nagłówki umożliwiają powiadomienie przeglądarki, że teraz będzie szedł plik o takiej i takiej nazwie, który internauta chce pobrać na swój dysk twardy. Nie będzie on jednak ujawniać katalogu na serwerze, gdzie on się znajduje. Cała komunikacja prowadzona jest za pośrednictwem PHP.

<?php
 
 	if(!isset($_GET['plik'])) // 1
 	{
 		die('Podaj nazwę pliku!');
 	}
 
 	$_GET['plik'] = basename($_GET['plik']); // 2
 	
 	if(@is_file('./pdf/'.$_GET['plik']))
 	{
 		// 3
 		header('Content-type: application/pdf');
 		header('Content-Disposition: attachment; filename="'.$_GET['plik'].'"');
 		readfile('./pdf/'.$_GET['plik']);
 	}
 	else
 	{
 		// 4
 		header('HTTP/1.1 404 Not Found');
 		exit('Nie znaleziono pliku '.htmlspecialchars($_GET['plik'])); // możesz wypisać całą treść strony z komunikatem o błędzie.
 	}
 
?>

Powyższy kod może być częścią jakiegoś serwisu, który swoje artykuły udostępnia także w formacie PDF do ściągnięcia, gdyż aktualnie tylko takie pliki można nim wysyłać. Przeanalizujmy go krok po kroku.

  1. Na początku dokonujemy sprawdzenia, czy ktoś w ogóle zainteresował się podaniem nazwy dokumentu do pobrania.
  2. Bezpieczeństwo na miejscu pierwszym - wszystko, co jest nazwą pliku i pochodzi od internauty, powinno być przepuszczone przez funkcję basename(), która wyciągnie z niego wyłącznie nazwę i odrzuci jakieś przejścia między katalogowe, co mogłoby zagrozić bezpieczeństwu. Wyobraź sobie, że ktoś wpisze sobie np. ../strona_hasla.php. Bez tego zabezpieczenia dostałby hasła dostępu do naszej strony, lecz basename() odrzuci niebezpieczny fragment ../. Mamy więc pewność, że internauta będzie ściągać TYLKO i WYŁĄCZNIE to, co chcemy, aby ściągał.
  3. Jeśli stwierdzimy, że plik istnieje, powiadamiamy nagłówkami, że oto nadejdzie dokument PDF jako załącznik o odpowiedniej nazwie. Funkcją readfile() wysyłamy jego zawartość.
  4. Gdyby ktoś podał niewłaściwą nazwę pliku, możemy wysłać mu komunikat błędu 404.

Powyższy przykład możemy nieco przerobić tak, aby z powodu podania błędnej nazwy internauta odsyłany był do naszego własnego komunikatu. Nagłówki umożliwiają robienie przekierowań HTTP i właśnie pragniemy pokazać, jak to się robi.

<?php
 
 	if(!isset($_GET['plik']))
 	{
 		die('Podaj nazwę pliku!');
 	}
 
 	$_GET['plik'] = basename($_GET['plik']);
 	
 	if(@is_file('./pdf/'.$_GET['plik']))
 	{
 		header('Content-type: application/pdf');
 		header('Content-Disposition: attachment; filename="'.$_GET['plik'].'"');
 		readfile('./pdf/'.$_GET['plik']);
 	}
 	else
 	{
 		header('Location: http://localhost/~kurs/notfound.php');
 		exit;
 	}
 
?>

Źródło jest zasadniczo podobne do poprzedniego przykładu. Zmiany widać jedynie w bloku else, gdzie wysyłamy tym razem nagłówek Location. Informuje on przeglądarkę, że treści nie będzie i powinna raczej skontaktować się z podanym plikiem. Innymi słowy, robimy przekierowanie internauty pod inny adres. Protokół HTTP 1.1 wymaga, aby w nagłówku był podany pełen adres do żądanego zasobu. HTTP 1.0 nie miał takich ograniczeń.

Kiedy omówiliśmy sobie już właściwości oraz niektóre możliwości nagłówków, możemy przejść do ciastek (ang. cookies) ustawianych właśnie za ich pomocą.

Ciastka w PHP

edytuj

Chyba każdy internauta słyszał o ciastkach i wyolbrzymianych "zagrożeniach" z nimi związanych. W rzeczywistości są to zwyczajne informacje umieszczane przez witryny WWW w przeglądarkach po to, aby był do nich dostęp między wywołaniami kolejnych podstron w obrębie witryny. Jedyny problem może pojawić się z tzw. ciastkami publicznymi, które może odczytać każda strona w Internecie. Jednak poza tym jest to bardzo pożyteczne narzędzie wykorzystywane m.in. w autoryzacji użytkowników.

Ciastka są ustawiane za pomocą nagłówków HTTP i mają pewien określony termin ważności. Po jego upływie przestają istnieć. Do wysyłania ciastek służy w PHP funkcja setcookie(), a do pobierania wartości tych ustawionych przez wcześniejsze żądania HTTP - specjalna tablica $_COOKIE. Napiszemy teraz prosty skrypt, który robił furorę kilka lat temu, w okresie popularyzacji dynamicznych witryn WWW. Chodzi o umieszczenie prostej informacji dot. ostatniej wizyty internauty u nas. Aby to wykonać, wystarczy przy pierwszym wejściu umieścić na np. miesiąc ciastko z datą ostatniej wizyty, po czym ją sukcesywnie odczytywać.

<?php
 	if(!isset($_COOKIE['wizyta']))
 	{
 		setcookie('wizyta', time(), time() + 30 * 86400);
 		echo 'Witaj, gościu.';
 	}
 	else
 	{
 		setcookie('wizyta', time(), time() + 30 * 86400);
 		echo 'Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']);	
 	}
 
?>

Trzy pierwsze parametry setcookie() są najważniejsze (ma ona ich trochę więcej). Jest to kolejno: nazwa ciastka, jego wartość oraz data ważności w sekundach od 1.1.1970. Data, a nie okres ważności, stąd przy jego ustawianiu przydaje się funkcja time(). W powyższym skrypcie sprawdzamy, czy ustawialiśmy już ciastko dla danego internauty. Jeśli nie, tworzymy je i wyświetlamy komunikat powitania. W przeciwnym wypadku także aktualizujemy wartość, ale też wyświetlamy datę ostatniej wizyty odczytaną właśnie z ciastka.

Zauważ, że wywołanie setcookie() nie nadpisuje wartości w tablicy $_COOKIE. Dlatego bez problemu można uprościć powyższy skrypt:

<?php
 	setcookie('wizyta', time(), time() + 30 * 86400);
 	if(!isset($_COOKIE['wizyta']))
 	{
 		echo 'Witaj, gościu.';
 	}
 	else
 	{
 		echo 'Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']);	
 	}
 
?>

Aby istniejące ciastko skasować, wywołujemy funkcję setcookie() z jakąś przeszłą datą ważności:

<?php
 
 	setcookie('wizyta', '', 0);
 
?>

Funkcje buforowania wyjścia

edytuj

Istnieją sytuacje, kiedy musimy wysłać jakiś kod HTML przed wysłaniem nagłówków HTTP, jednak tradycyjne metody nie pozwalają na to. W PHP można ten problem omijać, używając funkcji buforowania wyjścia. Ogólnie rzecz biorąc, przechwytują one treść wysyłaną przez echo albo print, zapisując ją do specjalnego bufora. Opróżniamy go samodzielnie na samym końcu skryptu, symulując efekt równoczesnego wysyłania nagłówków i kodu HTML. Pokażemy to na przykładzie:

<?php
 
 	ob_start();
 
 	echo '<h1>Tytuł witryny</h1><p>I inne komendy HTML.</p>';
 	
 	setcookie('wizyta', time(), time() + 30 * 86400);
 	if(!isset($_COOKIE['wizyta']))
 	{		
 		echo '<p>Witaj, gościu.</p>';
 	}
 	else
 	{
 		echo '<p>Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']).'</p>';	
 	}
 
 	ob_end_flush();
 
?>

Zwróć uwagę, że przed stworzeniem ciastka skrypt wysyła już kod HTML. Dlatego wszystkie instrukcje zawarliśmy między funkcjami ob_start() i ob_end_flush(). Pierwsza inicjuje buforowanie wyjścia, a druga kończy je, wysyłając jego zawartość do przeglądarki.

Buforowanie wyjścia może też posłużyć do celów algorytmicznych, kiedy musimy przechwycić wysyłany kod, aby go jeszcze dodatkowo obrobić. Naraz można mieć otwartych kilka buforów działających zgodnie z zasadą stosu, tj. ostatni otwarty bufor będzie pierwszym, z którego pobierzemy zawartość. Poniżej prezentujemy mały skrypt cenzorski. Przechwytuje on tekst i cenzuruje go, chyba że internauta zna sposób aktywujący prawdziwą treść. Jest to coś w sam raz dla walczących z dyktaturami opozycjonistów.

<?php
 	ob_start();	
 	
  	echo '<p>Pan Jan Nowak jest bardzo nieprzyzwoitym człowiekiem. Powiada, że dzień bez łapówki to
 		dzień stracony. Pracuje w urzędzie miejskim Obiektowa i nie wstydzi się swych podejrzanych interesów.</p>';
 	
 	// CENZURA
 	// Pobieramy zbuforowany tekst
 	// I **dla czytelności** przykładu zapisujemy go w zmiennej	
 	$kod = ob_get_clean();
 	
 	if(isset($_GET['real']))
 	{
 		// Wtajemniczeni znają całą prawdę
 		echo $kod;
 	}
 	else
 	{
 		// Reszta może się tylko domyślać
 		echo str_replace(array(
 			'Jan Nowak',
 			'Obiektowo',
 			'Obiektowa'	
 		), array(
 			'Alojzy Kromka',
 			'Hyzia Wólka',
 			'Hyziej Wólki'	
 		), $kod);
 	}
 
?>

Buforowanie wyjścia jest też podstawą tzw. kompresji GZip. Jest to kompresowanie treści strony przed wysłaniem tak, aby zajmowała mniejszą objętość, przez co użytkownik szybciej ją pobierze. Kompresję wspierają wszystkie nowoczesne przeglądarki (np. Opera, Firefox).

Aby uruchomić kompresję GZip, twoja wersja PHP musi mieć doinstalowaną bibliotekę zlib. Wtedy możesz rozpocząć buforowanie poniższym kodem:

 ob_start('ob_gzhandler');
 ob_implicit_flush(0);

Parametr przekazywany do ob_start() to nazwa tzw. uchwytu (handlera) służącego do modyfikacji zbuforowanej treści. ob_gzhandler jest jednym z predefiniowanych uchwytów, zajmującym się właśnie kompresją GZip. Druga z funkcji nakazuje wywołanie uchwytu dopiero, gdy będziemy mieli już cały kod HTML. Musimy o niej pamiętać dlatego, że nie można skompresować danych wyjściowych partiami - musi to być przeprowadzone w sposób ciągły. PHP samodzielnie wykrywa, czy przeglądarka użytkownika posiada obsługę tej możliwości, dlatego nie musisz o tym pamiętać.

Ciastka a bezpieczeństwo

edytuj

Ciastka są podstawą wielu systemów autoryzacji użytkowników dzięki możliwości przesyłania za ich pomocą danych między stronami. Jednak wielu początkujących programistów PHP nie rozumie lub nie wie, jak robić to bezpiecznie. Ciastko jest zwyczajnym nagłówkiem HTTP i naprawdę nie stanowi dużego problemu przechwycenie jego treści. Dlatego pamiętaj, aby nigdy nie przesyłać nim loginów, haseł zalogowanego użytkownika, ani żadnych innych danych potencjalnie pomocnych przy autoryzacji. Jest to całkowicie zła droga, gdyż systemy autoryzacji pisze się z wykorzystaniem sesji. Sesja jest pewnym rekordem z informacjami identyfikacji danego internauty przechowywanymi w bazie albo w pliku tekstowym na serwerze. Posiada długi, alfanumeryczny identyfikator i to on przesyłany jest ciastkiem. Porównując ID z ciastka, a także takie parametry, jak adres IP czy używana przeglądarka, można wyeliminować przypadki kradzieży ID sesji. Mechanizmu sesji nie musisz pisać samemu, gdyż PHP posiada własny. Omówimy go w następnym rozdziale.