Author Archives: markn

About markn

Mark is the owner and founder of Timesheets MTS Software, an mISV that develops and markets employee timesheet and time clock software. He's also a mechanical engineer, father of four, and a lifelong lover of gadgets.

    Find more about me on:
  • googleplus
  • linkedin

Bash Shell Vulnerability Found – Patching Ubuntu

The interwebz is abuzz with the latest vulnerability for *nix based systems that us the BASH shell. The so-called Shell Shock exploit allows devious users to run commands against your system using a bug in the way BASH handles environment variables. Ouch. To check if your system is vulnerable run the following in a shell:

env x='() { :;}; echo vulnerable’ bash -c “echo this is a test”

If your system is vulnerable you’re going to see this:

vulnerable
 this is a test

If your system is not vulnerable you’re going to see something like this:

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
this is a test

There’s a patch available for Ubuntu systems already. All I did was this:

sudo apt-get update
sudo apt-get install bash

And hey presto the system is no longer vulnerable.

Microsoft Word Content Controls and VBA

I’ve been developing some “smart” forms a government department in the last few months. These forms had to be developed in good old MS Word. Now, Word is not really suited to creating tightly controlled forms, but, in my case it’s all I had to work with and I’ve had to work within the limitations presented to me. The only real solution available was to use Word Content Controls and then to protect portions of the document that users shouldn’t be able to edit. I’ve managed to get things working fairly well and thought it worth sharing a bit of code I’ve developed along the way that should help anyone working with Microsoft Word Content Controls and VBA.

Data Validation

If you’re doing forms then data validation is critical. It’s pretty straightforward if you’re working with content controls if you make use of the ContentControlOnExit event. For example, here’s a bit of basic validation that ensures the value entered into a field is currency:

Private Sub Document_ContentControlOnExit(ByVal CC As contentControl, Cancel As Boolean)
  Dim sngTotalCost As Single
  Dim oCC As contentControl
  Select Case CC.Tag
    Case "sTotalCost"
      If Not validateCurrency(CC.Range.Text) Then
        Cancel = True
        Beep
        CC.Range.Select
        Exit Sub
      Else
        CC.Range.Text = Format(parseCurrency(CC.Range.Text), "$#,###0.00")
      End If
  End Select
End Sub

Public Function validateCurrency(sValue As String) As Boolean
  Dim iLoop As Integer
  Dim bReturn As Boolean
  Dim iAsc As Integer
  
  On Error GoTo errorHandler
  
  bReturn = False
  
  validateCurrency = bReturn
  
  sValue = Trim(sValue)
  sValue = Replace(sValue, "$", "")
  sValue = Replace(sValue, ",", "")
  
  If Len(sValue) = 0 Then
    validateCurrency = True
    Exit Function
  End If
  
  For iLoop = 1 To Len(sValue)
    iAsc = Asc(Mid(sValue, iLoop))
    
    If iAsc = Asc(".") Or (iAsc >= Asc("0") And iAsc <= Asc("9")) Then
    
    Else
      Exit Function
    End If
  Next iLoop
  validateCurrency = True
  Exit Function
errorHandler:
  MsgBox "An error has occurred" & vbCrLf & "Module: ThisDocument" & vbCrLf & "Procedure: validateCurrency" & vbCrLf & "Error Number: " & Err.Number & vbCrLf & "Description: " & Err.Description, vbOKOnly
  Err.Clear
End Function

Public Function parseCurrency(sValue As String) As Single
  Dim iLoop As Integer
  Dim iAsc As Integer
  On Error GoTo errorHandler
  parseCurrency = 0
  
  sValue = Trim(sValue)
  sValue = Replace(sValue, "$", "")
  sValue = Replace(sValue, ",", "")
  
  If Len(sValue) = 0 Then
    parseCurrency = 0
    Exit Function
  End If
  
  For iLoop = 1 To Len(sValue)
    iAsc = Asc(Mid(sValue, iLoop))
    
    If iAsc = Asc(".") Or (iAsc >= Asc("0") And iAsc <= Asc("9")) Then
    
    Else
      Exit Function
    End If
  Next iLoop
  parseCurrency = Round(CSng(sValue), 2)
  Exit Function
errorHandler:
  MsgBox "An error has occurred" & vbCrLf & "Module: ThisDocument" & vbCrLf & "Procedure: parseCurrency" & vbCrLf & "Error Number: " & Err.Number & vbCrLf & "Description: " & Err.Description, vbOKOnly
  Err.Clear
End Function

In this code we simply check the tag of each content control as users move to the next one. Content control tags are set on the Developer tab from within Word. If the control has a tag we’re interested in then the value (Range.Text) is run through the ValidateCurrency function. If it is valid then the parseCurrency function is used to format the value correctly and write it back to the content control contents. If the value entered isn’t valid then a user alert is raised and the focus is returned to the content control

Excel Style “Automatic” Calculations

If you’ve got data validation sorted out it’s a simple step to have read only fields in your forms whose values are derived from user entered fields. This bit of code takes the value of a currency field, multiplies it by 1.1 and writes the value into a second field.

Private Sub Document_ContentControlOnExit(ByVal CC As contentControl, Cancel As Boolean)
  Dim sngTotalCost As Single
  Dim oCC As contentControl
  Select Case CC.Tag
    Case "sTotalCost"
      If Not validateCurrency(CC.Range.Text) Then
        Cancel = True
        Beep
        CC.Range.Select
        Exit Sub
      Else
        CC.Range.Text = Format(parseCurrency(CC.Range.Text), "$#,###0.00")
      End If
      Set oCC = ActiveDocument.SelectContentControlsByTag("sTotalCost").Item(1)
      sngTotalCost = parseCurrency(oCC.Range.Text)
      
      Set oCC = ActiveDocument.SelectContentControlsByTag("sTotalCostGST").Item(1)
      With oCC
        .LockContents = False
        .Range.Text = Format(sngTotalCost * 1.1, "$#,###0.00")
        .LockContents = True
      End With
  End Select
  Set oCC = Nothing

End Sub 

It’s pretty simple to see what’s happening here. The value in the content control with the tag sTotalCost is validated for currency, and if a correct value the number has a calculation applied to it and the resultant value is written to a second content control with the tag sTotalCostGST. Note that I use the LockContents method to be able to write to the second content control and then use it again to make it read only.

Changing Document Format Based on Content Control Value

The last thing I’ll share is using the value in a content control to change some format in a Word document. In this case I simply set the font color of the content control based on the value selected in the control

Private Sub Document_ContentControlOnExit(ByVal CC As contentControl, Cancel As Boolean)
  Dim sngTotalCost As Single
  Dim oCC As contentControl
  Select Case CC.Tag
    Case "sTag1", "sTag2", "sTag3"
      If CC.Range.Text = "Yes" Then
        CC.Range.Font.ColorIndex = wdGreen
      End If
      If CC.Range.Text = "No" Then
        CC.Range.Font.ColorIndex = wdRed
      End If
  End Select
  Set oCC = Nothing
End Sub

These content controls were of the dropdown list type. When “Yes” is selected the font color is set to green with the ColorIndex property. When set to “No” the Font.ColorIndex is set to Red. Pretty simple.

How to Always Know the IP Address of your Home or Office Modem

A lot of home broadband plans in Australia do not offer a fixed IP address (I am not sure about plans in other countries). This can be troublesome if you want to get remote access to your home PCs with something like TightVNC or if you want to serve a little web page off of your home computers. It can also be a pain in the ass if you want to lock down access to remote servers based on IP address. I’ve worked up a little system of my own than allows me to work out what the currently assigned IP address is for my modem at home regardless of where I am in the world. Doing the same yourself is surprisingly simple. All you need is:

  • A home based server running PHP that can run timed jobs with CRON or some equivalent windows based program.
  • A remote web server running PHP.

The system uses three php files and one text file.

File 1 : check-and-update-ip.php

The first, which I’ll call check-and-update-ip.php resides on your local home server and is executed periodically either with CRON or some equivalent windows program. I’ve got my file to run every 30 minutes, so worse case if I need to access my home network and the assigned IP address has just changed I’ll have to wait a maximum of 30 minutes for the update. Here’s the contents of check-and-update-ip.php

<?php
// check-and-update-ip.php
//
// This file calls a file on the remote server which returns the $_SERVER['REMOTE_ADDR'] value.
// This value is compared with a stored value and if different written back to a stored value
  $remote_host="http://remote-host-address.com/file-location";
  $current_remote_ip=file_get_contents($remote_host."/remote-ip.php");  
	
	$last_remote_ip=file_get_contents($remote_host."/last-ip.txt");
	
	if ($current_remote_ip<>$last_remote_ip)
  {
			echo $remote_host."/write-ip.php?ip=".$current_remote_ip;
	  	$write_ip=file_get_contents($remote_host."/write-ip.php?ip=".$current_remote_ip);
			
			echo $write_ip;
	}
	else
	{
		echo "external ip address has not changed since last check";	
	}
?>

This file gets the returned contents of remote-ip.php from the remote server. This file simply returns the super global $_SERVER[‘REMOTE_ADDR’], compares it with the remotely stored contents of last-ip.txt and if different calls write-ip.php to record the change.

File 2: remote-ip.php

This file resides on the remote web server and contains just one line of PHP.

<?php
// remote-ip.php
//
// This file simply echoes back the remote client IP
echo $_SERVER['REMOTE_ADDR'];
?>

Of course this is just echoing back the client address, which in this case happens to be the IP address assigned to my home router by my ISP.

File 3: write-ip.php

This file is responsible for writing IP address changes to last-ip.txt.

<?php
//write-ip.php
//
//This file takes one parameter in correct IP format and writes it to a text file.  Incorrect parameter formats are rejected.

  if (isset($_GET['ip']))
		$new_ip=trim($_GET['ip']);
	else
	{
		echo "no IP parameter";
	  die();
	}
		
	if (strlen(trim($new_ip))==0 || strlen($new_ip)>15 )	
	{
		echo "invalid IP parameter";
		die();
	}	
	$quartet_array=explode(".",$new_ip);
	
	if (count($quartet_array)!=4)
	{
		echo "invalid IP parameter";
		die();		
	}
	
	if (!is_numeric($quartet_array[0]) || !is_numeric($quartet_array[1]) || !is_numeric($quartet_array[2]) || !is_numeric($quartet_array[3]))
	{
		echo "invalid IP parameter";
		die();	 		
	}
	
	
	$file_name="/server/path/to/last-ip.txt";
  $file_handle=fopen($file_name,'w') or die('cannot open file');
	
	$contents=$new_ip;
	
	fwrite($file_handle,$contents);
	fclose($file_handle);
	
	echo $new_ip. " written to last-ip.txt";
?>

This script expects one parameter in normal 4 quartet IP address format (XXX.XXX.XXX.XXX). Any other parameter format is rejected. If the parameter format is correct then the parameter is written to last-ip.txt.

File 4: last-ip.txt

This simple text file is an empty text file that can be uploaded to your remote server. You’ll need to chmod 777 it to make it writeable by write-ip.php. And finally we get to useful bit, it’s this file that you can access from any web browser by typing the file location into your browser, something like:

http://www.remote-host-address.com/path/to/last-ip.txt

And hey presto you’ll see the IP address of your modem at home. You can then use that IP in your remote desktop tool, or just type it into your browser if you’re hosting a website from home. I also use the text file as an include in other remote PHP files to provide IP based exceptions.

How I Reload my iptables Rules on Ubuntu Reboot

There’s a few ways that custom iptables rules can be re-loaded when your Ubuntu server reboots. I’ve chosen to reload mine using the /etc/network/interfaces file. Here’s what I’ve included in that file:

auto lo
iface lo inet loopback
pre-up iptables-restore < /etc/iptables.firewall.rules
auto eth0
iface eth0 inet dhcp

The key line here is the line starting with pre-up. This directs the iptables-restore to reload my rules from the /etc/iptables.firewall.rules file.

Another way of accomplishing the same thing is to create a script file in the /etc/network/if-pre-up.d/ directory and put the following in it:

#!/bin/sh
/sbin/iptables-restore < /etc/iptables.firewall.rules

Then set the permissions on the script file with:

sudo chmod +x /etc/network/if-pre-up.d/your-filename

WordPress Posts with Identical Dates

