WordPress permalinks, directory structure and 403 forbidden response codes

So, full disclosure: I probably set JeffreyBarke.net up wrong. Or, at least not in a “typical” way. However, it’s been the way it is for a long time now and I never thought there was a problem until yesterday, when I was checking Google Webmaster Tools for crawl errors. It turns out I was getting two 403 Forbidden error codes on two pages: Code and Tools. At first, I didn’t believe the results because the pages render fine, but I checked the actual headers and, yerp, there was the 403.

Before I talk about how I got the pages to return proper 200 response codes, I want to give a little background on how JeffreyBarke.net is structured and how it got the way it is. The site is primarily powered by WordPress, which is installed in the web root. However, there are a number of directories (some legacy, some not) that are also in the web root. I have WordPress set up to use “clean URL” permalinks (achieved via mod_rewrite). Sometimes, the WordPress mod_rewrite URLs overlap with existing directories, and this is causing the 403 response codes.

JeffreyBarke.net got this way because I used to have WordPress installed in a /blog/ subdirectory and the site root was custom-powered by a mix of flat and dynamic files. I got tired of this arrangement and decided to run everything through WordPress by doing a clean install at the web root. However, I also wanted certain demos, tutorials, code and tools which couldn’t be integrated with WordPress to remain at their current URLs.

This means I have a physical directory that looks like the following excerpt:

/
	index.php
	tools
		codeigniter-encryption-key-generator
			index.php
	wp-admin
	wp-content
	wp-includes

Since there’s no index.php file in the tools directory, when you navigate to http://jeffreybarke.net/tools/, mod_rewrite routes you to the WordPress page with the tools slug. When you navigate to http://jeffreybarke.net/tools/codeigniter-encryption-key-generator/, the actual index.php file is returned.

Here’s the interesting part, though: since I disabled directory browsing in the root .htaccess file and there’s no index.php, a request for /tools/ should return a 403 Forbidden response code. I’m kind of surprised that the page renders a’tall.. If I enable directory browsing, the page doesn’t render and I get the standard Apache directory view.

Ok, enough background—how do I have my cake and eat it too?

These are the mod_rewrite rules WordPress automatically generates for my permalink structure:

# BEGIN WordPress
<IfModule mod_rewrite.c>
	RewriteBase /
	RewriteRule ^index\.php$ - [L]
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule . /index.php [L]
</IfModule>
# END WordPress

The second RewriteCond checks that the URL isn’t referring to an existing directory, which, in the case of both /code/ and /tools/, it is. It’s this second RewriteCond I want to disable, but only for specific directories.

This can be achieved by adding the following lines to the .htaccess file that contains the WordPress permalink structure:

# Fix 403 errors on existing directories; WordPress overrides.
RewriteCond %{REQUEST_URI} ^/(code|tools)/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php [L]

Internet Explorer 8: The X-UA-Compatible meta element and conditional comments

I woke up this morning to discover that Paul Irish had left a comment on one of my GitHub commits:

A user reported that this actually didnt work.. the ie=edge never jumped ie8 into standards mode..

did you see it succeed?

Hmm… this didn’t sound good. The error in question was on our HTML5 template for The Knot. We had wrapped the X-UA-compatible meta element with an IE conditional comment. I don’t remember exactly why we did this, but I think it had something to do with HTML validation.

Now, I know that HTML validation doesn’t matter (see also “The value of HTML validation”), but I do like using it as a lint tool (and yes, I prefer to just see a green favicon in the title bar). However, I also understand that pages don’t have to validate in the wild and I had never actually tested wrapping the X-UA-Compatible meta element with a conditional comment. I just assumed it would work, because, well, why shouldn’t it?

Damn. Once again, I learn the value of assumptions. It turns out that the way it’s coded doesn’t work. IE8 completely ignores the X-UA-Compatible meta element. Since I recently read about conditional comments blocking downloads, I was curious if this had anything to do with it and if the solution I had settled on for that problem would also help with this one. Voilà!

