Lately I’ve been working on a project that required me to handle a lot of file-system related operations, especially paths.
While PHP offers some basic functions to handle paths, such as basename and dirname to resolute the (direct) parent and base name of a path, it does not offer any means of normalizing or combining a path if it’s on a remote file system that is not in the server’s reach. If the files are local, it offers the function realpath.
I didn’t like the case and decided to write a ‘static’ utility class to handle file paths safely, without worrying about possible path masquerading from broken code.
I hope someone will find the result useful:
<?php /** * @class Path * * @brief Utility class that handles file and directory pathes * * This class handles basic important operations done to file system paths. * It safely renders relative pathes and removes all ambiguity from a relative path. * * @author Liran Nuna */ final class Path { /** * Returns the parent path of this path. * "/path/to/directory" will return "/path/to" * * @arg $path The path to retrieve the parent path from */ public static function dirname($path) { return dirname(self::normalize($path)); } /** * Returns the last item on the path. * "/path/to/directory" will return "directory" * * @arg $path The path to retrieve the base from */ public static function basename($path) { return basename(self::normalize($path)); } /** * Normalizes the path for safe usage * This function does several operations to the given path: * * Removes unnecessary slashes (///path//to/////directory////) * * Removes current directory references (/path/././to/./directory/./././) * * Renders relative pathes (/path/from/../to/somewhere/in/../../directory) * * @arg $path The path to normalize */ public static function normalize($path) { return array_reduce(explode('/', $path), create_function('$a, $b', ' if($a === 0) $a = "/"; if($b === "" || $b === ".") return $a; if($b === "..") return dirname($a); return preg_replace("/\/+/", "/", "$a/$b"); '), 0); } /** * Combines a list of pathes to one safe path * * @arg $root The path or array with values to combine into a single path * @arg ... Relative pathes to root or arrays * * @note This function works with multi-dimentional arrays recursively. */ public static function combine($root, $rel1) { $arguments = func_get_args(); return self::normalize(array_reduce($arguments, create_function('$a,$b', ' if(is_array($a)) $a = array_reduce($a, "Path::combine"); if(is_array($b)) $b = array_reduce($b, "Path::combine"); return "$a/$b"; '))); } /** * Empty, private constructor, to prevent instantiation */ private function __construct() { // Prevents instantiation } } |
Usage of this class is very simple, Path::basename and Path::dirname perform the same operation as PHP’s native dirname and basename, but safer:
<?php // PHP's native basname will return '..' echo basename('/path/to/treasure/island/monster/../..') . "\n"; // Safe basename will return 'treasure' echo Path::basename('/path/to/treasure/island/monster/../..') . "\n"; // PHP's native dirname will return '/path/to/treasure/island/monster/..' echo dirname('/path/to/treasure/island/monster/../..') . "\n"; // Safe dirname will return '/path/to' echo Path::dirname('/path/to/treasure/island/monster/../..') . "\n"; |
Path::normalize will sanitize paths and return the safe real path even if it does not exist on the server:
<?php // Normalize will 'sanitize' this path // Result: '/path/to/candy/up/ahead/please/go/right' echo Path::normalize( '///../path//to/./monster/././/' . '//../candy/.//./up/ahead/.//./' . 'test//back/../..//please/go///' . '/left/./../right/123_test!/../' ) . "\n"; |
Lastly, Path::combine will combine paths from variable amount of strings and arrays to form one safe path:
<?php // Combine paths from a relative path and root // Result: '/var/www/www.site.com/index.html' echo Path::combine( '/var/www/www.site.com/', 'img/../css/jqueryui/../../index.html' ) . "\n"; // Combine will also take values from arrays // Result: '/path/to/directory/sub/TEST/test/lastDirectory/filename.ext' echo Path::combine( array( "/path/to", "folder/../directory" ), 'sub', array( array( array( 'TEST', 'test', ) ), 'lastDirectory', ), 'filename.ext' ) . "\n"; |
As always, code I post is under the WTFPL, so you can use it without any obligations.
Brilliant….been struggling on an app because of uri resolution issues and this has saved me several hours of additional work :-)
You saved my day :) — And a few hour’s code…
Also I like the licence :) This way I can include your code in my public-domain-ish project :).
You rock. Path resolution was driving me nuts.
This saved me a lot of time. Thanks.
And thanks for pointing me to the WTFPL, I’d never come across that before.
The normalize function is AWESOME!!! You saved my day, many thanks!
Hmm it appears like your blog ate my first comment (it was extremely long)
so I guess I’ll just sum it up what I wrote and say,
I’m thoroughly enjoying your blog. I as well am an aspiring blog blogger
but I’m still new to the whole thing. Do you have any suggestions for inexperienced blog writers?
I’d definitely appreciate it.