I’m building a catalog site in WordPress for one of my collections. The catalog currently exists in a large Excel spreadsheet and I’ve been usng it to create a CSV file that can be imported using the WP CSV plugin. This greatly speeds up the creation of the content for the catalog. My plan is that each item in my catalog is a post and the CSV file contains all of the information for each post. One of the imported values in the CSV file includes is the post date. I imported each item with an identical post date without giving it too much thought. This is because the catalog site isn’t going to actually display any post dates. Anyway, it turns out that this was a bad idea because the Next Post and Previous Post links in the single.php template will not bring up any of the posts with the same post date. A bit of digging about and I’ve worked out why.

My single.php theme file contains calls to previous_post_link() and next_post_link() which are supposed to render the next/previous links. I drilled down into these functions which can be found in the /wp-includes/link-template.php file. Both functions call the function get_adjecent_post(). Looking at that a bit closer it includes a bunch of SQL criteria code (for selecting posts from the same taxonomy for example) and, as you’d expect it creates a date criteria clause. That bit of php looks like this:

$adjacent = $previous ? 'previous' : 'next';
 $op = $previous ? '<' : '>';
 $order = $previous ? 'DESC' : 'ASC';

Those little ‘<' and '>‘ symbols give the answer why my imported posts with identical post dates don’t display in the next/previous post links. The date criteria to select the next/previous post is “greater than” or “less than” and obviously if the post dates are identical they are NOT greater than or less than. So, the solution is to NOT import posts with identical post dates. Unfortunately I’d already done this so I opened up the wp_posts table in MySQL workbench and edited the post date of each post I’d imported. Next time I do a bulk import I’ll make sure to increment the post date column values in the CSV BEFORE I do the import.

Sorting WordPress Category Archives

By default WordPress category archives are sorted by date.  This is fine when you’re publishing a blog but if you’re using WP for other reasons you might want to adjust the way the archives are sorted.  This is done rather easily by creating a new filter in your functions.php file like this:

//filter to alphabetically sort category archives
add_filter('posts_orderby','sort_category_archive_alphabetically');

And then creating a new sort function like this:

function sort_category_archive_alphabetically($orderby )
{
 if (is_category())
 {
 return "post_title ASC";
 }
 return $orderby;
}

In this example I’m alphabetically sorting all categories.  You could restrict it to a specific category by changing the is_category() call to include a category ID (is_category(8)) or by including a category name (is_category(‘Your Category Name’)).

I’m doing a simple sort by post_title here but more complex sorts are possible.  For example, the WordPress API gives a more complex example that explains how to do a LEFT JOIN in the SQL to select posts and sort by values in tables other than the WP_POSTS table.

Default Image Captions in WordPress

I’m building a numismatic reference website in WordPress for the Perth Numismatic Society that is eventually going to list several thousand items along with images. The images will be presented as thumbnails which can be clicked on to show larger images using the Responsive Lightbox plugin. The client wanted a default caption under each image instructing users to click on the image to view a larger one. I honestly thought adding a default image caption would be easy but it turns out it isn’t. Nothing I Googled up actually worked including this example in the WordPress Codex or this plugin. So I actually had to use the old grey matter and work it out myself.

So I had a poke around the and ended up in the /wp-admin/includes/media.php file and found this function and filter:

function image_add_caption( $html, $id, $caption, $title, $align, $url, $size, $alt = '' )
{
//function code removed
}
add_filter( 'image_send_to_editor', 'image_add_caption', 20, 8 );

Ahah, simple. Remove the existing image_add_caption filter and replace it with my own. So, I’ve grabbed that code and added it and a couple of other things to my child theme functions.php file as:

