Svart bälte i mod_rewrite

Idag blir det lite åsikter, lite praktisk teknik, och en gammal PDF. För säkerhets skull ska jag väl säga att åsikterna här på bloggen bara är mina egna och inte nödvändigt delas av organisationer, myndigheter och liknande som jag jobbar med/för/åt/mot.

Det verkar som om riksdagens webbplats har ändrat URL:er¹ för all information som fanns i rixlex-databasen (som numera inte officiellt kallas för rixlex, utan bara ”Dokument” — mycket märkligt att bara slänga bort ett så inarbetat varumärke). Tidigare var exempelvis URL:en för upphovsrättslagen följande:


http://rixlex.riksdagen.se/htbin/thw/?%24%7BOOHTML%7D=SFST_DOK&%24%7BSNHTML%7D=SFSR_ERR&%24%7BBASE%7D=SFST&BET=1960%3A729&%24%7BTRIPSHOW%7D=format%3DTHW

Nu är den följande:


http://www.riksdagen.se/webbnav/index.aspx?nid=3911&bet=1960:729

Jag är inte imponerad. Okej, den nya håller sig åtminstone inom 72 tecken, vilket gör det åtminstone tänkbart att det går att skicka länken via mail utan att den radbryts sönder. Lycka till med att hålla den i huvudet, läsa upp den över telefon eller skriva ner den på en servett, dock. Den är fortfarande teknikspecifik (.aspx) och avslöjar onödiga detaljer om det bakomliggande systemet.

(Inom parantes är det intressant att om man går via den ”rätta” vägen, dvs www.riksdagen.se -> dokument -> Lagar -> Svensk författningssamling -> sök på ”Beteckning”, så får man samma resurs, men följande URL:


http://www.riksdagen.se/webbnav/index.aspx?nid=3911&dok_id=SFS1960:729&rm=1960&bet=1960:729

92 tecken pga upprepad information. Varför anges parametrarna dok_id och rm när de inte innehåller någon information som inte finns i bet?)

Om man försöker gå till den gamla adressen får man, tillsammans med den nya rätta länken, ett felmeddelande som går ut på att besökaren uppmanas att meddela webmastern för den webbplats länken han klickade på fanns om att länken ändrats. Ett tips: Det kommer aldrig att hända. Jag gissar att det är gjort i avsikt om att fasa ut de gamla länkarna. Ett tips till: Det kommer inte att gå. Har man en gång publicerat något på en URL är man skyldig att underhålla den URL:en tills universum imploderar.

Jag skrev om det här för ett drygt år sedan i ett sammanhang, men lade visst aldrig ut PM:en. Här är den:
Om indirekta gränssnitt till myndigheters webbtjänster

Jag försöker efterleva den här regeln själv. När jag för några veckor sedan flyttade den här bloggen till ett nytt webhotell passade jag på att se över mina gamla URL:er. Jag började med att kolla vilka URL:er som efterfrågades, genom att gå igenom webserverloggar:

$ cat access_log | awk '{ print $7, $9 }' | sort|uniq -c | sort -n
[...]
 639 /SyndicationService.asmx/GetRss 404
 702 / 200
 845 /favicon.ico 404
 891 /feed/ 200
 915 /feed/feed 304
 929 /wp-content/staffan.jpg 200
 946 /feed/atom/ 200
1082 /wp-content/themes/journalized/layout.css 200
1083 /wp-content/themes/journalized/skins/sand-skin.css 200
1087 /wp-content/themes/journalized/style.css 200
1218 /misc/quickies_of_the_day.html 200
1426 /feed/atom 304
1661 /feed/ 304
1960 /unfiled/forhandstitt-pa-lagennu-20.html/feed/ 200
2233 /feed 200
2490 /feed/atom/ 304
3261 /robots.txt 404
3969 /wp-comments-post-blahonga.php 200
4039 /wp-comments-post-blahonga.php 403

Ok, mycket vanliga accesser där. Låt oss filtrera ut alla 404:or, dvs anrop efter sidor där servern svarat ”finns inte”

$ cat access_log | awk '{ print $7, $9 }' | sort|uniq -c | sort -n
[...]
  11 /script.aculo.us 404
  13 /PermaLink.aspx?guid=ec7b7df6-3f0a-4653-9409-47adef5b4c20 404
  14 /PermaLink.aspx/b5eb341d-353b-4c95-8e3c-3451a4e5dce3 404
  16 /PermaLink.aspx?guid=e9422f27-b552-44a7-9ac0-1e8a3d03b99b 404
  18 /law/lagen.nu/1974:152 404
  19 /law/more_on_information_law.html 404
  21 /wp-comments-post.php 404
  23 /PermaLink.aspx/2b58e629-47ec-4a13-8a28-0b438a1e5d5b 404
  26 /misc/lagen.nu_now_public.html 404
  30 /default.aspx 404
  41 /PermaLink.aspx?guid=4ffa3efe-aa74-49d9-9f7a-c4f23983dc86 404
 132 /wp-comments-post.php?UNIQUE=253f7b5d921338af34da817c00f42753 404
 211 /index.atom 404
 374 /Trackback,guid,.aspx 404
 489 /misc/bra-bloggar-daliga-floden.html 404
 518 /running/still_burning_those_calories.html 404
 639 /SyndicationService.asmx/GetRss 404
 845 /favicon.ico 404
3261 /robots.txt 404

Hmm. Vissa av de här har aldrig fungerat och ska inte fungera — förmodligen dåligt skrivna sökspindlar och/eller spamrobotar som försöker. Vissa andra saker ska fungera, men gör det inte. Vi kan börja med den här:


http://blog.tomtebo.org/PermaLink.aspx?guid=ec7b7df6-3f0a-4653-9409-47adef5b4c20

Det är en rest från tiden då jag körde dasBlog. Varje blogginlägg fick en permalänk baserat på ett GUID. Enkelt, men inte så snyggt. När jag bytte från dasBlog till blosxom blev istället permalänkarna på den här formen


http://blog.tomtebo.org/misc/blog_updated.html

Mycket snyggare. Permalänken baseras nu på kategori (”misc”) och en URL-anpassad version av inläggsrubriken (”blog_updated”). Men det finns forfarande problem. Exempelvis är det en implementationsdetalj att blosxom bygger på statiska .html-filer, det ska inte behöva synas i URL:en. Och vad händer om jag vill publicera ett nytt inlägg i kategorin ”misc” med samma inläggsrubrik?

När jag bytte från Blosxom till WordPress behöll jag den här permalänkstrukturen. Inte förrän i samband med webhotellsflytten fixade jag om det till dagens moderna och supersmidiga:


http://blog.tomtebo.org/2004/05/04/blog_updated/

Här har jag tagit bort kategorin ur URL:en (ett inlägg kan tillhöra mer än en kategori, och numera använder jag snarast taggar för att klassificera inlägg), för att ersätta det med en datumstämpel. Den säger mer, och som en added bonus är den hackable. Pröva följande:

http://blog.tomtebo.org/2004/05/04/
http://blog.tomtebo.org/2004/05/
http://blog.tomtebo.org/2004/

(Vad mycket mer jag skrev på den tiden, förresten — fyra inlägg på en dag!). Att definera sin permalänkstruktur i wordpress är en enkel inställning – bara att ange en formatsträng i stil med ”/%year%/%monthnum%/%day%/%postname%/”.

Så. Mitt gamla inlägg om att uppgradera bloggprogramvara har funnits på tre helt skilda URL:er. Det är ju dåligt. Men som jag skrev: Har man en gång publicerat något på en URL är man skyldig att underhålla den URLen tills universum imploderar.

mod_rewrite to the rescue. Den här apachemodulen är suverän på att ta en URL som efterfrågas, mangla om den, och vidarebefordra förfrågningen till resten av apache.

