eRIZ’s weblog

PHP, webdesign, Linux, Windows i inne, bo nie samym chlebem człowiek żyje
Serdecznie zapraszam do udziału w ANKIECIE

mod_rewrite - (pozornie) beznadziejne problemy, które można rozwiązać

mod_rewrite, rewriting, przyjazne URL-e/adresy, maskowanie, przepisywanie, nazw jest sporo. Zresztą, najpopularniejsza pochodzi od swojego protoplasty - czyli mod_rewrite powstałego pod skrzydłami Apache’a jako moduł. Teraz właściwie standard, jeśli chodzi o nowoczesne strony www - nie tylko ze względu na wygląd, ale i (jak ptaszki ćwierkają, choć jest to wątpliwe wobec oficjalnych źródeł) SEO.

Jak zwał, tak zwał, adres http://example.org/kawalek/adresu wygląda dużo estetyczniej i jest łatwiejszy do zapamiętania niż potworki typu http://example.org/?kawalek=adresu&i=jeszcze&inny=fin

Niby nie jest to takie skomplikowane, ale niektóre sytuacje wydają się nie do rozwiązania.

Uwaga, notka tasiemcowata, więc jest spis treści. Polecam się również uzbroić w odpowiednią ilość czasu. ;]

  1. Czyżby? - wstępniak
  2. Z kopyta - podstawy
  3. A nie łatwiej? - czy jest sens korzystania
  4. Podstawowe praktyczne zastosowania - jo. (jak obok ;))
    1. Banowanie po IP
    2. Blokada przed hotlinkowaniem (no troszkę inna ;))
    3. www, czy nie www
    4. Wymuszanie szyfrowania połączenia
    5. Z ukośnikiem na końcu, czy bez?
  5. Częste pomyłki - najczęstsze błędy
  6. Sprytne rozwiązania dzięki mod_rewrite - czy wiesz że…?
    1. Wydzielenie katalogu public_html
    2. Wirtualne subdomeny
    3. Jeden serwis, podobne domeny - wiele języków
  7. Przypadki beznadziejne - wyższa szkoła jazdy - co jest pozornie nie do rozwiązania
    1. Przetwarzanie ciągu po znaku zapytania
    2. przepisywanie adresu ze znakami specjalnymi - w tym polskimi
    3. negocjacja zawartości bezpośrednio w Rewrite
  8. Implementacja rzecz straszna - strona i skrypty - po stronie skryptów i klienta
    1. Łatwe i szybkie generowanie identyfikatorów przyjaznych dla URL
    2. Łatwe korzystanie z cache całych stron
    3. Pliki CSS/obrazki
    4. Problemy z Light/Thick/Grayboxem i wyświetlaniem obrazków
    5. Zmuszenie formularzy do korzystania z przyjaznych URL-i
  9. Epilog

Czyżby?

Obrałem sobie kiedyś takie osobiste zadanie, aby chociaż spróbować zmierzyć się z każdym problemem związanym z przepisywaniem adresów na zrozumiałe dla skryptu formy. Rewriting to tak naprawdę forma zamaskowania tego, co widzą skrypty wobec postaci widzianej przez gości i przeglądarki. Jak to wygląda, patrz: zajawka (teaser).

Zacznijmy od problemów, które wiążą się z oczywistymi oczywistościami

Z kopyta

Żeby móc się tym jakkolwiek pobawić, trzeba zrozumieć parę rzeczy. Najważniejsza, to upewnić się, czy nasz serwer/demon w ogóle umożliwia wykorzystanie przepisywania adresów. Już chyba wszystkie hostingi udostępniają mod_rewrite gotowy do użycia.

Najpopularniejszy jest – rzecz jasna – format wprowadzony przez Apache, czyli zapisywanie regułek do plików htaccess. Są one bezproblemowo interpretowane przez serwery korzystające z demona z piórkiem w herbie oraz Litespeed. Niestety, pozostałe serwery nie obsługują nadpisywania konfiguracji w htaccess i powoduje to konieczność korzystania albo z obejść (tzn. przekierowywanie wszystkich żądań do skryptu zajmującego się tylko obrabianiem regułek), albo przepisywanie reguł na format danego serwera. Nie jest to zbyt trudne, ale często bywa uciążliwe chociażby z powodu dostępności do konfiguracji oraz konieczności restartowania demona. Owszem, powstawały już pewnego rodzaju pomysły na ominięcie tego problemu (np. htscanner), ale nie są one jeszcze tak satysfakcjonujące, jakbyśmy tego chcieli.

Skupię się na regułkach przystosowanych dla Apache/Litespeed (a to z tej racji, że napędzają większość rynku hostingowego).

We własnym warsztacie dobrze jest zainstalować Apache na własnym komputerze i w tym środowisku zająć się testowaniem regułek. Może to być również dowolny pakiet, który zawiera w sobie Indianina (WAMP, XAMP, LAMP, WebServ, itp), trzeba tylko pamiętać, aby w Linuksie pamiętać o skompilowaniu demona z flagą –enable-rewrite, a w Windows odkomentować linijkę z:

  1. LoadModule rewrite_module modules/mod_rewrite.so

Czytaj: skasować z jej początku #. W niektórych przypadkach będzie również konieczne ustawienie dyrektywy AllowOverride All. Rzecz jasna, cały czas mam na myśli plik httpd.conf, po którego modyfikacji restartujemy demona. Jeśli w konfiguracji nie jest nic zepsute, wszystko powinno być OK i utworzenie pliku .htaccess (samo rozszerzenie) w katalogu htdocs z zawartością:

  1. RewriteEngine On

nie wysypie nam serwera z błędem numer 500 po wywołaniu strony. Zakładam, że wszystko jest w porządku. Właśnie w ten sposób zaczniemy zabawę z przepisywaniem adresów. Wszystko sprowadza się do odpowiedniego manewrowania dyrektywami RewriteRule.

Przykładowy plik htaccess może wyglądać np. tak:

  1. RewriteEngine On
  2.  
  3. RewriteBase /katalog
  4. RewriteRule ^kategoria/(.+) kategoria.php?co=$1

Składnia nie należy do szczególnie skomplikowanych. Podstawą przepisywania adresów jest znajomość wyrażeń regularnych, których omówienie jest tematem na jeden albo i na całą serię artykułów. Wyrażenia są dość przydatne, wykorzystywane w wielu sytuacjach. Może kiedyś o nich napiszę. [;

Podstawą dyrektywą, która dodaje wyrażenie do puli przetwarzanych, jest RewriteRule. Składnia jest następująca:

  1. RewriteRule WYRAŻENIE ZAMIENNIK [FLAGI]

  • WYRAŻENIE, to wyrażenie regularne, którym zostanie przetestowany adres. Części, które zostaną wykorzystane w zamienniku otaczamy nawiasami.

  • ZAMIENNIK, to właściwy adres, który zostanie otwarty przez serwer. Aby wykorzystać frazy, które zostaną podstawione w miejsce nawiasów w zamienniku, korzystamy z formatu $1, $2 - liczba oznacza kolejny numer podstawnika w wyrażeniu regularnym. Przykładowe zastosowanie podałem we wcześniejszym listingu.

  • FLAGI, to dyrektywy którymi zostaną potraktowane regułki. W zasadzie, to przydatnych jest kilka, np.

    • [QSA], co jest akronimem od Query String Append. Jeśli korzystamy z jakichkolwiek formularzy posługujących się metodą GET, reflinkami, etc, jest to flaga niezbędna. Powoduje ona dopisanie do wywoływanego skryptu ciągu zwanego QUERY_STRING, czyli wszystkiego po pytajniku w adresie.

    • [L] - oznacza, że dana reguła jest ostatnią do przetwarzania. Wszystkie RewriteRule poniżej nie zostaną wykonane.

    • [NC] - powoduje, że wyrażenie jest testowane niezależnie od wielkości znaków w adresie (domyślnie, wyrażenia regularne rozróżniają wielkie i małe litery)

    • [R] - zamiast ukrycia prawdziwego adresu skryptu, serwer na niego przekierowuje (po ludzku: przeglądarka otwiera go tak, jakby był wpisany bezpośrednio do paska adresu). Opcjonalnie przyjmuje kod przekierowania, np. [R=301]

    • [PT] - dość ciekawa, ale rzadko wykorzystywana flaga. Oprócz mod_rewrite, są również inne moduły, które operują na adresie skryptu, jak np. mod_alias, czy inne. Umieszczenie tej flagi powoduje, że pozostałe rozszerzenia Apache otrzymają do przetwarzania już przepisany adres.

    Oczywiście flagi można łączyć podając je po przecinku, np. [L,NC,QSA].

Jeszcze powinienem wspomnieś o dyrektywnie zwanej RewriteBase. W wyrażeniach regularnych są dostępne znaki oznaczające początek oraz koniec frazy. Nieraz zdarza się tak, iż nasz katalog na stronę ma URL postaci np. http://example.org/~uzyszkodnik/ albo dowolny podkatalog względem głównego. Każdorazowe dopisywanie ~uzyszkodnik do wyrażenia byłoby uciążliwe. I tu pomaga RewriteBase, która określa ten początek dla wszystkich wyrażeń w pliku.

A nie łatwiej…?

No dobrze, można przecież cały ruch przekierować do jednego skryptu i z jego poziomu dokonywać dalszych przekierowań. Zapytam tylko - po co? Poza kilkoma niektórymi przypadkami nie ma to sensu, jest to tylko marnowanie mocy na przetwarzanie żądania przez interpreter skryptów, który może odciążyć właśnie silnik przepisywania adresów.

W jakich sytuacjach można pominąć? W zasadzie zawsze. ;] Chociażby wyświetlanie galerii zdjęć, czy też cache całych stron, ale o tym później.

W praktyce jest nieco inaczej - wiele gotowych skryptów/bibliotek przerzuca przepisywanie bezpośrednio do własnych mechanizmów ze względów przenośności - aby ten sam skrypt mógł działać na jak największej liczbie serwerów (pamiętajmy, że jest wiele innych demonów, w których konfiguracja przepisywania adersów jest zupełnie inna) oraz z innych szczególnych względów, o których powiem później.

podstawowe praktyczne zastosowania

Tutaj przede wszystkim przyda się nam dyrektywa RewriteCond. Umożliwia ona warunkowe wykonanie najbliższego przepisywania. Składnia?

  1. RewriteCond %{testowana_zmienna} wyrażenie [FLAGI]

  • testowana_zmienna - jest to jedna z typowych zmiennych dla skryptów. Jest to np. znaczna większość pozycji z PHP-owej zmiennej predefiniowanej $_SERVER, np. IP klienta, przeglądarka, itp. Pełna lista dostępna jest na stronach Apache.

  • wyrażenie - wyrażenie regularne, którego spełnienie jest jednoznaczne z “przepuszczeniem” warunku (warunek zwraca PRAWDA)

  • flagi - tu są tylko dwie: NC i OR. Pierwsza jest analogiczna, co w RewriteRule - ignoruje wielkość znaków, natomiast druga - sprawia, że wyrażenie jest łączone z następnym przez alternatywę (odpowiednik if … else if …).

Banowanie po IP

Jeśli chodzi o banowanie na poziomie serwera przez deny from all, to ma jedną, ale za to zasadniczą wadę - uniemożliwia wyświetlenie eleganckiego komunikatu dla użytkownika. Owszem, można skorzystać z własnych stron błędów, ale wówczas np. nie zapiszemy, który konkretnie adres IP nam szczególnie daje się we znaki. Gotową listę adresów można pobrać z np. ze strony projektu Sblam!

Tworzymy wówczas htaccess o mniej więcej takiej zawartości:

  1. RewriteEngine On
  2.  
  3. RewriteCond %{REMOTE_ADDR} ^(255\.255\.255\.255)|(0\.0\.0\.0)$
  4. RewriteCond %{REQUEST_URI} !^ban\.php$
  5. RewriteRule . ban.php [L,R]

I teraz łopatologiczne wyjaśnienie:

  1. Najpierw sprawdzamy, czy IP jest na naszej czarnej liście. - adresy są przykładowe, zamiast podanych podajemy te, które chcemy zablokować. Kropki escape’ujemy, czyli poprzedzamy odwrotnym ukośnikiem - aby kropka była interpretowana jako kropka, a nie dowolny znak. Nawiasy stanowią pogrupowanie wyrażeń, pionowa kreska - je oddziela.

  2. Sprawdzamy, czy nie banujemy przypadkiem strony z błędem. - gdyby nie sprawdzać, to całe nasze działanie nie miałoby po prostu sensu. ;) Jak pewnie zauważyłeś(aś), wykrzyknik przed wyrażeniem stanowi jego zanegowanie.

  3. Przekierowujemy klienta na stronę błędu. - chyba bez komentarza. ;]

Oczywiście rozwiązanie jest dobre tylko dla niewielkiej liczby adresów - przy większej już lepiej obarczyć tym wykonywany skrypt.

Ale jeśli chcemy wyciąć całe klasy, to nie ma problemu - możemy również użyć rewrite:

  1. RewriteCond %{REMOTE_ADDR} ^(255\.255\.255\.[0-9]+)$

Wystarczy podmienić ze wcześniejszym RewriteCond, z listingu powyżej.

Wycinanie całego IP może być bolesne dla sieci osiedlowych. Wielu adminów przychodzi nam z pomocną dłonią i wypuszcza w nagłówku X-Forwarded-For wewnętrzny adres IP. Wtedy można wyciąć tylko tego delikwenta, jeśli adres jest nam znany.

  1. RewriteEngine On
  2.  
  3. RewriteCond %{REMOTE_ADDR} ^(255\.255\.255\.255)|(0\.0\.0\.0)$
  4. RewriteCond %{HTTP:X-Forwarded-For} ^(192\.168\.0\.2)$
  5. RewriteCond %{REQUEST_URI} !^ban\.php$
  6. RewriteRule . ban.php [L,R]

Blokada przed hotlinkowaniem

Transfer jak woda - nie za darmo, rachunki też za to są naliczane. Wiele osób zamiast chociaż przegrać obrazki do siebie, linkuje bezpośrednio do naszych. Można ten proceder utrudnić (nie ukrócić).

Najprostszą metodą jest po prostu odcięcie od źródła - czyli blokowanie jakichkolwiek żądań z obcych stron. Ale czemu by nie wyświetlić zamiast tego reklamy? ;] “Uniwersalna” byłaby nie do końca OK - nie wszystkie obrazki przecież mają takie same wymiary. Proste założenie - sprawdzamy nagłówek referer, jeśli nie ma tam naszej strony - serwujemy reklamówkę. Ale w taki sposób, że pobieramy z poziomu skryptu url do właściwego obrazka, pobieramy wymiary i generujemy własny o odpowiednich. Co dalej? Inwencja autora. ;] Oczywiście nie jest to rozwiązanie stuprocentowe - wiele zapór, czy pakietów bezpieczeństwa po prostu blokuje wysyłanie nagłówka http_referer.

Ale tak, czy tak - zawsze będzie to utrudnienie. W praktyce? Sprawa jest prosta:

  1. RewriteEngine On
  2. RewriteCond %{HTTP_REFERER} !http://example\.org [NC]
  3. RewriteRule ^(.+)\.(jpg|gif|jpeg|png)$ hotlink.php?img=$1.$2 [L]

I teraz wszystko dostajemy w zmiennej $_GET[’img’]. Co teraz? A powiedzmy, że coś napiszemy. :]

  1. <?PHP
  2. if(empty($_GET['img']) OR !file_exists(basename($_GET['img'))){
  3.     die;
  4. }
  5.  
  6. $i = geimagesize(basename($_GET['img']));
  7.  
  8. if(!$i){
  9.     die;
  10. }
  11.  
  12. $img = imagecreate($i[0], $i[1]);
  13. $col = imagecolorallocate($im, 0, 0, 0);
  14.  
  15. imagestring($img, 5, 0, 0, 'prawdopodobnie skradziony obraz, oryginal pochodzi ze strony: http://example.org');
  16.  
  17. header('Content-type: image/png');
  18. imagepng($img);
  19.  
  20. ?>

Interpretacja ścieżek, innych - inwencja autora. Można również wygenerować obrazek i na niego przekierowywać, możliwości jest mnóstwo. Moim zdaniem, najlepiej wygenerować i wysłać na jakiś darmowy hosting w celu oszczędzenia transferu. :]

www, czy nie www?

To pytanie odwiecznie wzbudza kontrowersje. Ja jestem tego samego zdania, co autorzy kampani no-www - www, to nie jest żaden protokół, ludziom już od dawna się jednoznacznie kojarzy ciąg strona.com ze stroną w Sieci.

Przyzwyczajenie jednak drugą naturą człowieka i nie można takiego odciąć od naszych stron. Ciekawie robi np. last.fm, który przekierowuje na preferowaną przez autorów wersję.

Tu wystarczy prosta regułka:

  1. RewriteEngine On
  2.  
  3. RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
  4. RewriteRule ^(.*)$ http://%1/$1 [R=301,L]

Aby pozbyć się za każdym razem www w adresie. Kod zapożyczony ze strony kampanii no-www. Aby zrobić na odwrót, zmodyfikujemy lekko regułki:

  1. RewriteEngine On
  2. RewriteCond %{HTTP_HOST} !^www\.(.+)$ [NC]
  3. RewriteRule ^(.*)$ http://www.%1/$1 [R=301,L]

Jak pewnie zauważyłeś(aś), skorzystano tutaj z wartości z wyrażenia przetwarzanego w RewriteCond, tym zajmiemy się później, jak to praktycznie wykorzystać.

wymuszanie szyfrowania połączenia

Bezpieczeństwo w dzisiejszych czasach jest kwestią coraz ważniejszą. Certyfikat SSL dla strony nie jest dzisiaj już wydatkiem fortuny, jak to było jeszcze parę lat temu.

Oczywiście nie ma sensu być kimś przewrażliwionym i szyfrowanie danych dostępu publicznie jest tylko marnowaniem mocy procesora serwera. Zaczniemy jednak od włączenia SSL dla strony rejestracji/logowania użytkownika:

  1. RewriteCond %{HTTPS} off
  2. RewriteRule ^/(rejestracja|logowanie) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

I rejestracja/logowanie będzie przebiegało przez bezpieczne połączenie z serwerem. Dobrze by było wymusić wyświetlanie obrazków oraz pozostałych strony przez zwykły HTTP.

  1. RewriteCond %{HTTPS} on
  2. RewriteRule ^/(^[rejestracja|logowanie]) http://%{HTTP_HOST}%{REQUEST_URI} [R,L]
  3.  
  4. RewriteCond %{HTTPS} on
  5. RewriteCond %{REQUEST_URI} \.(gif|png|jpg|css|js)$
  6. RewriteRule (.*) http://%{HTTP_HOST}%{REQUEST_URI} [R,L]

I to tyle, jeśli chodzi o SSL.

z ukośnikiem na końcu, czy bez?

Kolejna kwestia spędzająca nieraz sen z powiek osobie, która próbuje się pozbyć ukośnika na końcu. Na początku trzeba ustalić jedną rzecz - nie da się, powtarzam - nie da się usunąć slesza na końcu ścieżki, jeśli istnieje katalog o tej nazwie. Próbowałem na wszelakie możliwe sposoby, pytałem i wniosek - nie da się (jeśli jednak się da - będzie fajnie, gdy mnie ktoś oświeci ;)). Jedyne wyjście - zaprefiksowanie nazwy katalogu i subtelne przekierowywanie żądań.

Na co trzeba uważać? Żeby nie wpaść w zapętlone przekierowanie. ;]

  1. RewriteCond %{REQUEST_URI} (.*)/$
  2. RewriteCond %{REQUEST_FILENAME} !-d
  3. RewriteRule ^([a-zA-Z\-]+)/$ /$1 [R=301,QSA,L]

I powinno być ok. ;]

częste pomyłki

Podstawowe triki już omówiłem, teraz pora na najczęstsze błędy przy przepisywaniu adresów. Większość w oparciu o problemy poruszane na Polskim Forum PHP. ;)

  1. Zamiana parametrów w RewriteRule.

    Zdarzył się jeden taki przypadek, że ktoś pisząc regułkę zamienił miejscami wzorzec z zamiennikiem. Pamiętaj: składnia RewriteRule zawsze jest następująca:

    1. RewriteRule WZORZEC ZAMIENNIK [FLAGI]

    I nigdy inna. Problem może często występować przy próbie uwzględniania spacji jako wzorca. Spacja jest znakiem rozdzielającym wzorzec z zamiennikiem i należy ją zawsze poprzedzić odwróconym ukośnikiem, aby ją uwzględnić czy to we wzorcu, czy w zamienniku.

  2. Przejście na przyjazne adresy i zamaina linków.

    Ok, regułki są, testy się udają, ale jest pewien prozaiczny problem - w kodzie linki się same nie zmienią. ;) Musisz je zmienić ręcznie na nową konwencję. Owszem, można skorzystać z buforowania wyjścia w PHP i je zamienić korzystając z wyrażeń regularnych, ale jest to nieco bez sensu. ;) Gdy już zamienisz linki, dobrą praktyką jest powiadomienie wyszukiwarek, który rodzaj odnośników powinny wyświetlać w wynikach wyszukiwarki. O tym pod koniec notki.

  3. Zasięg RewriteCond.

    Wiele osób zapomina o pewnej dość istotnej kwestii - wszystkie RewriteCond obowiązują tylko do najbliższego RewriteRule. Następne wykonają się już bezwarunkowo. Rozwiązań tego problemu jest kilka:

    1. Powtórzyć bloki z RewriteCond tyle razy, ile korzystamy z RewriteRule (przy dwóch, trzech jest znośnie, przy większej ilości - masochizm)

    2. Zmodyfikować testowane wyrażenia tak, aby zbić je do jednego (patrz: przykład z adresami IP - nie rozdzielałem adresów IP, tylko je łączyłem). Niestety, nie da się tak ze wszystkim, gdyż testowane zmienne mogą być różne.

    3. Przerzucić obsługę przepisywanych adresów bezpośrednio do skryptu sprawdzając jedynie, czy istnieją pliki/katalogi, czy nie. O tym już na koniec artykułu.

  4. Zastosowanie wszędzie tych samych wzorców.

    To właściwie problem konstrukcji/rozumienia składni wyrażeń regularnych. Rozważmy przykład:

    1. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),([a-zA-Z]+)$ index.php?set=$1&id=$2&get=$3&show=$4
    2. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),([a-zA-Z]+)$ index.php?set=$1&id=$2&get=$3&pokaz=$5
    3. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),([a-zA-Z]+)$ index.php?set=$1&id=$2&get=$3&polecamy=$6

    Pytanie było na forum - dlaczego przetwarza mi tylko pierwszą regułkę? Ależ odpowiedź jest prosta - skąd Rewrite ma wiedzieć, które regułka jest przypisana do konkretnego działu? [; Trzeba w jakiś sposób to rozróżnić:

    1. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),show$ index.php?set=$1&id=$2&get=$3&show=show
    2. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),pokaz$ index.php?set=$1&id=$2&get=$3&pokaz=pokaz
    3. RewriteRule ^index,([0-9]),([a-zA-Z]+),([a-zA-Z]+),polecamy$ index.php?set=$1&id=$2&get=$3&polecamy=polecamy

    I teraz jest ok.

  5. Stosowanie kropek we wzorcach.

    Ponownie program związany bardziej z wyrażeniami regularnymi. Kropka jest składnikiem wzorca dopasowującym dowolny znak. Wystarczy poprzedzić ją backslashem (\.) i będzie ok. :)

  6. Błędne przetwarzanie liczby argumentów

    Załóżmy że mamy podział /strona/podstrona i następujące reguły:

    1. RewriteRule (.+) index.php?strona=$1 [L]
    2. RewriteRule (.+)/(.+) index.php?strona=$1&podstrona=$2 [L]

    Co jest nie tak? Podstrony nigdy nie wywołamy w ten sposób. Dlaczego? Gdyż pierwsze wyrażenie będzie pasowało zarówno do linku z podstroną jak i tej bez. Dlatego trzeba zamienić reguły miejscami tak, aby największa liczba przetwarzanych parametrów była interpretowana na samym początku.

    1. RewriteRule (.+)/(.+) index.php?strona=$1&podstrona=$2 [L]
    2. RewriteRule (.+) index.php?strona=$1 [L]

sprytne rozwiązania dzięki mod_rewrite

Przepisywanie adresów, to nie tylko sposób na lepsze zaprezentowanie URL, ale także sposób na podniesienie funkcjonalności serwisu, czy też logiki aplikacji. Zresztą - zobacz. (;

Wydzielenie katalogu public_html

Niestety, zdarzają się nadal prymitywne hostingi, które wszystkie wgrywane przez FTP pliki wrzucają do katalogu publicznego. Jakie to niesie za sobą skutki? Chyba nie trzeba mówić - mniejsze bezpieczeństwo skryptów, trzeba pilnować newralgicznych danych.

Męcząc się z CakePHP w jego pliku htaccess natrafiłem na fajne rozwiązanie:

  1. RewriteEngine On
  2. RewriteRule ^(.*)$ public_html/$1

Jedna linijka i mamy katalog public_html. :) Wszystkie pozostałe regułki wrzucamy do osobnego htaccess znajdującego się w katalogu publicznym.

