Hoe te slagen met URLs

Als je een dynamische website aan het bouwen of aan het onderhouden bent, ben je misschien wel tegen het probleem aangelopen hoe je van onvriendelijke URL's af moet komen. Misschien heb je het ALA artikel van Bill Humphries over dit onderwerp al gelezen.

Het belangrijkste verschil tussen het artikel van Bill Humphries en de oplossing die ik hier zal geven, is dat ik besloot de eigenlijke verwerking van de URL's plaats te laten vinden in een PHP script, terwijl zijn oplossing reguliere expressies in een .htaccess bestand gebruikt.

Als je liever met PHP werkt in plaats van reguliere expressies en als je je oplossing wil integreren in je dynamische PHP sites, kan deze methode misschien wel eens de oplossing voor jou zijn.

Waarom zou je je druk maken over URL's?

Goede URL's zouden een opbouw moeten hebben als /producten/autos/bmw/z8/ of /artikelen/januari.htm en niet iets als index.php?id=12. Helaas is de URL in het laatste voorbeeld een soort van URL die de meeste systemen maken. Maar zitten we nu vast aan slechte URL's? Nee.

He idee is 'virtuele' URL's te maken die er mooi uitzien en die geïndexeerd kunnen worden door zoekmachines (als je je links op die manier instelt) - in feite kunnen de URL's voor de dynamische inhoud er uit zien zoals jij het wil - en dat tegelijkertijd de statische inhoud (die misschien ook op de server staat) bereikbaar is door de daarbij horende 'echte' URL.

Toen ik mijn nieuwe site aan het bouwen was, was ik aan het kijken naar een manier om mijn URL's gebruikersvriendelijk te maken aan de hand van de volgende punten:

  1. Een gebruiker gaat naar een URL zoals www.example.com/cars/bmw/z8/
  2. De code kijkt of de ingevoerde URL overeenkomt met een bestaand statisch HTML bestand.
  3. Als dit zo is, dan wordt het bestand geladen. Als dit niet zo is, dan wordt stap 4 uitgevoerd.
  4. De string van de URL wordt gebruikt om te kijken of er dynamische inhoud is die overeenkomt met de ingevoerde URL (bijvoorbeeld in een database).
  5. Als dit zo is, dan wordt het artikel weergegeven.
  6. Als dit niet zo is, wordt een http 404 foutmelding of een zelfgemaakte melding weergegeven.

Een selectie gereedschappen

Met dit artikel heb je alle informatie die je nodig hebt om deze oplossing te gebruiken. Het is echter meer een selectie van gereedschappen dat een complete stap voor stap handleiding om tot een kant en klare oplossing te komen. Voordat je aan de gang gaat moet je jezelf ervan verzekeren dat je over de volgende dingen beschikt:

  • mod_rewrite en .htaccess bestanden
  • PHP (en een basiskennis van PHP)
  • een database zoals MySQL (optioneel)

De index doet het werk

Nadat ik over het web had gesurft en een aantal forums had gelezen, kwam ik tot de conclusie dat de volgende oplossing het krachtigste was: Alle requests (verzoeken) - sommige dingen uitgezonderd - aan de server worden doorgestuurd naar één enkel PHP script, welke de gevraagde URL behandeld en bepaald welke gegevens er geladen moeten worden, als er al wat geladen moet worden.

He doorsturen wordt gedaan door een bestand genaamd .htaccess welke de volgende commando's bevat:

RewriteEngine on
RewriteRule !\.(gif|jpg|png|css)$ /your_web_root/index.php

De eerste regel zet de rewrite engine (mod_rewrite) aan. De tweede regel stuurt alle requests door naar een bestand genaam index.PHP, behalve requests voor afbeeldingen en CSS bestanden.