remove_filter( 'image_send_to_editor', 'image_add_caption');
add_filter( 'image_send_to_editor', 'add_default_caption', 100, 8);
function add_default_caption( $html, $id, $caption, $title, $align, $url, $size, $alt = '' ) {

	/**
	 * Filter whether to disable captions.
	 *
	 * Prevents image captions from being appended to image HTML when inserted into the editor.
	 *
	 * @since 2.6.0
	 *
	 * @param bool $bool Whether to disable appending captions. Returning true to the filter
	 *                   will disable captions. Default empty string.
	 */
        if (substr($html,0,strlen('([0-9]+)/', $html, $matches ) )
		return $html;

	$width = $matches[1];

	$caption = str_replace( array("rn", "r"), "n", $caption);
	$caption = preg_replace_callback( '/<[a-zA-Z0-9]+(?: [^<>]+>)*/', '_cleanup_image_add_caption', $caption );
	// convert any remaining line breaks to <br>
	$caption = preg_replace( '/[ nt]*n[ t]*/', '<br />', $caption );

	$html = preg_replace( '/(class=["'][^'"]*)align(none|left|right|center)s?/', '$1', $html );
	if ( empty($align) )
		$align = 'none';

	$shcode = '' . $html . ' ' . $caption . '';

	/**
	 * Filter the image HTML markup including the caption shortcode.
	 *
	 * @since 2.6.0
	 *
	 * @param string $shcode The image HTML markup with caption shortcode.
	 * @param string $html   The image HTML markup.
	 */
	return apply_filters( 'image_add_caption_shortcode', $shcode, $html );
}

You can see I start by removing the existing image_add_caption filter but it turns out this is an utter waste of time because the filter executes even though I’ve removed it. So I’ve got around this by declaring the new filter with a lower priority than the existing one (so it happens AFTER) the existing filter. And then checked the $html to see if it starts with the caption shortcode. That’s this line:

if (substr($html,0,strlen('[caption'))=='[caption')
 return $html;

So, if a caption has already been applied we just exit from our new filter.  However, if there’s no caption we can now apply our default caption like this:

if (empty($caption))
 $caption="Click Image to Enlarge";

Uninstall Surveys

There was a post on the almost dead Business of Software forum last week from a poster who had recently implemented an ‘uninstall survey’ in his try-before-you-buy software product. An uninstall survey is triggered when a software product is uninstalled from the host operating system. It usually takes one of two forms, the first is a small program that asks some questions and then emails the results or posts them to a website. The second (and far more common) type of survey is triggered by popping up a webpage on the software product website.

But, I digress, the main point of the post was that the number of responses the poster was receiving was small and the number of useful responses was even smaller. I’ve included an uninstall survey with my product Time Clock MTS for several years and this mirrors my experience. I’m lucky to see a 10% response rate (10% of total downloads) and of those, not more than 1 in 10 or 20 actually contains useful information. At first glance it seems to be hardly worth the effort but the hour or two it took to implement has probably seen an ROI measured in thousands of percent. It has allowed me to save the occasional sale by responding to a user who complained of missing features in my product that were not missing at all. It has allowed me to find holes in my documentation and fill them, it has highlighted certain software features I was missing and that respondents found useful, and it has helped me fix up problems with the process flow in the software.

My experience certainly pours water on the comments of one poster on the BoS forums who suggested:

Indirectly related, the existence of an uninstaller often implies a poor architecture, such as apps that stuff random files and overwrite libraries in many places.

This is an insanely negative attitude. Knowing that a product is not perfect and can be improved is the ONLY way of being able to build a better product. And the best way to build a better product is to establish channels of communication with your customers. The software uninstall survey is one channel that is virtually free to establish and provides a means of communication that, in the case of downloadable trial software, would otherwise be impossible. Consider that a trial version of my software can be downloaded, installed, and trialed all without the user having to email me, ask me for a registration key, or even make me aware that they are using the software. Giving the user the opportunity to communicate with me once they have decided to uninstall (and presumably not purchase) my software has proven to be invaluable.

If you’re wanting to implement an uninstall survey, you sell Windows software and you’re using the very nifty (and free) Inno Setup then it’s dead simple. Just add an URL to the [INI] section of your Inno Setup script like this:

[INI]
Filename: {app}UninstallSurvey.url; Section: InternetShortcut; Key: URL; String: http://www.some-domain.com/uninstall-survey.htm

And then add an [UninstallRun] section to your script that opens the URL.

[UninstallRun]
Filename: {app}UninstallSurvey.url; Verb: open; Flags: shellexec

In my case I open an URL to present the user with questions. But there’s no reason why the [UninstallRun] section couldn’t run a small executable file that pops a window up to the user asking them the same questions. Those results could either then be emailed or POST’ed to your website. For your reference here’s the Time Clock MTS Uninstall Survey. It used to have a lot more questions but I’ve simplified it greatly based on the response of one poster in the BoS thread I referenced in the first paragraph. Respondents simply fill in the details and I receive an email contain the responses and a couple of other bits of information such as the users IP address and user-agent. The user-agent is particularly useful so that I know what version of Windows the respondent is using.

There you have it, uninstall surveys. Quick and easy to implement, zero on-going costs, and they provide you with a communication channel to software users that is unlikely to exist otherwise. If you’re selling try-before-you-buy software you’d be crazy to not be using them.

Setting up Apache Permissions for www-data and an FTP User

A problem I have when setting up a website on a new server (or a new site on an existing server) is sorting out permissions for the Apache user (www-data) and an FTP user. I can never quite remember how to set things up so that I am not continually needing to log into a console to adjust permissions so I can FTP some files to the server. This post is the definitive reminder to myself showing how it should be done. This example applies to Ubuntu but I guess it is equally applicable to other flavours of Linux.

Add the FTP User to the www-data Group

First thing we want to do is add the FTP user (you have created an FTP user haven’t you?) to the www-data usergroup. www-data is the user/group used by Apache.

sudo adduser ftp-username www-data

Change Ownership of Files

The next step is to set the www-data group as the owner of all files and directories in the HTML source directory.

sudo chown -R www-data:www-data /var/www

Grant Group Permissions

Now we want to add write permission for the www-data group for all the files and directories in the HTML source directory.

sudo chmod -R g+w /var/www

Add umask for New Files

The final step is to make a change to an Apache configuration file so that the umask for new files created by Apache is such that the www-data group has write permissions on them. Open /etc/apache2/envvars in your text editor of choice and add this to the bottom of the file:

umask 007

The three octal digits for umask are for the Owner/Group/Others. The 0 leaves permissions unmasked (ie left at read/write/execute) and 7 gives no permissions at all. This would be equivalent to chmod 770. There’s a useful chart here showing the relationship between the binary rwx permissions and the octal numbers used by chmod and umask.

Credit for this must go to the top voted answer to this question on askubuntu.com.

WordPress, Responsive Lightbox with FancyBox and IE6, IE7, and IE8

A week or so back the client for a website I am developing in WordPress reported that the site was slow to load in Internet Explorer 8. At the time I confirmed the problem existed (using a Virtual Box VM with XP/IE8) but didn’t fix the issue. I’ve only just gotten around to looking at the actual source of the problem today. First up I downloaded and installed the Fiddler Web Debugger onto the VM so I could see what was going on when the site loaded in IE8. Here’s what displayed at the bottom of the Fiddler log:

Fiddler Log

Fiddler Log

A number of PNG images associated with FancyBox have failed to load which explains the delay in loading the site in IE8. Immediately I was thinking two things. First, it’s got something to do with the Responsive Lightbox plugin I am using for LightBox image effects, and secondly, there’s some conditional CSS classes that clearly are not working properly because the site loads fine in all other browsers. A quick peek at the source for the site shows that the FancyBox css file is here:

'/wp-content/plugins/responsive-lightbox/assets/fancybox/jquery.fancybox-1.3.4.js

I loaded that file up in my text editor and sure enough at the bottom of the file I see this:

#fancybox-loading.fancybox-ie6 div	{ background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_loading.png', sizingMethod='scale'); }

/* IE6, IE7, IE8 */

.fancybox-ie .fancybox-bg { background: transparent !important; }

.fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_n.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_ne.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_e.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_se.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_s.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_sw.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_w.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_nw.png', sizingMethod='scale'); }

They look like helper classes for early versions of Internet Explorer that don’t support CSS3 fully. The AlphaImageLoader filter allows images with alpha channel transparency to be loaded in IE6/7/8 while newer CSS3 compliant browsers can display them natively without the need for such tedious mucking about in CSS files. A bit of searching suggests that the DXImageTransform.Microsoft.AlphaImageLoader parameter src uses a path relative to the page the CSS is loaded in rather than relative to the location of the CSS file itself. So, in IE6/7/8 if a page is “foo.htm” then these CSS classes are trying to load “http://www.mysite.com/fancybox/fancy_shadow_n.png” but in reality the image is buried deep in the WordPress content folders.

I could have hardcoded the image locations into the calls but of course this would just break again if I moved the site to a different host. So for now, the fix was easy enough, just comment out that part of the CSS, upload the file back to the web host and hey presto the site loads perfectly in IE8. Of course, when a LightBox image is displayed in IE8 it doesn’t have pretty borders but that’s a minor problem I can live with.