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.