Men det funkar bäst när den nya URL:en kan konstrueras från information i den gamla — att redirecta /1960:729 till /1960/729.html (som jag gör på lagen.nu) är en smal sak. Men hur redirectar man /PermaLink.aspx?guid=ec7b7df6-3f0a-4653-9409-47adef5b4c20 till /2004/05/04/blog_updated/?

Jo, i små steg. När jag bytte bort dasBlog var jag förutseende nog att spara en lista som listade GUIDs kopplat till inläggsrubriker. Den listan (guid-redirect.map) ser ut ungefär såhär:

104dd495-6cec-4da9-9532-27f6e7c7fc98 /index.php?category_name=music&name=nine_cancels_stockholm_gig_this_saturday
1e2d1f2d-689e-4d2e-81e4-b1a661991e22 /index.php?category_name=programming&name=wiki_as_a_documentation_tool
25552627-a3bb-4088-bdf1-7a59edf5b03e /index.php?category_name=music&name=entombed_turbonegro_show_impressions
4e142c41-5287-434d-8124-f64f74dfc905 /index.php?category_name=music&name=all_music_sites_suck
077618b1-3eeb-419b-a886-f29878f9eac5 /index.php?category_name=music&name=pet_peeve_of_the_day

(Vad mycket mer rock det var på den här bloggen en gång i tiden). Notera att högerledets permalänkstruktur inte stämmer överens med någon av de tre jag använt mig av — den här är i princip en sökning direkt i wordpressdatabasen, på nycklarna category_name och name. Man kan gå till en länk i stil med http://blog.tomtebo.org/index.php?category_name=misc&name=blog_updated och man kommer bli redirectad till rätt ställe, tack vare wordpress-pluginen Permalink redirect.

Hur använder man den här listan med mod_rewrite? Man använder RewriteMap-direktivet. Like so:

RewriteEngine On
RewriteMap guids txt:guid-redirect.map
RewriteCond %{QUERY_STRING} ^guid=(.*)
RewriteRule PermaLink\.aspx ${guids:%1|undefined.guid} [R=301,L]

För den som inte är helt hemma på grundläggande mod_rewrite-magi: Ett RewriteRule-direktiv specar hur en inkommande URL (första argumentet, här PermaLink\.aspx) ska skrivas om till en utgående URL (andra argumentet, här ${guids:%1|undefined.guid}), och hur den omskrivningen ska göras (flaggor inom hakparanteser, här R=301,L). Ett RewriteRule-direktiv tillämpas bara om eventuella föregående RewriteCond-direktiv är uppfyllda.

Så, om och endast om omgivningsvariabeln QUERY_STRING (den delen av URL:en som följer ?) är satt till något som matchar guid= följt av något, ta detta något och ersätt det med resultatet från en uppslagning i guid-redirect.map. (Eller undefined.guid om inget hittas – vilket kan hända om den lista jag sparade i dasBlog->blosxom-bytet inte är fullständig. I så fall har vi tappat information och jag har förstört webben lite grann. Mea culpa.)

Redirecten syns utåt, eftersom flaggan R=301 betyder ”gör en HTTP-redirect med statuskod 301, vilket betyder Moved Permanently”. En sökmotorspindel, bokmärkessystem eller annat som blir uppmärksammad på detta ska i teorin sluta försöka använda den gamla URL:en och gå över till den nya. Inte för att de gör det, men JAG gör iallafall rätt på MIN sida. Pfft!

Men vänta, det kommer mer! Blosxom hade ett system för att mappa bloggpostrubriker till URL:er och WordPress har ett annat. Skillnaden består främst i att blosxom konverterar skiljetecken till underscore, medans wordpress tar bort dem. När jag gjorde migreringen kompenserade jag inte 100% för detta. För vissa poster fanns det på blosxom-tiden inte ens en entydning mappning mellan rubrik och URL. Av den anledningen hade det här inlägget först en URL på formen /misc/showcenter_review.html, och senare på formen /misc/product_review__pinnacle_showcenter.html.

Så genom att gräva fram en gammal blosxomdatabackup och diffa den mot wordpressdatabasen kunde jag konstruera följande nya mappningsfil (slug-redirect.map):

...
showcenter_review                               product_review__pinnacle_showcenter
smalltalk_risk_and_success_stories              smalltalk__risk_and_success_stories
some_parts_of_microsoft_still_doesn_t_get_it    some_parts_of_microsoft_still_doesn__t_get_it
stockholm_marathon_2004_race_report             stockholm_marathon_2004__race_report
stockholm_marathon_three_weeks_left             stockholm_marathon__three_weeks_left
...

(Åååh, stockholm marathon, nu blir jag nostalgisk – tänk när man tyckte en vanlig marathon var något att vara rädd för). Svårigheten med URL-mappningen nu är att den ska bara göras för vissa URL:er – de som finns i slug-redirect.map. Alla andra ska lämnas orörda. Det fixas lätt genom följande mod_rewrite-konfiguration. Jag låter kommentarerna tala för sig själv.

RewriteMap slugs txt:slug-redirect.map
# This supports the old custom wordpress permalink structure (that was
# created to mimick the older Blosxom permalink structure) -- it's a
# lot of rules, but the only one *really* neccesary is the first one.

# Not really needed per se since the Permalink Redirect thing does this now
# (and in a better way, since it rewrites it directly to
# wordpress-defined permalink structure) -- but it is needed for the
# filename-to-post-slug-adjustment magic further down. The Permalink
# Redirect comes in at a later stage and does an externally visible
# 301 redirect to the /yyyy/mm/dd/post-slug/ style URL, so the ugly
# "index.php?category_name=foo&name=bar" urls are not directly visible
# to the user.
RewriteRule ^(.+)/([^/]+).html(/[0-9]+)?/?$ /index.php?category_name=$1&name=$2&page=$3 [QSA]
RewriteRule ^(.+)/([^/]+).html/trackback/?$ /index.php?category_name=$1&name=$2&tb=1 [QSA]
RewriteRule ^(.+)/([^/]+).html/feed/(feed|rdf|rss|rss2|atom)/?$ /index.php?category_name=$1&name=$2&feed=$3 [QSA]
RewriteRule ^(.+)/([^/]+).html/(feed|rdf|rss|rss2|atom)/?$ /index.php?category_name=$1&name=$2&feed=$3 [QSA]
RewriteRule ^(.+)/([^/]+).html/page/?([0-9]{1,})/?$ /index.php?category_name=$1&name=$2&paged=$3 [QSA]

# Additional adjustment from old Bloxsom filename based URLs to WordPress "post slug" based urls
# This is deep mod_rewrite magic
# First, see if the querystring matches the "normal"
# wordpress way of finding a post through category + name
RewriteCond %{QUERY_STRING} ^category_name=([^\&]+)&name=([^\&]+)
# then see if the name exists in the slug redirect map
RewriteCond ${slugs:%2} ^.+$
# if so then check the querystring again to populate %1 and %2
RewriteCond %{QUERY_STRING} ^category_name=([^\&]+)&name=([^\&]+)
# and rewrite it
RewriteRule ^index\.php$ index.php?category_name=%1&name=${slugs:%2}

RewriteMap kan mer än att bara slå upp saker i textfiler – Jag nämde tidigare Permalink Redirect. Hade den pluginen inte funnits hade det gått att lösa med en RewriteMap av typen prg och ett specialskrivet script som gräver fram den saknade datuminformationen från wordpressdatabasen. Det hade säkert varit lärorikt, men jag slipper ändå gärna tacksamt att behöva skriva det.

Svårare än så är det inte. Viktiga punkter att ta med sig från det här:

  1. Greppa dina loggfiler för att ta reda på vilka URL:er som efterfrågas men inte hittas.
  2. RewriteMap kan lösa alla dina problem så länge som du inte tappat bort information som behövs för att mappa gamla URL:er till nya
  3. Har man en gång publicerat något på en URL är man skyldig att underhålla den URLen tills universum imploderar.

¹) Jo, jag har hört att det numera egentligen inte finns något som heter ”URL”. Det här inlägget är web-PK nog som det är utan att jag blandar in begreppet ”URI”.

Blogguppgradering

Om du ser det här betyder det att DNS-förändringarna slagit igenom och att den nya versionen av bloggen är live. Här följer en redogörelse, främst av intresse för andra webbloggtekniknördar.

Så vad är nytt? För dig som läser det här genom RSS-flödet i stort sett ingenting, men ni som lever kvar på nittonhundratalet och fortfarande manuellt besöker webbsajter du vill läsa kanske noterar att siten bytt utseende och numera har det trista default-wordpress-utseendet. Den främsta anledningen är att jag ville slippa mecka så mycket med konfigurering och templateeditering — nu kör jag en relativt standardmässig WordPress 2.2 med extra default på allt, och använder widgetssystemet för att pimpa min sida.

Fast helt ut gick det ju inte. Jag har varit tvungen att hacka runt i php-filerna litegrann för att integrera taggar och slipa bort de värsta irritationsmomenten med Kubrik-temat. Kanske fortsätter jag och översätter den engelska text som förekommer till svenska.

I övrigt har jag hållt mängen plugins till ett minimum. Just nu använder jag:

  • Spam Karma 2 för att hålla spamrobotarna i schack
  • Disable wpautop för att hålla WordPress ifrån att lägga in okynnesradbrytningar i mina fint HTML-formatterade postningar
  • Simple Tagging för att kategorisera mina postningar lite mer kaotiskt, tillsammans med en widget för att visa ett taggmoln.
  • Permalink redirect för att hantera övergången från den gamla permalänkstrukturen (/kategori/bloggrubrik.html, ursprungligen ett arv från tiden jag körde blosxom) till den nya (/2007/06/26/bloggrubrik)

Det där med gamla permalänkar kan utvecklas — jag har bytt blogpubliceringssystem ett gäng gånger sedan jag började blogga nån gång 2003, och jag har alltid försökt att se till att gamla länkar till enskilda inlägg fortsätter att fungera, trots att den underliggande tekniken ändrat på sig. Ni vet, Cool URIs don’t change och allt det där. Mängen magiska mod_rewrite-instruktioner i min serverkonfigurering börjar bli skrämmande. Dessutom verkar mina tidigare konfigureringar drabbats av bitröta – redirect-regler som funkade förut hade gått sönder till höger och vänster. Jag vill särskilt rekommendera mod_rewrite-direktivet RewriteMap — ett superkraftfullt verktyg för att lösa ungefär allt med (och/eller skjuta sig i foten på spektakulära nya sätt).

Part 8: Finishing touches

(The last in a series of blog posts about the tech behind lagen.nu. The other parts are here: first, second, third, fourth, fifth, sixth and seventh)

URL design: Insired both by ”Cool URIs don’t
change”
and the general REST
emphasis
on sensible URL design, I designed a URL scheme for
lagen.nu that maps closely to how swedish laws are identified, and
one that is future-proof in that it hides implementation
details. Since I hope that many people will find it useful to link
to individual pages on lagen.nu, it’s very important that those
links doesn’t break.

To illustrate: the generated html files are placed in directories
named html/year/id.html. Suppose that I
just were to let Apache (or any other webserver) serve content straight
from that directory. For example, the copyright law has SFS id
1960:729, and so with this scheme the URL would be
http://lagen.nu/html/1960/729.html (or
http://lagen.nu/1960/729.html, depending on what I would
set DocumentRoot to). This would work fine, the URL’s would be
easy to understand, and people would link to all those individual pages from
all over the web.

But now suppose I wake up one day and decide to stick all data in a big
database, and build a PHP frontend? The URL’s would change, and probably be
on the form
http://lagen.nu/showlaw.php?sfs=1960:729. Again, nice
short URL, easy to understand… but now the links from all over
are broken.

So, mod_rewrite
to the rescue: With just the simple rule:

    RewriteRule ([^:]*):(.*) /html/$1/$2.html
  

, the resource found at URL
http://lagen.nu/html/1960/729.html is now also available
at http://lagen.nu/1960:729. This is even nicer to read,
futureproof, and enables someone that knows the ID of any law to
go straight to the page for that law quickly.

As a added bonus, it makes the text and XML version of the laws
easily available too: During generation, I put these versions in
sibling directories to html/, named text/ and
xml/, respectively, and then use the following rules:

    RewriteRule ([^:]*):(.*).xml	/xml/$1/$2.xml
    RewriteRule ([^:]*):(.*).txt	/text/$1/$2.txt
  

There are other parts, such as the index pages and the about
pages, where the underlying flat-file nature of the site shines
through, such as http://lagen.nu/om/me.html, but those
pages are not as likely to be linked to. Still, if I should decide
to change it at some point, I’ll probably make some mod_rewrite
based backwards compatibility hack. Also, if you want to do this
on a Win32 platform, you’re out of luck. See my previous
post
for alternatives.

Update functionality: As the body of swedish law is always
changing, I had to plan ahead of how to keep the site up to
date. New laws are usually published every Wednesday, and it’s out
of the question to download every page from Regeringskansliets rättsdatabaser
once a week. So instead, I store the highest previously-fetched
ID, and from the update routine, I try fetching laws, increasing
that ID, until I finally get a ”law not found” error. The laws can
be either ”base constitutions”, i.e. new laws, or ”change
constitutions”, laws that specify that some other, older, law
should change (think ”source code file” and ”patch”,
respectively).

If it’s a base constituion it’s pretty simple, just
download it and process it from start to finish. If it’s a change
constitution, however, we find out wich base constitution it
changes, fetch that, see if it has been updated (”patched”), and
if so, store the old versions of that law somewhere, then do the
normal regeneration process. In this way, I can, over time, build
up an archive of old law revisions, so that I can tell how the law
looked at a particular date. For now, I have only a few months
worth of history, but the value of this will grow as the time goes
on. In particular, it would be cool to be able to do CVS-style
diffs between arbitrary revisions of a law, or to be able to link
to a law as it looked at a particular moment of time.

Front page: With the information we gather during the
update process, we can build a list of recently changed laws, and
put it on the front page. Similarly, we build a list of recent new
court verdicts, and also one with site news. All these are
published in a side bar on the front page, while the main content
area is filled with a static listing of some of the most important
laws. The different parts of the side bar leads to different news
pages, which details site, law and verdict news in greater detail.

RSS feeds: Hey, it’s 2004, how could any self-respecting
new site not have a RSS feed? I generate feeds for all three news
types (site, law and verdict) using PyRSS2Gen,
a nice little lib for creating RSS 2.0 feeds. I haven’t tried them
out much, but feedvalidator says they’re OK, and they work fine in
all RSS readers I’ve tested with (although Opera tends to show the
raw HTML instead of rendering it, which probably indicates that I
should include it in some other way than just escaping it and
showing it in the description tag. Maybe it would be
useful to use a service like Feedburner for
this.

Conclusion: This marks the end of this eight-part posting. I
hope that you’ve picked up a trick or two. As should be apparent,
I am no python wizard or even a programming guru in general, but
over the years I’ve found a style of development that works for me
in a single-developer context (doing multiple-developer projects
is a totally different thing), mostly centered around the XP tenet
”Do the simplest thing that could possibly work”, or in my own
words: ”Don’t try to find the right way of doing something — if
you just work away, the right way will find you”.

Old permalinks sort of not working

It seems that there is some sort of subtle bug in my import script. If there are more than one entry per day, permalinks to the earliest item turn up an empty page. Here’s an example. The entry as such is sort-of-working, as you can see in this date view, but the permalink (and edit/delete links as well) simply turn up empty pages. The problem doesn’t manifest itself with entries created by dasblog, so I think the bug is in my script.