haxite.org

Professionalism has no place in art, and hacking is art. Software Engineering might be science; but that's not what I do. I'm a hacker, not an engineer. - Jamie Zawinski

Mini tutorial używania mod_rewrite z życiowymi przykładami (;

02-01-2006 16:06



Mocnym punktem mod_rewrite jest konfigurowalność i elastyczność na miarę Sendmaila. Najgorszą cechą mod_rewrite jest konfigurowalność i elastyczność Sendmaila.

Moduł serwera Apache mod_rewrite to nardzędzie oparte na regułach przepisywania adresów wewnątrz serwera służące m.in. do:
* czynienia URL-i witryny bardziej przyjaznych użytkownikowi (dostępność; funkcjonalność)
* czynienia URL-i witryny bardziej przyjaznych wyszukiwarkom (SEO)
* blokowania hotlinkowania wybranych dokumentów na serwerze (prawa autorskie; limitowanie kradzieży bandwidthu)
* nieeksponowania prawdziwych ścieżek skryptów odwiedzającym strony (bezpieczeństwo; estetyka)


Ten mały tutorial wprowadza do świata przepisywania i maskowania URL-i poprzez zestaw praktycznych przykładów. Po pełną dokumentację dyrektyw oraz oficjalny tutorial w języku angielskim odsyłam do strony producenta (linki na końcu artykułu).

W tutorialu przedstawię najczęstszą sytuację, jaką możemy spotkać: wykupiliśmy hosting na dobrym serwerze apaczowym z modułem mod_rewrite oraz mamy aktywne ładowanie ustawień z plików typu '.htaccess'.

Do stosowania reguł przepisywania należy mieć choć podstawową znajomość wyrażeń regularnych. Zbiór niektórych wyrażeń został zawarty w tabelce w dokumentacji: http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html#rewriterule .

* 1. Dyrektywy RewriteEngine oraz RewriteBase
Składnia:
RewriteEngine on|off

W katalogu głównym naszej witryny utwórz plik .htaccess (niektórzy klikacze mają problem ze stworzeniem takiego pliku pod Windowsem. Polecam polecenie konsolowe echo Test > .htaccess). Dyrektywa RewriteEngine definiuje, czy w danym katalogu (oraz potomnych - jest ona dziedziczona i może zostać nadpisana w podkatalogu) ma być aktywny silnik przepisywania. W Twoim przypadku oczywiście pożądane jest ustawienie jej wartości na On - żeby zacząć pracę z regułkami przepisywania należy włączyć ich obsługę (: Dokumentacja Apache również zaleca ustawianie tej dyrektywy na wartość off zamiast wykomentowania wszystkich reguł przepisywania (zysk wydajności).

Składnia:
RewriteBase URL-path

RewriteBase definiuje ścieżkę relatywną do aktualnego katalogu. Wg. niej powinno będzie budowane podzapytanie. Domyślnie jest to FIZYCZNA ścieżka aktualnego katalogu na serwerze, dlatego będziesz musiał wpisać wartośc / jako argument tej dyrektywy.

Sekcja pliku .htaccess służąca przepisywaniu URL-i rozpoczyna się teraz tak:

RewriteEngine On
RewriteBase /


* 2. Dyrektywa RewriteCond
Składnia:
RewriteCond TestString CondPattern

Definiuje dokładnie jeden warunek dokonywania przepisywania URL-a. Dzięki nim ograniczamy dokonywanie przepisywania tylko dla wybranych przypadków - np. pobierania dokumentów wynikowych ze skryptów .php. Selekcja przypadków nadającyh się do zapytania polega na dopasowaniu argumentu TestString do wyrażenia regularnego (typu Perl czyli PCRE) CondPattern. Przykład:

** Przykład 2.1 Dopuszczenie do przepisywania wyłącznie ścieżek rozpoczynających się od rw/ z "rw/foo" na foo.php

RewriteCond ${REQUEST_URI} ^/rw/[^/]+

Wypisanie kilku takich dyrektyw po sobie łączy domyślnie ich warunki operatorem AND => ogólny warunek będzie spełniony tylko, gdy każdy warunek po kolei będzie spełniony. Operator można zmienić na OR dołączając flagę [OR] na koniec CondPattern.

** Przykład 2.2 Dopuszczenie do przepisywania ścieżek wywołujących pliki z rozszerzeniem ".html" lub o nazwie rozpoczynającej się od "indeks"

RewriteCond %{REQUEST_URI} ^/[^?]+\.html(\?.*)?$              [OR]
RewriteCond %{REQUEST_URI} ^[^?]+/indeks\.[\w]+(\?.*)?$


Kolejną możliwą do użycia flagą warunku jest [NC] czyli niezwracanie uwagi na wielkość liter. Dodając to do poprzedniego przykładu otrzymasz:

** Przykład 2.3 Łączenie flag warunków

RewriteCond %{REQUEST_URI} ^/[^?]+\.html(\?.*)?$              [NC,OR]
RewriteCond %{REQUEST_URI} ^[^?]+/indeks\.[\w]+(\?.*)?$       [NC]


Zamiast wyrażenia regularnego można zastosować jedno ze specjalnych wyrażeń testujących. Są to funkcje np. sprawdzające istnienie pliku wskazanego przez argument TestString. Przykład

** Przykład 2.4 Specjalne wyrażenia testujące RewriteCond

Sprawdzenie, czy żądany plik jest istniejącym katalogiem
RewriteCond %{REQUEST_FILENAME} -d
Powyższa dyrektywa pozwoli Ci np. zamaskować listowanie katalogów poprzez własny dokument HTML z informacją o braku dostępu itp.

Sprawdzenie, czy żądany plik jest niepustym plikiem:
RewritCond %{REQUEST_URI} -s

Porównanie leksykograficzne ciągów znaków. Sprawdzenie, czy aktualna nazwa hosta jest "mniejsza" od zadanego ciągu:
RewritCond %{REQUEST_URI} <srv5.twojfajnyserwis.net

Odwrócenie warunku - np. nie chcesz aby Twoje warunki działały jeśli klient przychodzi z adresu IP Neostrady:
RewritCond %{REMOTE_ADDR} !^83\.25

Po wszystkie dostępne zmienne serwera oraz specjalne warunki testujące odsyłam do: http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html#rewritecond

* 3. Dyrektywa RewriteRule
Składnia:
RewriteRule Pattern Substitution

Programiści Perla mogą sobie wyobrazić działanie tej dyrektywy jako $NowyUrl = s/Pattern/Substitution/$StaryUrl/. Ta dyrektywa jest esencją mod_rewrite. Jej reguły są (podobnie jak RewriteCond) kompilowane podczas przechodzenia przez plik .htaccess i trzymane w pamięci do ewentualnego stosowania na zapytaniach naszego katalogu. Dyrektywa RewriteRule może być poprzedzona wspomnianą RewriteCond aby ograniczyć przepisywanie tylko do niektórych przypadków.

W Twoim przypadku, gdy działasz na lokalnym katalogu wpisując dyrektywy w pliku .htaccess argument Pattern nie będzie rozpoczynał się od slasza jak prawdziwy REQUEST_URI. Zostanie on obcięty o rozpoczynający URN katalogu ("/"). W ten sposób jeśli plik .hataccess stworzymy w katalogu głównym serwera (teoretycznie dostępny pod http://twojfajnyserwis.net/.htaccess), to zapytanie klienta o adres http://twojfajnyserwis.net/index50.php włoży do Pattern ciąg "index50.php", a http://twojfajnyserwis.net/podkatalog/niusy.php z kolei włoży "podkatalog/niusy.php".

Pewnie zastanawiasz się, na co komu taki argument Pattern skoro jest on uzupełniany przez serwer i nie daje nam pola do popisu swoją znajomością wyrażeń regularnych. Otóż daje nam ową możliwość - jeśli serwer nie dopasuje zmiennej REQUEST_URI (uciętej o URN aktualnego katalogu) do naszego argumentu Pattern, to przepisanie po prostu nie dokona się. Jest to więc, de facto, drugi po RewriteCond warunek przepisania. Jeśli RewriteCond zostanie zaspokojone, a żadno wyrażenie z RewriteRule nie zostanie dopasowane, to serwer będzie próbował zrealizować zapytanie jak by silnik mod_rewrite nie działał.

Rodzi się pytanie "po co więc jest RewriteCond skoro zażądana przez klienta ścieżka i tak jest testowana na argumencie RewriteRule"? Otóż RewriteRule nie daje takiej możliwości testowania jak RewriteCond. Nie pozwala testować zawartości Cookie, sprawdzania adresu IP klienta itp. Stąd RewriteCond stosuj, gdy chcesz sprawdzić coś innego od ścieżki zapytania jeśli RewriteRule tego nie robi.

Parametr Substitution definiuje wynikowy URL, jaki rzeczywiście ma być zrealizowany na serwerze. W domyślnym zastosowaniu serwer wewnątrz swojego procesu wykona niewidoczne podzapytanie i postara się zwrócić URL powstały po zastosowaniu ostatniej regułki przepisywania, podczas gdy w pasku adresu przeglądarki klienta pozostaje URL pierwotny.

Jeśli serwer napotka błąd podczas realizacji podzapytania, zostanie on obsłużony jak zwykłe zapytanie, np. zostanie zwrócony domyślny dokument błędu HTTP 404. Wyjątkowym przypadkiem jest zapętlenie się regułek (makabra!) - serwer robiąc podzapytania nadal sprawdza nasze warunki i regułki, jeśli w ciągu 10 podzapytań nie nastąpi zakończenie takiego przepisywania, to serwer wywoła błąd HTTP 500. Stąd wniosek dla Ciebie, że dobrą zasadą jest stosowanie URL-i publicznych, które łatwo regułka odróżni od wewnętrznego podzapytania i w ten sposób wykluczy zapętlenie. Limit 10 podzapytań możesz podwyższyć stosując dyrektywę RewriteOptions, patrz: http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html#rewriteoptions .

Przykład 3.1 Przykład wprowadzający.

Załóżmy, że chcesz zablokować listowanie katalogów poprzez wyświetlanie własnego dokumentu z informacją znajdującego się w tym katalogu, co .htaccess.

RewriteCond %{REQUEST_FILENAME} -d
RewriteRule .*             informacjaOListowaniu.php


Wyłumaczenie: argument RewriteCond ograniczy wykonanie następującej reguły jedynie do zapytań o katalogi. RewriteRule przepisze dowolne (wyrażenie ".*" pasuje do dowolnego ciągu znaków, nawet pustego) dopuszczone zapytanie na dokument "informacjaOListowaniu.php". Ciekawostka: po użyciu takiej regułki przepisywania wpisz w pliku informacjaOListowaniu.php kod:
<?php echo $_SERVER['REQUEST_URI']; ?>
.

Przykład 3.2 Budowanie regułek przepisywania dla serwisu opartego o PHP/(dowolny język server-side).

Załóżmy, że Twój serwis pehapowy posługiwał się do tej pory takimi ścieżkami:

http://twojfajnyserwis.net/index.php

                         /index.php?dzial=niusy
                         /index.php?dzial=artykuly
                         /index.php?dzial=ksiegaGosci
                         /index.php?dzial=repozytorium

                         /index.php?dzial=nius&id=[0-9]+
                         /index.php?dzial=artykul&id=[0-9]+
                         /index.php?dzial=repozytorium&id=[0-9]+


Gdzie [0-9]+ oznacza dowolną nieujemną liczbę całkowitą.

Przyznasz, że takie ścieżki nie wyglądają zbyt zachęcająco dla ludzkiego oka i fajnie, gdyby ktoś cudownie je mógł prezentować jako:

http://twojfajnyserwis.net/index

                         /niusy
                         /artykuly
                         /ksiegaGosci
                         /repozytorium

                         /nius/[0-9]+
                         /artykul/[0-9]+
                         /repozytorium/[0-9]+


Jak widać, index.php przyjmuje wartość argumentu "dzial" tylko z ustalonego zbioru. Można to ograniczenie zaimplementować w naszym warunku RewriteCond pozostałe zapytania traktując jako nie nadające się do przepisywania. Do rzeczy:

Przykład 3.3 Warunki i regułki do obsługi serwisu (pominięto powtarzające się dyrektywy RewriteEngine i RewriteBase)


RewriteCond %{REQUEST_URI}                                       ^/(niusy)|(artykuly)|(ksiegaGosci)|(repozytorium)|(nius)|(artykul)
RewriteRule ^((nius)|(artykul)|(repozytorium))/([0-9]+)          /index.php?dzial=$1&id=$5   [L,NC]
RewriteRule ^((niusy)|(artykuly)|(ksiegaGosci)|(repozytorium))   /index.php?dzial=$1         [L,NC]


Ważne jest, aby mod_rewrite potrafił rozróżniać co jest zapytaniem "uładzonym", a co nieprzepisywalnym zapytaniem o fizycznie istniejący plik. Dlatego, jeśli na serwerze istniały katalogi o nazwach np. "niusy" czy "artykul/61" to dostęp do nich zostanie stracony. Stąd, idąc za wskazówką daną w poprzednim dziale, wprowadzisz niewielki token w URL-u to rozróżniania uładzonych URL-i od prawdziwych. Założymy, że nowe, bezpieczniejesze URL-e będą miały postać:

http://twojfajnyserwis.net/mr/index

                         /mr/niusy
                         /mr/artykuly

itd.

Nazwa "mr" oznacza np. "mod_rewrite" i oczywiście na serwerze nie należy trzymać katalogu o takiej nazwie, bo dostęp do niego zostanie utracony. Co prawda, nauczyłeś się już odróżniać zapytania o istniejące fizycznie pliki i katalogi (RewriteCond %{REQUEST_FILENAME} -d) i mógłbyś już dodać na początku warunek "zwracający" katalog "/mr/", lecz działoby się to kosztem wydajności i było zupełnie zbędne przy Twoich założeniach początkowych.

Zmodyfikuj więc swoje regułki wg przykładu:

** Przykład 3.4 Warunki i regułki z tokenem identyfikującym ścieżki dla mod_rewrite ("/mr/")


RewriteCond %{REQUEST_URI}                                          ^/mr/(niusy)|(artykuly)|(ksiegaGosci)|(repozytorium)|(nius)|(artykul)
RewriteRule ^mr/((nius)|(artykul)|(repozytorium))/([0-9]+)          /index.php?dzial=$1&id=$5   [L,NC]
RewriteRule ^mr/((niusy)|(artykuly)|(ksiegaGosci)|(repozytorium))   /index.php?dzial=$1         [L,NC]


Dlaczego kolejność regułek przepisywania jest akurat taka? Otóż, jeśli regułka ^mr/([a-z]+) zawiera się w ^mr/([a-zA-Z]+)/([0-9])+ itd. to obowiązuje nas zasada "do szczegółu do ogółu" (: Tj. w liście reguł najpier należy zdefiniować najbardziej szczegółową. Gdy serwer nie dopasuje do niej zapytania, sprawdzi następną itd. aż do znalezienia poprawnej. W powyższym przykładzie są tylko 2 możlie regułki i któraś z nich zostanie na pewno spełniona (taką selekcję zapewnił nam RewriteCond). Jeśli RewriteCond nie zwrócił prawdy, to regułki nie są w ogóle testowane i serwer traktuje zapytanie dosłownie.

Jak widzisz, pojawiły się jakieś dziwaczne flagi tuż za argumentem Substitution. Flaga [L] oznacza "last" czyli traktowanie aktualnej regułki jako ostatniej w łańcuchu - dalsze próby dopasowania nie będą dokonywane. Warto ją stosować ze względu na wydajność przy dużej ilości regułek, coby Twój serwer po znalezieniu idealnego przepisania nie próbował dopasować kolejnego. Flaga [NC] oznacza dopasowywanie wyrażenia regularnego z "no-case" - nieuwzględnianiem wielkości liter. Dlatego w wyrażeniu wyrzuciłem już "A-Z". Kolejność flag w tej dyrektywie nie gra roli.

Wprowadziłeś token identifikujący ścieżki dla mod_rewrite więc powinieneś się spodziewać, że takie ścieżki nie będą zawierały przypadkowych wartości (np. pierwszy człon będzie czymś spoza (niusy|artykuly|ksiegaGosci|repozytorium). Dobrze byłoby obsłużyć takie zapytania przesyłając podejrzany argument do skryptu, co zaraz Ci pokażę. Również użyjesz regułki zabraniającej dostępu bezpośredniego do katalogów poprzez zwracanie swojego dokumentu z ostrzeżeniem. (Aczkolwiek, ta ostatnia nie może się stosować do http://twojfajnyserwis.net/).

** Przykład 3.5 Dodanie obsługi podejrzanych zapytań


RewriteCond %{REQUEST_URI}         ^/mr/(niusy|artykuly|ksiegaGosci|repozytorium)(/[0-9]+)?
RewriteRule ^mr/([a-z]+)/([0-9]+)  /index.php?dzial=$1&id=$2   [L,NC]
RewriteRule ^mr/([a-z]+)           /index.php?dzial=$1         [L,NC]
# jesli powyzsze przepisania nie doszly do skutku to zapytanie celuje w jakis podejrzany URL
RewriteRule %{REQUEST_URI}         ^/mr/
RewriteRule ^mr/([a-z])/([0-9]+)   /index.php?dzial=podejrzanyDzial&nazwa=$1&id=$2     [L,NC]
RewriteRule ^mr/([a-z])            /index.php?dzial=podejrzanyDzial&nazwa=$1&id=brak   [L,NC]
RewriteRule ^mr/                   /index.php?dzial=podejrzanyDzial&nazwa=brak&id=brak [L] # tutaj bez NC bo jest zbędny

RewriteCond %{REQUEST_FILENAME}    -d
RewriteCond %{REQUEST_URI}         ^/[^?]+
RewriteRule (.*)                   /index.php?dzial=informacjaOListowaniu&sciezka=$1   [L]


Warunek RewriteCond %{REQUEST_URI) ^/[^\?]+ został dodany, coby regułka nie chwyciła katalogu głównego serwera - tego raczej nie chcesz. Od teraz będziesz miał pełną kontrolę nad wywoływanymi URL-ami na swoim serwerze - zapytania próbujące dostać się do katalogu oraz podejrzane zapytania o podstrony będą zgłaszane Twojemu skryptowi. Dane diagnostyczne będą przesyłane do zmiennych skryptu poprzez urkyty URL a klient niczego nie będzie się spodziewał.

Ostatnia regułka, jaką powinieneś dopisać do swojego .htaccess to dyrektywa ErrorDocument 404. Obsłuży ona zapytania, które nie zakwalifikowały się do Twoich reguł przepisujących (a może i zakwalifikowały się, tylko przypadkiem plik index.php nie istnieje) a odwołują się do nieistniejących plików.

Przykład 3.6 Dyrektywa ErrorDocument z głównego modułu serwera

ErrorDocument 404 index.php?dzial=blad404

* 4. Połączenie mod_rewrite i PHP w celu przekazywania nieokreślonej liczby argumentów do skryptu

W zaawansowanych serwisach liczba argumentów skryptu może nie być znana i pisanie wielu dlugich regułek nie będzie miało większego sensu. Budowanie URL-i może odbywać się w skrypcie PHP dynamicznie i ich powtórne przetwarzanie do zmiennych tablicy $_GET również. Przedstawię prosty sposób na realizację tego:

** Przykład 4.1 Przepisywanie nieznanej liczby argumentów przy pomocy kodu PHP


RewriteCond %{REQUEST_URI} ^/mr/[a-z0-9]+
RewriteRule ^mr/(.+)   index.php?argStr=$1    [L]
RewriteRule ^mr/       index.php              [L]


W ten sposób zamiast URL-i typu

http://twojfajnyserwis.net/index.php?modul=forum&sekcja=ogladaj&idWatka=616

możemy stosować

http://twojfajnyserwis.net/mr/modul/forum/sekcja/ogladaj/idWatka/616

W pliku .htaccess wkażesz pehapowi malutki skrypcik inicjujący przechwytywanie zmiennych do tablicy $_GET:

** Przykład 4.2 Kod PHP przepisujący wartości z URL-a do zmiennej $_GET

W pliku .htaccess:

php_value auto_prepend_file mr.php


W pliku mr.php:
<?php

error_reporting( E_ALL );

if( array_key_exists('argStr', $_GET) )
{
   $arr = explode( '/', $_GET['argStr'] );
   $d = 2*(int)( count( $arr )/2 );
   
   for( $i=0; $i<$d; $i+=2 )
   {
      $k = $arr[ $i ];
      $v = $arr[ $i+1 ];
      $_GET[ $k ] = $v;
   }
}

?>


* 5. Przekierowanie HTTP

Ta krótka sekcja pokaże Tobie jedynie znacznie flagi [R] dla dyrektywy RewriteRule. Do tej pory przekierowania robiłeś wewnątrz procesu serwera - klient nie miał o tym zielonego pojęcia, gdyż z jego punktu widzenia zapytania HTTP zostały obsłużone tak samo. Co jednak, gdy przenosimy serwis na inny serwis i chcemy, aby stare URL-e ładnie przekierowywały klientów na nowy serwer? Lub jeszcze banalniejsza sytuacja: chcesz, aby klienci mogli oglądać serwis wchodząc przez domenę www.twojfajnyserwis.net zamiast twojfajnyserwis.net:

** Przykład 5.1


RewriteCond %{HTTP_HOST} ^twojfajnyserwis\.net$
RewriteRule (.*)         [url]http://www.twojfajnyserwis.net/$1[/url]  [L,R=301]


Serwer dołącza nagłówek HTTP "Location" do odpowiedzi, co powoduje przejście przeglądarki pod wskazany adres (może on się znajdować na innej maszynie serwującej WWW) - w ten sposób mod_rewrite działa WIDOCZNIE dla klienta i to już zadaniem klienta jest wysłanie i obsłużenie wyniku takiego zapytania. Flaga [R], jak widać, wzięła swój argument o wartości "301". Ów argument jest opcjonalny, a jego wartość oznacza numer Kodu Odpowiedzi HTTP zwracanego przez serwer. Więcej: http://pl.wikipedia.org/wiki/Kod_odpowiedzi_HTTP. Domyślnym kodem jest HTTP 302 Moved Temporarily - "Przeniesiono Tymczasowo". Jeśli chcemy całkowicie przenieść serwis lepiej zwracać kod 301 - w ten sposób Googlebot i niektóre przeglądarki zapamiętają nowy adres witryny.

* 6. Wskazówki dotyczące wdrażania mod_rewrite

** 6.1 Wprowadzając nowe URL-e zawierające dodatkowe slasze ("/") pamiętaj, aby w Twoim HTML-u poprawić relatywne łącza i odwołania do obrazków! Przeglądarka nic nie wie o przepisywaniu adresów na serwerze i musi dostawać bezwzględne ścieżki do dokumentów.

** 6.2 Przy zapętleniach regułek przepisujących serwer zwróci dokument opisujący błąd HTTP 500 z treścią podobną do "More information about this error may be available in the server error log". Zajrzyj do pliku błędów serwera (np. error.log) aby dowiedzieć się więcej o zaistniałym błędzie.

** 6.3 W celu poprawienia wyników w wyszukiwarkach możesz dodawać tytuł podstrony do ścieżki dokumentu. Mod_rewrite może kompletnie porzucać tą sekcję ścieżki - ważne, żeby crawlery wyszukiwarek ją indeksowały. (Rodzi tu się mały problem z odwzorowaniem URL->dokument: nieskończona ilość URL-i może celować w dokładnie ten sam dokument. Jednak ten sam problem istniał bez mod_rewrite, gdy można było wpisać index.php?foo=tutajDowolnyTekst i i tak otrzymywało się ten sam dokument).

A poza tym uważam, że IE nalezy zniszczyć.

----------------------------------------------
Dziękuję.
Częściowy kopirajt: http://creativecommons.org/licenses/by-sa/2.5/ Paweł Zdziarski
faxe
Przesłał:
faxe
Komentarze (5):

lexus
Dołączył: 2004-10-31

02-01-2006 17:44
ładnie ;)

http://itmaniac.eu

frodo
Dołączył: 2003-09-07

02-01-2006 23:02
Cytat:
A poza tym uważam, że IE nalezy zniszczyć.


Katon byłby Ci wdzięczny :P

A odnośnie całości, wszyscy jesteśmy Ci wdzieczni za porządny artykuł :)

Myśl! Inaczej Oni zrobią to za Ciebie i zajmiesz ustalone miejsce w szeregu!

Nick
Dołączył: 2005-12-25

03-01-2006 19:31
dobry art niejest zly , ;) robilem to ale nie tak bardzo rozbudowany ;)

boze od dzisiaj po kres swiata , bedziesz moim wrogiem !

mam powód by być tu znowu środkowym palcem pozdrowic wrogów

button
Dołączył: 2005-03-01

07-01-2006 21:18
gud łork :)

hakto
Dołączył: 2013-04-05

05-04-2013 15:54
X lat temu z tego tutoriala uczylem sie mod_rewrite, stare dobre czasy (:
Zaloguj się aby dodać komentarz.