Prettifying URLs with fake subdirectories using mod_rewrite

Lately, I have been trying to define a common basis for most of my web projects, since I often end up reinventing the wheel every time. I have tried a few PHP frameworks, but none of them tickled my fancy, but I have complicated tastes. I am known for reimplementing something from scratch rather than wasting time adapting other people’s code to my needs, and it’s often much faster too.

Therefore I have been working on JBFW, my very own PHP/Javascript framework. One of the key components of it is pretty URLS and a centralized index.php to handle most of the things.

If you access http://mysite.com/news?lang=en, the server will transparently route that to http://mysite.com/index.php?pagename=news&lang=en. At that point, index.php runs the news module if it’s present, and then loads the news template (possibly showing the result of what was done in the module, if it was called at all.) I find that it’s a very slick and modular way of handling things, as static pages only need new templates and boom, they are live, with the rest of the framework readily accessible.

The mod_rewrite configuration for such a behavior is as follows:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?pagename=$1&%{QUERY_STRING}

This means: if the requested filename is not a directory and is not a file, route the request as described (knowing something about regular expressions, or being proficient in creative swearing — which goes hand in hand with regexp — comes in very handy at this point.)

I used a similar, but coarser, approach on my own main website, http://www.nicolucci.eu. There, I even used fake subdirectories, so that http://www.nicolucci.eu/photography/book/glimpses will have it show the photography-book-glimpses template. Neat, but doesn’t work with real subdirectories. It’s not a big problem on that site, but when you need to have a separate administration section, you need real subdirectories. The problem is that, using the approach I described above, http://mysite.com/admin/login into http://mysite.com/index.php?pagename=admin/login, and while the correct function could be run in PHP by mangling the request as it’s done for the “top level” modules, it could quickly turn into a nightmare.

The solution is to add another set of mod_rewrite rules, as follows:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^((.*)/)+(.*)$ /$2/index.php?pagename=$3&%{QUERY_STRING} [L]

Yes, that’s incredibly messy, and I’m pretty sure that there is a better way to do it. However it works, and for now I’m going to concentrate on finishing the website and go back to it later. What this does is: if the request has one or more blocks ending wish a slash (the directory), followed by something else (the file name), it is routed to index.php inside that directory, using the file name as a parameter to pagename, plus the original query string as usual. The [L] at the end tells mod_rewrite: please refrain from doing any other change, this is flaky enough. This ensures that http://mysite.com/admin/login effectively calls http://mysite.com/admin/index.php?pagename=login.

Note that this is another block of RewriteCond and RewriteRule, and goes before the original one. I tried to put them together, since the conditions are the same, but after fifteen minutes of trying all combinations I gave up. I’m sure I was one attempt away from getting it right.

A very clever thing (ok I’m kidding, it’s a side effect I hadn’t fully realized but I’m glad it’s there) is that addresses such as img/logo.png are not rewritten because those files do exist. It would probably make sense to exclude common file extensions, such as image files and javascript, from this kind of mangling; or even better, make it only work when the “file name” part does not have a dot in it. I’ll find a way to do it, at some point.

To finish up, an extra little trick that can come in very handy when you want to make sure that certain files are not downloadable by anybody:

<Files ~ "\.sqlite3$">
Order Allow,Deny
Deny From All
</Files>

Make sure that there are no spaces on either side of the comma in the first line. I was quite frustrated because I kept getting the infamous error 500, and there it was.

I hope this spares someone from wasting as much time as I did with this kind of thing!