Converting Relative Paths to Absolute with ColdFusion

For a project I'm currently working on, I need to convert relative paths to their absolute equivalent. The paths in question could be for anything: images, JavaScript files, CSS files, etc.

What I want is input of: convertRelPathToAbs('../../../assets/style.css') to output: /myApplication/assets/style.css, assuming that those paths are equivalent, of course.

Because we don't know what to expect, the first thing I want to do is pass-through any external files or paths that are already absolute.

private string function convertRelPathToAbs(required string pathIn)
output="false"
{  
    var local = structNew();

    //if the path is already absolute, just pass-through
    if (left(arguments.pathIn, 1) eq '/'){
        return arguments.pathIn;
    }

    //if the path is external, just pass-through
    if (lcase(left(arguments.pathIn, 4)) eq "http"){
        return arguments.pathIn;
    }

Next, we need to get some contextual information. Namely, we need the absolute path of the currently-executing template. This is important because it is how the browser evaluates the relative paths encoded in the HTML. If your page is at http://domain.com/folder/1/2/index.cfm and requests ../assets/style.css, then the browser doesn't request ../assets/style.css, it requests http://domain.com/folder/1/assets/style.css.

In order to make that same conversion in our code, we need the absolute path of the currently executing template and the relative path that needs to be converted. The rest is string comparison and list operations.

    /**************************************************/
    /**** else... the path is relative, convert it ****/
    /**************************************************/

    //get the absolute path of the current request to base relative paths from
    local.result = getDirectoryFromPath(CGI.SCRIPT_NAME);

Now, start an infinite loop (don't worry, we'll stop it later!):

    while(true){

For each iteration of the infinite loop, get the first portion of the path, using a forward-slash as a list delimiter.

        local.listTop = listGetAt(arguments.pathIn, 1, '/');

Then, handle different possible values. If its value is just a single period, then that means to use the current folder. We don't need to change anything about our base absolute path.

        if (local.listTop eq '.'){
            //current directory, just ignore

Then, handle the value ../. This value means we want to go up one level in the directory structure. To do that, we'll delete the last element in the base absolute path.

        }else if (local.listTop eq '..'){
            //up 1 level in the tree, if possible
            if (listLen(local.result, '/') gt 0){
                local.result = listDeleteAt(
                    local.result, 
                    listLen(local.result, '/'),
                    '/'
                );
            }

Somewhat simplified, this is referred to as traversal -- we're traversing the path.

The last case we want to handle is something other than a ../ or a ./, which would indicate a folder name. At this point in the relative path, we've stopped going backwards in the directory tree and we want to start going forward. For our purposes, we just need to stop the loop.

        }else{
            break;
        }

The last thing we need to do within the loop is to drop the first item from the relative path (the input) so that the relative path can be appended to the absolute path later and not contain the ./, and close the loop.

        arguments.pathIn = listDeleteAt(arguments.pathIn, 1, '/');
    }

The above code changes ../../assets/style.css to ../assets/style.css, for example.

There's one last thing we need to account for in the function. There is no leading slash at the beginning of the relative path, which is problematic if traversal takes us to the root of the domain, because the absolute path should start with a slash. We'll start by adding that slash to the beginning of the resulting relative path:

    if (left(arguments.pathIn, 1) neq "/"){
        arguments.pathIn = "/" & arguments.pathIn;
    }

However, when we do this paths where traversal did not go to the root of the domain now contain double slashes, like so: /myApp/1//assets/style.css. Most browsers will handle this just fine, but it's easy enough to fix, so we should. To do so, we just need to remove any trailing slashes from the base absolute path, if they remain:

    if (len(local.result) gt 0 and right(local.result, 1) eq '/'){
        local.result = left(local.result, len(local.result) - 1);
    }

Lastly, return the combination of the modified base absolute path and the modified input:

    return local.result & arguments.pathIn;
}

When you put it all together, this is what you end up with:

private string function convertRelPathToAbs(required string pathIn)
output="false"
{  
    var local = structNew();

    //if the path is already absolute, just pass-through
    if (left(arguments.pathIn, 1) eq '/'){
        return arguments.pathIn;
    }

    //if the path is external, just pass-through
    if (lcase(left(arguments.pathIn, 4)) eq "http"){
        return arguments.pathIn;
    }

    /**************************************************/
    /**** else... the path is relative, convert it ****/
    /**************************************************/

    //get the abs path of the current request to base rel paths from
    //local.baseAbsPath = ;
    local.result = getDirectoryFromPath(CGI.SCRIPT_NAME);
    while(true){
        local.listTop = listGetAt(arguments.pathIn, 1, '/');
        if (local.listTop eq '.'){
            //current directory, just ignore
        }else if (local.listTop eq '..'){
            //up 1 level in the tree
            if (listLen(local.result, '/') gt 0){
                local.result = listDeleteAt(
                    local.result, 
                    listLen(local.result, '/'),
                    '/'
                );
            }
        }else{
            break;
        }
        arguments.pathIn = listDeleteAt(arguments.pathIn, 1, '/');
    }
    if (left(arguments.pathIn, 1) neq "/"){
        arguments.pathIn = "/" & arguments.pathIn;
    }
    if (len(local.result) gt 0 and right(local.result, 1) eq '/'){
        local.result = left(local.result, len(local.result) - 1);
    }
    return local.result & arguments.pathIn;
}

This does exactly what I set out to do. Here is some sample input and output:

input: assets/style.css --- output: /test/1/2/assets/style.css
input: ./assets/style.css --- output: /test/1/2/assets/style.css
input: ../assets/style.css --- output: /test/1/assets/style.css
input: ../../assets/style.css --- output: /test/assets/style.css
input: ../../../assets/style.css --- output: /assets/style.css

Notice that it gracefully handles when you attempt to traverse too high in the directory structure. The last two tests have different input but the same output.

in ColdFusion Posted 2010-04-19 11:30

0 responses:

Leave a comment:

Leave this field empty: