Linux is a case-sensitive operative system. PHP’s file_exists function uses the underlying operating system to look for a file, so if you’re on Linux file_exists is case sensitive. For example, if you have a file at /home/jdoe/foobar.txt, then file_exists('/home/jdoe/FOOBAR.txt'); would return false. If you’ve ever wanted to do a case-insensitive lookup in a Linux environment you know that PHP doesn’t have any built-in support for that (understandably).

Below is a work-around. It’s not efficient and it does something I hate to do in PHP scripts: it uses a shell command. As a result, if you want to use this you need to be really, really, really, really careful about cleaning up any untrusted input that’s going into the backticks. escapeshellarg and escapeshellcms would be great places to start.

The disclaimer

This is a hacked down version of something I wrote for a personal project. That is, the code below hasn’t been tested. Next, it is really inefficient, especially if called without the second argument. I wouldn’t recommend using this in a frequented or publicly available page.

The code

function nfile_exists($file, $tld = '/') {
    /* change this to the path to your "find" command, if it's not /usr/bin/find */
    $find = '/usr/bin/find';

    /* this gets the filename out of the path */
    $fname = preg_replace('@^.*/([^/]+)$@', '\\1', $file);

    /* this uses "find" to look for the filename using case-insensitive matching */
    $cmd = sprintf('%s %s -iname %s',
        $find, escapeshellarg($tld), escapeshellarg($fname)
    );

    /* sticks the results of find into the results array */
    $results = explode("\n", `$cmd`);
    foreach ($results as $k => $r)
        if (!$r)
            unset($results[$k]);

    /* looks to see which of these, if any, match */
    foreach ($results as $r)
        if (files_match_ncase($r, $file))
            return $r;

    return FALSE;
}

function files_match_ncase($f1, $f2) {
    $dirchain1 = explode('/', dirname($f1));
    $dirchain2 = explode('/', dirname($f2));

    if (count($dirchain1) !== count($dirchain2))
        return FALSE;

    $lim = count($dirchain1);
    for ($loop = 0; $loop < $lim; $loop++)
        if (strcasecmp($dirchain1[$loop], $dirchain2[$loop]) !== 0)
            return FALSE;

    return TRUE;
}

Usage

Call nfile_exists the same way you would call file_exists. You can (optionally) specify the top-most part of the path structure where case is known. For example:

<?php

echo nfile_exists('/home/jdoe/some/obscure/path/foobar.jpg', '/home/jdoe');

?>

The code above will only match the file specified if it exists in /home/jdoe, but it will not catch a file in /home/JDOE. This makes nfile_exists a lot faster, because essentially it will start the case-insensitive search at /home/jdoe instead of at /, which would mean having to search every file on the system!

I haven’t taken the time to do this, because I didn’t need to for my project, but this could pretty easily be improved by changing the algorithm in nfile_exists as such: instead of find <path> -iname <filename>, which will search every directory under <path> for that file, you could break the file you’re looking for up into individual directories, and perform a find <path so far> -type d -iname <directory name> -maxdepth 1. This way you’re just going one directory at a time looking for the next directory, and ultimately the file. This is much more precise and efficient.

Calling nfile_exists without specifying the second argument, which is the top-most directory where case is known, is very inefficient. Without the second argument, the system will search every file on the machine until it finds one with your file name, and then sees if they have the same paths. Ideally, you always want to specify the top level directory.