(In plaats van 'your_web_root' moet je het pad naar de root van je account invoeren. Let op: het is beter iets als /home/web/ te gebruiken dan iets als http://example.com.

Je kan het .htaccess bestand in de root map plaatsen of in een sub map, maar als het in een sub-map geplaatst wordt, worden alleen requests voor bestanden en mappen in deze map doorgestuurd.

De magie binnen index.PHP

Nu we alle requests naar index.PHP doorgestuurd hebben, moet we bekijken wat we ermee doen.
Bekijk de volgende PHP code eens. De uitleg staat beneden.

//1. kijk of het bestand dat wordt aangeroepen bestaat

if(file_exists($_SERVER['DOCUMENT_ROOT'].$_SERVER['REQUEST_URI'])
and ($SCRIPT_FILENAME!=$_SERVER['DOCUMENT_ROOT'].$_SERVER['REQUEST_URI'])
and ($_SERVER['REQUEST_URI']!="/")){
$url=$_SERVER['REQUEST_URI'];
include($_SERVER['DOCUMENT_ROOT'].$url);
exit();
}

//2. Zo nee, ga verder en kijk of er dynamische inhoud is
$url=strip_tags($_SERVER['REQUEST_URI']);
$url_array=explode("/",$url);
array_shift($url_array); //de eerste is toch leeg

if(empty($url_array)){ //dit is een 'request' voor index.PHP
include("includes/inc_index.PHP");
exit();
}

//Bekijk of iets in de database overeenkomt met de 'request'
//Dit is een protype zonder werking. Plaats hier je eigen code
if(check_db($url_array)==true()){
do_some_stuff(); output_some_content();
exit();
}

//3. Ook niets in de database: Error 404!
else{
header("HTTP/1.1 404 Not Found");
exit();
}

Stap 1, regels 1-9: kijken of het bestand dat wordt aangeroepen echt bestaat:

Als eerste gaan we kijken of er een echt bestand bestaat dat overeenkomt met de URL (Dit kan een statische HTML pagina zijn, maar ook een PHP of CGI script). Als er een dergelijk bestand bestaat, dan includen we het.

Op regel 3 kijken we of het bestand bestaat door middel van $_SERVER['DOCUMENT_ROOT'] en $_SERVER['REQUEST_URI']. Als het request bijvoorbeeld www.example.com/bmw/z8/ is, dan bevat $_SERVER['REQUEST_URI'] /bmw/z8/. $_SERVER['DOCUMENT_ROOT'] is een constante die het adres van je 'web root' bevat, het pad naar de map waarin je webpagina's staan.
Opmerking: Als register_globals uit staat, wat standaard is vanaf PHP 4.2.0, dan moet je $_SERVER['DOCUMENT_ROOT'] en $_SERVER['REQUEST_URI'] gebruiken.

Regel 4 is heel erg belangrijk. Er wordt gekeken of het request niet voor index.PHP zelf bedoeld was. Als dit zo zou zijn en als er niets aan gedaan zou worden, dan zou dat een oneindige lus veroorzaken.

Op regel 5 kijkt het script of $_SERVER['REQUEST_URI'] niet slechts een "/" bevat. Dit zou betekenen dat het gewoon een request aan index.PHP is. Als deze controle niet wordt uitgevoerd, dan resulteert dit in een PHP error. (Verderop in deze tutorial komt dit nog aan bod)

Als het request langs al deze controles komt, dan wordt het bestand geïncluded en wordt het parsen van index.PHP gestopt met exit().

stap 2, regels 14-28: kijken naar dynamische content:

Als eerste zetten we $_SERVER['REQUEST_URI'] om naar een array, waar makkelijker mee te werken is.

We gebruiken strip_tags() om op HTML en Javascript code te filteren(basis beveiliging tegen hacken) en $explode() om $_SERVER['REQUEST_URI'] te bij de schuine strepen de splitsen en in een array te zetten. Als laatste gebruiken we array_shift() om de eerste entry van de array te verwijderen, aangezien $_SERVER['REQUEST_URI'] altijd met een schuine streep begint.

Alle delen van de request string zijn nu opgeslagen in $url_array. Als het request voor www.example.com/bmw/z8/ zou zijn, dan zouden $url_array[0] en $url_array[1] respectievelijk "bmw" en "z8" bevatten. Als de gebruiker de afsluitende schuine streep (trailing slash) in de URL niet is vergeten, dan bestaat $url_array[2] zonder waarde.

Hoe je omgaat met deze laatste entry hangt helemaal van jou af. Doe gewoon wat je ermee wilt doen.

Maar wat als $url_array nou leeg is? Je hebt je misschien al beseft dat dit betekent dat $_SERVER['REQUEST_URI'] slechts een slash(schuine streep) bevat, zoals ik net al heb uitgelegd.

Dit is dus het geval dat het request is bedoeld voor index.php (www.example.com of www.example.com/). Mijn oplossing was simpel; gewoon de content voor de voorpagina includen, maar je kan ook een entry uit een database laden.

Je kan nu verder gaan alle andere requests af te handelen. Dit is de taak van je creativiteit. Nu kan je de delen van de URL gebruiken om dynamische content te laden. Je kan bijvoorbeeld in je database kijken voor content die overeenkomt met de URL. Op de regels 25-28 staan een schets van wat code die hier voor kan zorgen.

Stel dat je een string hebt als /artikelen/januari.htm. $url_array[0] bevat "artikelen" en $url_array[1] bevat "januari.htm". Als je je artikelen opslaat in een tabel genaamd "artikelen" met onder andere een kolom "maand", dan zou de volgende code het goede artikel ophalen:

str_replace (".htm","", $url_array[1]);
//verwijdert .htm uit de URL
$query="SELECT * FROM $url_array[0] WHERE month='$url_array[1]'";

Je kan $url_array ook omzetten en een script aanroepen, zoals Bill Humphries in zijn artikel aangeeft. (Het script moet via include() aangeroepen worden)

Stap 3, regels 30-32: niets gevonden:

Deze laatste stap handelt het geval af waarin er noch een statisch bestand, noch dynamische content gevonden is. Dit betekent dat er een HTTP 404 error gegeven moet worden. Met PHP doe je dit met de functie header()(In het grote stuk voorbeeldcode kan je de code voor een 404 error zien.).

Pas op: hackers

Er zit een zwak punt in deze manier van werken. Zodra je gaat kijken naar een statisch bestand, dan maak je in wezen contact met het bestandssysteem van de server.

Normaliter mogen requests via het web zeer beperkte rechten hebben. Dit hangt echter af van de zorg waarmee de server is ingesteld. Als iemand bijvoorbeeld ../../../ zou invoeren of naar /.een_gevaarlijk_script zou gaan, dan zou dit ze toegang tot mappen achter je web root of de mogelijk scripts uit te voeren kunnen geven. Gewoonlijk is dit niet zo eenvoudig, maar het is nooit onverstandig deze mogelijke zwaktes na te kijken.

Het is handig HTML en Javascript (en misschien SQL) code uit de URL te filteren. HTML en Javascript kunnen eenvoudig met strip_tags() weggehaald worden. Iets anders wat erg handig is, is het begrenzen van de lengte van de URL, wat je met de volgende code kan doen:

if(strlen($_SERVER['REQUEST_URI'])>100){
header("HTTP/1.1 404 Not Found"); exit;
}

Als iemand een URI van meer dan 100 tekens probeert in te voeren, dan wordt een 404 error gegeven en dan stopt het script automatisch. Deze code kan je gewoon aan het begin van je script invoegen, samen met eventuele andere veiligheidsmaatregelen.

Hoe om te gaan met beveiligde mappen en CGI-bin

Toen ik eenmaal klaar was met deze oplossing, realiseerde ik me dat er nog een problem was. Ik heb een paar beveiligde mappen, bijvoorbeeld voor statistieken. PHP heeft een andere gebruiker en kan hierdoor niet in de map komen, als je een bestand bijvoorbeeld wil includen.

Om dit probleem op te lossen moet je voor elke beveiligde map een extra regel aan .htaccess toevoegen. In dit voorbeeld is het de map /stats/:

RewriteEngine on
RewriteRule   ^stats/.*$      -                  [L]
RewriteRule !\.(gif|jpg|png|css)$ /your_web_root/index.php

Deze nieuwe regel zorgt ervoor dat alle requests voor /stats/ niet door de RewriteRules opgevangen worden. De - betekent dat .htaccess niets met dit request hoeft te doen en [L] stopt het uitvoeren van .htaccess zodra /stats/ wordt aangeroepen. De originele RewriteRule op regel 3 blijft gewoon van kracht op alle andere requests.

Ik raad deze oplossing ook aan voor je CGI-bin map en andere mappen waarin scripts zitten die met GET queries werken.

Bronnen

PHP/MySQL

Mod Rewrite

Dit artikel is vertaald met toestemming van A List Apart Magazine en de oorspronkelijke auteur.