wirtualne subdomeny

Widziałeś(aś) pewnie nieraz serwisy korzystające z konwencji użytkownik.strona.pl. Nie, to nie są osobno tworzone subdomeny. ;) Z takich adresów korzysta np. BLIP. Jest tak naprawdę jedna kopia skryptu, nikt niczego przy nowej rejestracji nie podpina, wszystki dzieje się automatycznie.

Trzeba przede wszystkim zacząć od konfiguracji DNS w hostingu/na serwerze. Dana domena musi miec uaktywniony tzw. wildcard, czyli opcję, która nakazuje serwerowi użycie zawartości z głównej domeny dla każdej nieistniejącej domeny. Jeśli nie ma takiej opcji w panelu administracyjnym, wystarczy zazwyczaj mail do administratora. Jeśli korzystamy z serwera dedykowanego/własnego - modyfikujemy pliki binda oraz Apache.

Wtedy wystarczy już odpowiedni htaccess w katalogu głównej domeny:

  1. RewriteCond %{HTTP_HOST} ^([^.]+)\.example\.org [NC]
  2. RewriteRule ^(.*) http://example.org/?user=%1 [QSA]

I w parametrze $_GET[’user’] będziemy mogli zidentyfikować użytkownika na podstawie subdomeny.

jeden serwis, podobne domeny - wiele języków

Nasza firma działa międzynarodowo, serwis posiada kilka wersji językowych. Mamy domeny firma.pl, firma.com, firma.co.uk, firma.ru. Dlaczego tworzyć osobne wersje stron dla każdego języka? Nie jest to może za bardzo SEO-przyjazne, ale koncepcyjnie - da się. ;) Zawsze można to wykorzystać w innym celu:

  1. RewriteCond %{http_host} firma\.([a-z\.0-9]+)$ [NC]
  2. RewriteRule ^(.*) http://firma.com/?lang=%1 [R=301,L,QSA]

I w zależności od oddziału jesteśmy przekierowywani na stronę centrali firmy z odpowiednio ustawionym językiem. :)

przypadki beznadziejne - wyższa szkoła jazdy

Niektóre sytuacje pozornie wydają się beznadziejne, ale to nie znaczy, że nie da się ich w ogóle rozwiązać. :] Jak to mawiają, zadanie nierozwiązywalne jest tylko pozornie nie do rozwiązania - wymaga jedynie większego nakładu czasu.

przetwarzanie ciągu po znaku zapytania

Zmieniamy konwencję odnośników na stronie, ale zależy nam na szybkim uwzględnieniu tego w wyszukiwarkach. Chcemy brzydkie adresy zamienić na te ładniejsze również w wynikach wyszukiwania.

Technicznie adresy będą nadal rozpoznawane - to w większości przypadków nie będzie stanowiło problemu. Trzeba jednak powiadomić wyszukiwarki, że zmiana nastąpiła. Jeśli stary schemat adresów również polegał na mod_rewrite, nie będzie problemu. Gorzej, gdy jest to konwencja bazująca na tzw. QUERY_STRING, czyli w stylu index.php?strona=asd&dzial=xyz.

Wiele osób próbuje bezpośrednio poszukiwać odpowiedniego ciągu przez RewriteRule. Niestety, nie ma to prawa zadziałać, gdyż ciąg po znaku zapytania jest dopisywany po adresu dopiero po operacji maskowania. Trzeba więc posłużyć się inną metodą:

  1. RewriteCond %{QUERY_STRING} strona=([^&;]*)
  2. RewriteCond %{QUERY_STRING} dzial=([^&;]*)
  3. RewriteRule . /%1/%2 [R=301]

Czemu takie “dziwne” wzorce? Jest to tzw. negacja znaków, czyli innymi słowy wszystkie oprócz znaku “&” i średnika. Są to separatory ścieżki, więc trzeba wszystko wydzielić, gdyż parametrów może być kilka. W ten sposób wyszukiwarki będą wiedziały od najbliższego przeindeksowania, którą wersję adresu wyświetlić.

przepisywanie adresu ze znakami specjalnymi

O ile znaki z alfabetu plus cyfry/podkreślenia/plusy nie są problemem przy przepisywaniu, to nieraz zdarza się tak, że zachodzi konieczność skorzystania ze znaków specjalnych w adresach (patrz: adresy w Wikipedii).

Popatrzmy chociaż na nawiasy, czy spacje w adresach. W wyrażeniach regularnych są one odpowiednio znakami specjalnymi, a w Rewritingu - separatorem wzorca i zamiennika. Aby bezproblemowo ich użyć, trzeba poprzedzić je odwrotnym ukośnikiem:

  1. RewriteRule ^dzial\((a-z)+\) index.php?dzial=$1 [L]