Since the download blocking also affects conditional comments around the body element and I wanted to avoid putting an empty conditional comment in the head, I moved the conditional comments to the html element. This solution also causes the conditional X-UA-Compatible meta element to be seen by IE!

UPDATE: There is one problem with this technique: it breaks Google Chrome Frame. Chrome Frame ignores IE conditional comments, so it never gets invoked. If you want your page to work with Chrome Frame and you want it to validate, send an HTTP header. If you can’t set the header, don’t wrap the meta element with conditional comments. Besides, validation doesn’t matter anyway, ;) .

<!--[if lte IE 6 ]><html lang="en-us" class="ie ie6"><![endif]-->
<!--[if IE 7 ]><html lang="en-us" class="ie ie7"><![endif]-->
<!--[if IE 8 ]><html lang="en-us" class="ie ie8"><![endif]-->
<!--[if !IE]>--><html lang="en-us"><!--<![endif]-->
<head>
	<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
</head>
</body>
</body>
</html>

Tests

  1. IE=7. Works!
  2. IE=edge. Works!
  3. IE=edge wrapped in conditional comment. Does not work!
  4. IE=edge wrapped in conditional comment with conditional comments around the html element. Works!

How to install PHPUnit with MAMP

Installing PHPUnit with MAMP’s PEAR installer is pretty straightforward, but there are a few things to note:

  1. Everything will be done through Terminal, so go ahead and launch it now. All of the commands I provide can be copied and pasted as they are.
  2. Make sure you’re using the version of PEAR that came with MAMP, not the version that came with your Mac. This means you have to use /Applications/MAMP/bin/php5/bin/pear at the prompt, not pear.
  3. PHPUnit 3.4 requires PHP 5.1.4 or later, but PHP 5.3.2 or later is recommended. MAMP 1.8.2 (the version I have installed) is running version 5.2.10. The latest version of MAMP includes version 5.3.2. So far, I haven’t noticed any problems running PHPUnit under 5.2.10.
  4. PHPUnit also requires PEAR 1.8.1. MAMP 1.8.2 ships with version 1.8.0. To check which version you have installed, type /Applications/MAMP/bin/php5/bin/pear -V.

If you need to upgrade PEAR, first use the following two commands: /Applications/MAMP/bin/php5/bin/pear channel-update pear.php.net
/Applications/MAMP/bin/php5/bin/pear upgrade pear

Now, it’s time to install PHPUnit. The PEAR channel used to distribute PHPUnit needs to be registered with the local PEAR environment and a component that PHPUnit depends upon is hosted on the Symfony Components PEAR channel. To register the channels and install, simply type: /Applications/MAMP/bin/php5/bin/pear channel-discover pear.phpunit.de
/Applications/MAMP/bin/php5/bin/pear channel-discover pear.symfony-project.com
/Applications/MAMP/bin/php5/bin/pear install phpunit/PHPUnit

PHPUnit is now installed, but to get it to run from Terminal, we need to move it into our $PATH. To do so, type: mv /Applications/MAMP/bin/php5/bin/phpunit /usr/local/bin/phpunit.

To test your install, type: phpunit --version.

You should see something like PHPUnit 3.4.15 by Sebastian Bergmann. And that’s it!

CAST()ing a sort in MySQL

I was asked to sort a table of lease data by floor in descending order today—simple, right? But after updating the query with ORDER BY floor DESC, I noticed the results were wrong. The 9th floor was always at the top and the 10th floor and above were between the second and first floors.

It was immediately obvious—the floor field was not stored as numeric data, but as character data. This struck me as odd, so I investigated the DB structure and values. The floor field was definitely being stored as character data, but why? The reason: the client wanted to store certain floors as LL.

So given this structure, how could I quickly and easily sort the floors? The answer: MySQL’s CAST function. By casting the floor field as an integer, the numeric floors would sort correctly. Even better, the character data LL would cast to 0, preserving a correct sort.

I updated the query with ORDER BY CAST(floor as UNSIGNED) DESC and obtained the desired results. Learn more about MySQL’s cast functions and operators.