Osobnym tematem są polskie znaki. Tu zaczynają się schody, które wynikają z pewnych (błędnych) założeń:

  • polskie znaki zawierają się w klasie [a-z] - błąd! Dla nas to oczywiste, że polskie diakrytyki mają swoje miejsce w alfabecie, ale czy wyrażenia regularne, to produkt polski? Nie - co by było np. w sytuacji Cyrylicy? Spowodowałoby to ogromny bałagan. Jednak wzorzec dowolnego znaku (kropka) pozwala na przesłanie naszych rodzimych literek dalej.

  • wszystkie przeglądarki wysyłają polskie znaki w tym samym kodowaniu - niestety - prawda jest okrutna. Jedna przeglądarka wyśle w utf-8, inna w iso-8859-2, a jeszcze inna rozbije na jakieś dziwne znaki. Trzeba sprawdzać przeglądarkę gościa i dokonywać odpowiednich przekierowań. Stąd odpada ręczne wpisywanie polskich znaków do zakresów (np. [a-ząćęłóńśźż])

Wówczas trzeba albo skorzystać ze sprawdzania przeglądarki poprzez powielenie zestawu reguł z innym kodowaniem, albo przekierowanie do skryptu i testowanie.

Uniwersalne przekierowanie dla wszystkich skryptów jest dość proste:

  1. RewriteEngine On
  2. RewriteCond %{REQUEST_FILENAME} !-f
  3. RewriteRule . index.php [L]

Po co jest ten RewriteCond? Ano po to, aby serwer normalnie wysyłał obrazki, a nie obarczał tym zadaniem skrypt. ;]

negocjacja zawartości bezpośrednio w Rewrite

Ilu łebmajstrów się napociło przy tym, aby w jakiś sposób sprawdzać, w jakim MIME wysyłać dokumenty HTMLapplication/xhtml+xml, czy text/html? Fakt - niektóre przeglądarki wprowadzają obostrzenia.

Czasem nie ma to znaczenia, a i statyczne pliki powinny mieć negocjację zawartości (np. cache pełnych stron). Owszem, można osobny skrypt, ale jaki jest sens czegoś takiego, skoro Apache jest w stanie wykonać to dość sprawnie? ;]

  1. RewriteCond %{REQUEST_URI} -f
  2. RewriteCond %{REQUEST_URI} \.htm
  3. RewriteCond %{HTTP:Accept} application/xhtml+xml
  4. RewriteRule (.+) $1 [T=application/xhtml+xml]

implementacja rzecz straszna - strona i skrypty

Rewrite, to nie tylko kwestia serwera - to także pewne różnice w konstrukcji skryptów i samych stron. Tu też można popełnić nieco błędów i to wcale nie takich oczywistych. Ale o tym później. ;]

łatwe i szybkie generowanie identyfikatorów przyjaznych dla URL

Zadanie jest niby proste: z podanego ciągu znaków (np. tytułu) mamy utworzyć identyfikator, który nie będzie encjowany (czytaj: wszystkie znaki nie zamienią się na stos procentów i liczb).

Co robimy? Polskie znaki zamieniamy na łacińskie odpowiedniki, spacje na myślniki.

  1. function ident($string){
  2.         $string = str_replace(' ', '-', $string);
  3.         $string = iconv('utf-8', 'ascii//translit', $string);
  4.         $string = preg_replace('#[^a-z0-9\-\.]#si', '', $string);
  5.         return str_replace('\'', '', $string);
  6.     }

Jak to działa? A spróbuj sam(a). ;]

łatwe korzystanie z cache całych stron

Słyszałeś(aś) pewnie o wtyczce WP-SuperCache. W wersji ekstremalnej buforuje ona całe strony tak, że do gościa trafia wersja odczytana bezpośrednio z pliku. Możemy i u siebie zrobić coś podobnego. [;

Idea jest dość prosta, wręcz łopatologiczna. Zakładam, że mamy schemat linków /dzial, strony zmieniają się bardzo rzadko. Przy każdym niezbuforowanym działaniu skryptu, cache’ujemy zawartość do pliku odpowiadającego strukturze linków, np. /dzial.htm. Kasujemy albo przy zmianie zawartości, albo przy pomocy bota uruchamianego przez crona. Jak wyglądałoby to przy Rewrite?

  1. RewriteEngine On
  2.  
  3. RewriteCond %{REQUEST_URI} [^\./\\] [NC]
  4. RewriteCond %{REQUEST_URI}.htm -f [NC]
  5. RewriteRule ^(.+)$ $1.htm [L]

I gotowe. [;

pliki CSS/obrazki

Błąd często popełniany przez początkujących - linki są skonstruowane wg konwencji katalogowej, np. dzial/strona. Na stronie głównej (nie zawsze, zależy ;)) wszystko jest ok, ale w poddziałach już znika formatowanie, obrazki również się nie wyświetlają.

Zacznijmy od przyczyny. Ustalmy jedną kwestię - przeglądarka NIE WIE, że strona używa przepisywania adresów. Łapiesz? Ok, inaczej - na stronie ścieżkę do CSS masz podaną mniej więcej tak: css/style.css. Gdy otworzysz stronę dzial, przeglądarka szuka stylu CSS w katalogu dzial/css/style.css.

Rozwiązań jest kilka, najpierw skrytykuję najgorsze. :D Mianowicie niektórzy piszą reguły tak, żeby w każdym z tych pseudokatalogów istniał plik ze stylami/obrazki. Niby wszystko działa, ale… Pozostaje jeszcze kwestia transferu. Otóż przy takim rozwiązaniu problemu przeglądarka pobiera ten sam plik OSOBNO dla każdego działu. O cache można raczej pomarzyć. :P

Natomiast jeśli chodzi o poprawne rozwiązania, to istnieją dwa:

  1. korzystanie z tagu <base />
    1. <base href="http://strona.pl" />
  2. ścieżki bezwzględne

    np. http://example.org/css/style.css albo /css/style.css

Które lepsze? Do wyboru, do koloru. ;) Osobiście preferuję drugie, a to z powodu, że stosuję czasem linki względne do podstrron. ;)

problemy z Light/Thick/Grayboxem i wyświetlaniem obrazków

Zacznijmy od tego, w jaki sposób *box w ogóle działa. Cała maszyneria podpinana jest tak, że typ otwieranej zawartości jest ustalany na podstawie rozszerzenia. Jeśli nie ma go podanego - zazwyczaj skrypt tworzy <iframe /> o sztywnych wymiarach. Czym to skutkuje - nie muszę mówić - obcięte zdjęcia, brak możliwości skorzystania z opcji galerii/pokazu slajdów.

Jeśli na stronie zdjęcia posiadają ścieżki w konwencji /galeria/zdjecia/tytul-1, to nie ma się czemu dziwić - *box nie sprawdza MIME zawartości i wrzuca wszystko do jednego wora - jakby to była zwyczajna strona. Dlaczego? Ano brak rozszerzenia pliku. Co z tym zrobić?

  • Zmodyfikować regułki tak, aby nazwy zdjęć kończyły się rozszerzeniem albo…

  • zmienić skrypt *boksa. Dodajemy własne reguły sprawdzania zawartości (np. wyrażenie regularne na URL).

Drugie rozwiązanie wymaga znajomości skryptu, w dodatku - przy najbliższej aktualizacji *boksa będziemy musieli zabrać się do roboty od nowa…

zmuszenie formularzy do korzystania z przyjaznych URL-i

Wszystko pięknie działa, adresy iście jedwabiste, ale masz formularz wyszukiwarki na stronie, szukasz, a tu w adresie paskudne ?query=fraza. Myślisz, co jest grane, przecież przepisywanie działa ok, szukanie niekoniecznie, ale na Wrzucie tak robią i jakoś działa. Ok, wyłącz JavaScript i spróbuj jeszcze raz. Też działa? No widzisz.

Zacznijmy od naprawienia zwykłego formularza. Masz flagę [QSA] na końcu regułek? ;]

  1. RewriteRule WZORZEC ZAMIENNIK [QSA]

Bez tej flagi nie zadziała. Jeśli chodzi o przyjazne URL a’la wrzuta, to bez pomocy JS nie da się osiągnąć czegoś takiego, aby zapytanie było przekierowywane bezpośrednio do URL /szukaj/fraza. Jeśli jednak jesteś uparty(a), żeby skorzystać z takiego ułatwiacza, to skorzystaj z mniej więcej takiego kodu:

  1. <form id="search" method="get" action="/szukaj">
  2.     <p><input type="text" name="query" /></p>
  3.     <p><input type="submit" value="szukaj" /></p>
  4. </form>
  5.  
  6. <script type="text/javascript">
  7. window.onload = function(){
  8.     var form = document.getElementById('search');
  9.    
  10.     form.onsubmit = function(){
  11.         window.location.replace('/szukaj/'+encodeURIComponent(form.getElementsByName('query')[0].value));
  12.         return false;
  13.     }   
  14.    
  15. }
  16. </script>

Ale to nie zmienia faktu, że nie wszyscy mają włączony JS. :P

epilog

Nad notką spędziłem parę ładnych godzin, a że ostatnio pewne osoby są łase na treść dla celów SEO, jestem zmuszony zrezygnować z Creative-Commons przynajmniej dla tej notki. Chcesz opublikować - skontaktuj się ze mną, nie będę tolerować samowolki.

Nie wszystko zdążyłem zweryfikować - komentarze i uwagi są - jak zwykle - mile widziane. :)

47 komentarzy

dopisz swój :: trackback :: RSS z komentarzami

RSS z komentarzami :: trackback

Skomentuj

Możesz używać znaczników XHTML. Dozwolone są następujące tagi: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Wszystkie komentarze przechodzą przez moderację oraz filtry antyspamowe. Nie zostanie opublikowany komentarz, jeśli:

  • Jego treść obraża kogokolwiek.
  • W treści znajdują się wulgaryzmy i słownictwo ogólnie uznane za nieprzyzwoite.
  • Mam wątpliwości co do autora wpisu (Wszelkie anonimy są kasowane - niezależnie od zawartości - wpisz prawdziwy e-mail. Jeśli usunąłem, Twoim zdaniem, komentarz niesłusznie - daj znać). Zdarza się, iż sprawdzam kim jest komentujący.
  • Zawiera jakąkolwiek formę reklamy.

Szufladka