PHP: Systems path to actual URL - php

Before i make my question I wanna say that I have tried every related post I found from stackoverflow such as PHP - Convert File system path to URL and nothing worked for me, meaning I did not get the Url I was looking for.
I need a
function PathToUrl($path)
{
...
}
which returns the actual Url of the $path
Example usage: echo PathToUrl('../songs'); should output http://www.mywebsite.com/files/morefiles/songs.
Note: The problem with the functions i found on stackoverflow is that they dont work with path that contains ../ for example on echo PathToUrl('../songs'); i would get something similar to http://www.mywebsite.com/files/morefiles/../songs which is not what i am looking for.

Here you go, I made this function and it works perfectly:
function getPath($path)
{
$url = "http".(!empty($_SERVER['HTTPS'])?"s":"").
"://".$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
$dirs = explode('/', trim(preg_replace('/\/+/', '/', $path), '/'));
foreach ($dirs as $key => $value)
if (empty($value)) unset($dirs[$key]);
$parsedUrl = parse_url($url);
$pathUrl = explode('/', trim($parsedUrl['path'], '/'));
foreach ($pathUrl as $key => $value)
if (empty($value)) unset($pathUrl[$key]);
$count = count($pathUrl);
foreach ($dirs as $key => $dir)
if ($dir === '..')
if ($count > 0)
array_pop($pathUrl);
else
throw new Exception('Wrong Path');
else if ($dir !== '.')
if (preg_match('/^(\w|\d|\.| |_|-)+$/', $dir)) {
$pathUrl[] = $dir;
++$count;
}
else
throw new Exception('Not Allowed Char');
return $parsedUrl['scheme'].'://'.$parsedUrl['host'].'/'.implode('/', $pathUrl);
}
Example:
Let's say your current location is http://www.mywebsite.com/files/morefiles/movies,
echo getPath('../songs');
OUTPUT
http://www.mywebsite.com/files/morefiles/songs
echo getPath('./songs/whatever');
OUTPUT
http://www.mywebsite.com/files/morefiles/movies/songs/whatever
echo getPath('../../../../../../');
In this case it will throw an exception.
NOTE
I use this regex '/^(\w|\d|\.| |_|-)+$/' to check the path directories which mean that only digit, word characters, '.', '-', '_' and ' ' are allowed. An exception will be trown for others characters.
The path .///path will be corrected by this function.
USEFUL
explode
implode
array_pop
parse_url
trim
preg_match
preg_replace

I see an answer was already posted, however I will post my solution which allows for direct specification of the URL :
echo up('http://www.google.ca/mypage/losethisfolder/',1);
function up($url, $howmany=1) {
$p = explode('/', rtrim($url, '/'));
if($howmany < count($p)) {
$popoff = 0;
while($popoff < $howmany) {
array_pop($p);
$popoff++;
}
}
return rtrim(implode('/', $p), '/') . '/';
}
Following is a much more elegant solution for virtual paths :
$path = '/foo/../../bar/../something/../results/in/skipthis/../this/';
echo virtualpath($path);
function virtualpath($path = null) {
$loopcount = 0;
$vpath = $path;
if(is_null($vpath)) { $vpath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); }
$vpath = rtrim($vpath,'/') . '/';
while(strpos($vpath,'../') !== false ) {
if(substr($vpath,0,4) == '/../') { $vpath = '/'. substr($vpath,4); }
$vpath = preg_replace('/(\/[\w\d\s\ \-\.]+?\/\.\.\/)/', '/', $vpath );
}
return $vpath;
}

Related

php convert ../ into full path

I want to convert ../ into full paths.For example I have following urls in css in https://example.com/folder1/folder2/style.css
img/example1.png
/img/example2.png
../img/example3.png
../../img/example4.png
https://example.com/folder1/folder2/example5.png
I want to convert them into full path like below for above examples
https://example.com/folder1/folder2/img/example1.png
https://example.com/folder1/folder2/img/example1.png
https://example.com/folder1/img/example1.png
https://example.com/img/example1.png
https://example.com/folder1/folder2/example5.png
I tried something like below
$domain = "https://example.com";
function convertPath($str)
{
global $domain;
if(substr( $str, 0, 4 ) == "http")
{
return $str;
}
if(substr( $str, 0, 1 ) == "/")
{
return $domain.$str;
}
}
I know am complicating it , There must be some easy way to this kind of operation.Please guide me .Thank you.
A simple idea:
build an array of folders with the url
when the folder (of the path) is .., pop the last item of the array
when it is ., do nothing
For other folders, push them.
Then you only have to join the folder array with / and to prepend the scheme and the domain.
$url = 'https://example.com/folder1/folder2/style.css';
$paths = [ 'img/example1.png',
'/img/example2.png',
'../img/example3.png',
'../../img/example4.png',
'https://example.com/folder1/folder2/example5.png' ];
$folders = explode('/', trim(parse_url($url, PHP_URL_PATH), '/'));
array_pop($folders);
$prefix = explode('/' . $folders[0] . '/', $url)[0]; // need to be improved using parse_url to re-build
// properly the url with the correct syntax for each scheme.
function getURLFromPath($path, $prefix, $folders) {
if ( parse_url($path, PHP_URL_SCHEME) )
return $path;
foreach (explode('/', ltrim($path, '/')) as $item) {
if ( $item === '..' ) {
array_pop($folders);
} elseif ( $item === '.' ) {
} else {
$folders[] = $item;
}
}
return $prefix . '/' . implode('/', $folders);
}
foreach ($paths as $path) {
echo getURLFromPath($path, $prefix, $folders), PHP_EOL;
}
demo

Check if the string is contained in any listed arrays

I'm executing a php program to return the files and folders in a given path. I need to exclude the files and folders that contain the string which are listed in the below arrays.
$folderArray = array("folder1", "temp");
$fileArray = array("test", "testing", "copy");
For example : I want to exclude folders with the word temp , files with the word copy (Eg: filename_copy.php) etc.
Current code:
$string = "filename-copy.php"; // this can be folder name/file name
foreach($folderArray as $a) {
if (!stripos($string,$a) !== false) echo "yes";
else echo "no";
}
foreach($fileArray as $a) {
if (!stripos($string,$a) !== false) echo "yes";
else echo "no";
}
But here I need to search in file array and folder array for the string in separate loops. Is there any other easy way to do this?
Can anyone help me to do this? Thanks in advance.
Here i am imploding these arrays $folderArray and $fileArray with | which in return make as pattern like this folder1|temp. So it will match any word either folder1 or temp
ini_set("display_errors", 1);
$folderArray = array("folder1", "temp");
$fileArray = array("test", "testing", "copy");
$string = "filename-copy.php";//sample filename
if(preg_match("/". implode("|", $fileArray)."/", $string))
{
echo "File to exclude";
}
$string = "foldername_temp";//sample folder name
if(preg_match("/". implode("|", $folderArray)."/", $string))
{
echo "folder to exclude";
}
You could have an array of variable names and use $$var_name variable variables (read more).
<?php
$folderArray = array("folder1", "temp");
$fileArray = array("test", "testing", "copy");
$arrs = array('folderArray', 'fileArray');
$string = "filename-copy.php";
foreach ($arrs as $array_name) {
foreach ($$array_name as $v) {
if (!stripos($string, $v) !== false) {
echo "yes";
} else {
echo "no";
}
}
}
Demo: https://eval.in/772973
You can use PHP in_array() function to do a exact match
<?php
$folderArray = array("folder1", "temp");
$fileArray = array("test", "testing", "copy");
if (in_array("filename-copy.php", $folderArray)) {
echo "yes";
}
?>
For the partial match you can use strpos()
function array_search_partial($arr, $keyword) {
foreach($arr as $index => $string) {
if (strpos($string, $keyword) !== FALSE)
return $index;
}
}
You can use the combo of RecursiveDirectoryIterator, RecursiveIteratorIterator and RecursiveFilterIterator (or RecursiveCallbackFilterIterator). This way you will retrieve only needed files.
Take a look at the answer to the similar question. Particularly at this part:
$files = new CallbackFilterIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$path,
FilesystemIterator::SKIP_DOTS
),
RecursiveIteratorIterator::LEAVES_ONLY
),
$filter
);
foreach ($files as $key => $file) {
// Do something with files.
var_dump($key, $file);
}
You can provide any filter function you want. In your case it will be something like this:
$folderArray = ["folder1", "temp"];
$fileArray = ["test", "testing", "copy"];
$filter = function ($current, $key, $iterator) use ($path, $folderArray, $fileArray) {
$path = preg_quote($path, '/');
$folderArray = implode('|', array_map('preg_quote', $folderArray));
$fileArray = implode('|', array_map('preg_quote', $fileArray));
if (
preg_match('/^' . $path . '\/(' . $folderArray . ')/', $key)
|| preg_match('/' . $fileArray . '/', $current->getFilename())
) {
return false;
}
return true;
};

PHP: normalize path of not existing directories to prevent directory traversals?

I would like to normalize a path from an external resource to prevent directory traversal attacks. I know about the realpath() function, but sadly this function returns only the path of existing directories. So if the directory doesn't exist (yet) the realpath() function cuts off the whole part of the path which doesn't exist.
So my Question is: Do you know a PHP function which only normalizes the path?
PS: I also don't want to create all possible directories in advance ;-)
There's no built-in PHP function for this. Use something like the following instead:
function removeDots($path) {
$root = ($path[0] === '/') ? '/' : '';
$segments = explode('/', trim($path, '/'));
$ret = array();
foreach($segments as $segment){
if (($segment == '.') || strlen($segment) === 0) {
continue;
}
if ($segment == '..') {
array_pop($ret);
} else {
array_push($ret, $segment);
}
}
return $root . implode('/', $ret);
}
Thanks to Benubird / Cragmonkey corrected me that under some situation my previous answer didn't work.
thus I make a new one, for the original purpose: Perform good, fewer lines, and with pure regular expression:
This time I tested with much more strict test case as below.
$path = '/var/.////./user/./././..//.//../////../././.././test/////';
function normalizePath($path) {
$patterns = array('~/{2,}~', '~/(\./)+~', '~([^/\.]+/(?R)*\.{2,}/)~', '~\.\./~');
$replacements = array('/', '/', '', '');
return preg_replace($patterns, $replacements, $path);
}
The correct answer would be /test/.
Not meant to do competition, but performance test is a must:
test case:
for loop 100k times, on an Windows 7, i5-3470 Quad Core, 3.20 GHz.
mine: 1.746 secs.
Tom Imrei: 4.548 secs.
Benubird: 3.593 secs.
Ursa: 4.334 secs.
It doesn't means my version is always better. In several situation they perform simular.
I think Tamas' solution will work, but it is also possible to do it with regex, which may be less efficient but looks neater. Val's solution is incorrect; but this one works.
function normalizePath($path) {
do {
$path = preg_replace(
array('#//|/\./#', '#/([^/.]+)/\.\./#'),
'/', $path, -1, $count
);
} while($count > 0);
return $path;
}
Yes, it does not handle all the possible different encodings of ./\ etc. that there can be, but that is not the purpose of it; one function should do one thing only, so if you want to also convert %2e%2e%2f into ../, run it through a separate function first.
Realpath also resolves symbolic links, which is obviously impossible if the path doesn't exist; but we can strip out the extra '/./', '/../' and '/' characters.
Strict, but safe implementation. If you use only ASCII for file names it would be suitable:
/**
* Normalise a file path string so that it can be checked safely.
*
* #param $path string
* The path to normalise.
* #return string
* Normalised path or FALSE, if $path cannot be normalized (invalid).
*/
function normalisePath($path) {
// Skip invalid input.
if (!isset($path)) {
return FALSE;
}
if ($path === '') {
return '';
}
// Attempt to avoid path encoding problems.
$path = preg_replace("/[^\x20-\x7E]/", '', $path);
$path = str_replace('\\', '/', $path);
// Remember path root.
$prefix = substr($path, 0, 1) === '/' ? '/' : '';
// Process path components
$stack = array();
$parts = explode('/', $path);
foreach ($parts as $part) {
if ($part === '' || $part === '.') {
// No-op: skip empty part.
} elseif ($part !== '..') {
array_push($stack, $part);
} elseif (!empty($stack)) {
array_pop($stack);
} else {
return FALSE; // Out of the root.
}
}
// Return the "clean" path
$path = $prefix . implode('/', $stack);
return $path;
}
My 2 cents. The regexp is used only for empty blocks of path:
<?php
echo path_normalize('/a/b/c/../../../d/e/file.txt');
echo path_normalize('a/b/../c');
echo path_normalize('./../../etc/passwd');
echo path_normalize('/var/user/.///////././.././.././././test/');
function path_normalize($path){
$path = str_replace('\\','/',$path);
$blocks = preg_split('#/#',$path,null,PREG_SPLIT_NO_EMPTY);
$res = array();
while(list($k,$block) = each($blocks)){
switch($block){
case '.':
if($k == 0)
$res = explode('/',path_normalize(getcwd()));
break;
case '..';
if(!$res) return false;
array_pop($res);
break;
default:
$res[] = $block;
break;
}
}
return implode('/',$res);
}
?>

PHP: building a URL path

I have a few strings to combine to build a full path. e.g.
$base = "http://foo.com";
$subfolder = "product/data";
$filename = "foo.xml";
// How to do this?
$url = append_url_parts($base, $subfolder, $filename); ???
String concatenation won't do, that would omit the necessary forward slashes.
In Win32 I'd use PathCombine() or PathAppend(), which would handle adding any necessary slashes between strings, without doubling them up. In PHP, what should I use?
Try this:
$base = "http://foo.com";
$subfolder = "product/data";
$filename = "foo.xml";
function stripTrailingSlash(&$component) {
$component = rtrim($component, '/');
}
$array = array($base, $subfolder, $filename);
array_walk_recursive($array, 'stripTrailingSlash');
$url = implode('/', $array);
when it comes down to something like this I like to use a special function with unlimited parameters.
define('BASE_URL','http://mysite.com'); //Without last slash
function build_url()
{
return BASE_URL . '/' . implode(func_get_args(),'/');
}
OR
function build_url()
{
$Path = BASE_URL;
foreach(func_get_args() as $path_part)
{
$Path .= '/' . $path_part;
}
return $Path;
}
So that when I use the function I can do
echo build_url('home'); //http://mysite.com/home
echo build_url('public','css','style.css'); //http://mysite.com/public/css/style.css
echo build_url('index.php'); //http://mysite.com/index.php
hope this helps you, works really well for me especially within an Framework Environment.
to use with params you can append the url like so for simplicity.
echo build_url('home') . '?' . http_build_query(array('hello' => 'world'));
Would produce: http://mysite.com/home?hello=world
not sure why you say string concat won't do, because something like this is basically similar to a string concat. (untested semi-pseudo)
function append_url_parts($base, $subf, $file) {
$url = sprintf("%s%s%s", $base, (($subf)? "/$subf": ""), (($file)? "/$file": ""));
return $url;
}
with string concat, we'd have to write a slightly longer block like so:
function append_url_parts($base, $subf, $file) {
$subf = ($subf)? "/$subf": "";
$file = ($file)? "/$file": "";
$url = "$base$subf$file";
return $url;
}
I usually go simple:
<?
$url = implode('/', array($base, $subfolder, $filename));
Either that or use a framework, and then use whatever route system it has.
There are a few considerations first.
Are you interested in getting the current path of the script or some other path?
How flexible do you need this to be? Is it something that is going to change all the time? Is it something an admin will set once and forget?
You want to be careful not to include the slash bug where your document has a slash added at the end because you were too lazy to figure out how to separate directory vars from the file var. There will only be one file and one base per URL and unknown number of directories in each path, right? :)
If you want to make sure there are no duplicate slashes within the resultant path, I like this little function...simply pass it an array of path part you want combined and it will return a formatted path - no need to worry whether any of the parts contain a slash alerady or not:
function build_url($arr)
{
foreach ( $arr as $path ) $url[] = rtrim ( $path, '/' );
return implode( $url, '/' );
}
This should work on all versions of PHP too.
Not my code, but a handy function which takes an absolute URL and a relative URL and combines the two to make a new absolute path.
The function has been modified to ignore an absolute URL passed as relative ( basically anything that includes a schema ).
$url = "http://www.goat.com/money/dave.html";
$rel = "../images/cheese.jpg";
$com = InternetCombineURL($url,$rel);
public function InternetCombineUrl($absolute, $relative) {
$p = parse_url($relative);
if(isset($p["scheme"]))return $relative;
extract(parse_url($absolute));
$path = dirname($path);
if($relative{0} == '/') {
$cparts = array_filter(explode("/", $relative));
}
else {
$aparts = array_filter(explode("/", $path));
$rparts = array_filter(explode("/", $relative));
$cparts = array_merge($aparts, $rparts);
foreach($cparts as $i => $part) {
if($part == '.') {
$cparts[$i] = null;
}
if($part == '..') {
$cparts[$i - 1] = null;
$cparts[$i] = null;
}
}
$cparts = array_filter($cparts);
}
$path = implode("/", $cparts);
$url = "";
if($scheme) {
$url = "$scheme://";
}
if(isset($user)) {
$url .= "$user";
if($pass) {
$url .= ":$pass";
}
$url .= "#";
}
if($host) {
$url .= "$host/";
}
$url .= $path;
return $url;
}
I wrote this function for all cases to combine url parts with no duplicate slashes.
It accepts many arguments or an array of parts.
Some parts may be empty strings, that does not produce double slashes.
It keeps starting and ending slashes if they are present.
function implodePath($parts)
{
if (!is_array($parts)) {
$parts = func_get_args();
if (count($parts) < 2) {
throw new \RuntimeException('implodePath() should take array as a single argument or more than one argument');
}
} elseif (count($parts) == 0) {
return '';
} elseif (count($parts) == 1) {
return $parts[0];
}
$resParts = [];
$first = array_shift($parts);
if ($first === '/') {
$resParts[] = ''; // It will keep one starting slash
} else {
// It may be empty or have some letters
$first = rtrim($first, '/');
if ($first !== '') {
$resParts[] = $first;
}
}
$last = array_pop($parts);
foreach ($parts as $part) {
$part = trim($part, '/');
if ($part !== '') {
$resParts[] = $part;
}
}
if ($last === '/') {
$resParts[] = ''; // To keep trailing slash
} else {
$last = ltrim($last, '/');
if ($last !== '') {
$resParts[] = $last; // Adding last part if not empty
}
}
return implode('/', $resParts);
}
Here is a check list from unit test. Left array is input and right part is result string.
[['/www/', '/eee/'], '/www/eee/'],
[['/www', 'eee/'], '/www/eee/'],
[['www', 'eee'], 'www/eee'],
[['www', ''], 'www'],
[['www', '/'], 'www/'],
[['/www/', '/aaa/', '/eee/'], '/www/aaa/eee/'],
[['/www', 'aaa/', '/eee/'], '/www/aaa/eee/'],
[['/www/', '/aaa/', 'eee/'], '/www/aaa/eee/'],
[['/www', 'aaa', 'eee/'], '/www/aaa/eee/'],
[['/www/', '/aaa/'], '/www/aaa/'],
[['/www', 'aaa/'], '/www/aaa/'],
[['/www/', 'aaa/'], '/www/aaa/'],
[['/www', '/aaa/'], '/www/aaa/'],
[['/www', '', 'eee/'], '/www/eee/'],
[['www/', '/aaa/', '/eee'], 'www/aaa/eee'],
[['/www/', '/aaa', ''], '/www/aaa'],
[['', 'aaa/', '/eee/'], 'aaa/eee/'],
[['', '', ''], ''],
[['aaa', '', '/'], 'aaa/'],
[['aaa', '/', '/'], 'aaa/'],
[['/', 'www', '/'], '/www/'],
It can be used as implodePath('aaa', 'bbb') or implodePath(['aaa', 'bbb'])

How to join filesystem path strings in PHP?

Is there a builtin function in PHP to intelligently join path strings? The function, given abc/de/ and /fg/x.php as arguments, should return abc/de/fg/x.php; the same result should be given using abc/de and fg/x.php as arguments for that function.
If not, is there an available class? It could also be valuable for splitting paths or removing parts of them. If you have written something, may you share your code here?
It is ok to always use /, I am coding for Linux only.
In Python there is os.path.join, which is great.
function join_paths() {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
return preg_replace('#/+#','/',join('/', $paths));
}
My solution is simpler and more similar to the way Python os.path.join works
Consider these test cases
array my version #deceze #david_miller #mark
['',''] '' '' '/' '/'
['','/'] '/' '' '/' '/'
['/','a'] '/a' 'a' '//a' '/a'
['/','/a'] '/a' 'a' '//a' '//a'
['abc','def'] 'abc/def' 'abc/def' 'abc/def' 'abc/def'
['abc','/def'] 'abc/def' 'abc/def' 'abc/def' 'abc//def'
['/abc','def'] '/abc/def' 'abc/def' '/abc/def' '/abc/def'
['','foo.jpg'] 'foo.jpg' 'foo.jpg' '/foo.jpg' '/foo.jpg'
['dir','0','a.jpg'] 'dir/0/a.jpg' 'dir/a.jpg' 'dir/0/a.jpg' 'dir/0/a.txt'
Since this seems to be a popular question and the comments are filling with "features suggestions" or "bug reports"... All this code snippet does is join two strings with a slash without duplicating slashes between them. That's all. No more, no less. It does not evaluate actual paths on the hard disk nor does it actually keep the beginning slash (add that back in if needed, at least you can be sure this code always returns a string without starting slash).
join('/', array(trim("abc/de/", '/'), trim("/fg/x.php", '/')));
The end result will always be a path with no slashes at the beginning or end and no double slashes within. Feel free to make a function out of that.
EDIT:
Here's a nice flexible function wrapper for above snippet. You can pass as many path snippets as you want, either as array or separate arguments:
function joinPaths() {
$args = func_get_args();
$paths = array();
foreach ($args as $arg) {
$paths = array_merge($paths, (array)$arg);
}
$paths = array_map(create_function('$p', 'return trim($p, "/");'), $paths);
$paths = array_filter($paths);
return join('/', $paths);
}
echo joinPaths(array('my/path', 'is', '/an/array'));
//or
echo joinPaths('my/paths/', '/are/', 'a/r/g/u/m/e/n/t/s/');
:o)
#deceze's function doesn't keep the leading / when trying to join a path that starts with a Unix absolute path, e.g. joinPaths('/var/www', '/vhosts/site');.
function unix_path() {
$args = func_get_args();
$paths = array();
foreach($args as $arg) {
$paths = array_merge($paths, (array)$arg);
}
foreach($paths as &$path) {
$path = trim($path, '/');
}
if (substr($args[0], 0, 1) == '/') {
$paths[0] = '/' . $paths[0];
}
return join('/', $paths);
}
My take:
function trimds($s) {
return rtrim($s,DIRECTORY_SEPARATOR);
}
function joinpaths() {
return implode(DIRECTORY_SEPARATOR, array_map('trimds', func_get_args()));
}
I'd have used an anonymous function for trimds, but older versions of PHP don't support it.
Example:
join_paths('a','\\b','/c','d/','/e/','f.jpg'); // a\b\c\d\e\f.jpg (on Windows)
Updated April 2013 March 2014 May 2018:
function join_paths(...$paths) {
return preg_replace('~[/\\\\]+~', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $paths));
}
This one will correct any slashes to match your OS, won't remove a leading slash, and clean up and multiple slashes in a row.
If you know the file/directory exists, you can add extra slashes (that may be unnecessary), then call realpath, i.e.
realpath(join('/', $parts));
This is of course not quite the same thing as the Python version, but in many cases may be good enough.
As a fun project, I created yet another solution. Should be universal for all operating systems.
For PHP 7.2+:
<?php
/**
* Join string into a single URL string.
*
* #param string $parts,... The parts of the URL to join.
* #return string The URL string.
*/
function join_paths(...$parts) {
if (sizeof($parts) === 0) return '';
$prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
$processed = array_filter(array_map(function ($part) {
return rtrim($part, DIRECTORY_SEPARATOR);
}, $parts), function ($part) {
return !empty($part);
});
return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}
For PHP version before 7.2:
/**
* Join string into a single URL string.
*
* #param string $parts,... The parts of the URL to join.
* #return string The URL string.
*/
function join_paths() {
$parts = func_get_args();
if (sizeof($parts) === 0) return '';
$prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
$processed = array_filter(array_map(function ($part) {
return rtrim($part, DIRECTORY_SEPARATOR);
}, $parts), function ($part) {
return !empty($part);
});
return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}
Some test case for its behaviour.
// relative paths
var_dump(join_paths('hello/', 'world'));
var_dump(join_paths('hello', 'world'));
var_dump(join_paths('hello', '', 'world'));
var_dump(join_paths('', 'hello/world'));
echo "\n";
// absolute paths
var_dump(join_paths('/hello/', 'world'));
var_dump(join_paths('/hello', 'world'));
var_dump(join_paths('/hello/', '', 'world'));
var_dump(join_paths('/hello', '', 'world'));
var_dump(join_paths('', '/hello/world'));
var_dump(join_paths('/', 'hello/world'));
Results:
string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
Update: Added a version that supports PHP before 7.2.
An alternative is using implode() and explode().
$a = '/a/bc/def/';
$b = '/q/rs/tuv/path.xml';
$path = implode('/',array_filter(explode('/', $a . $b)));
echo $path; // -> a/bc/def/q/rs/tuv/path.xml
The solution below uses the logic proposed by #RiccardoGalli, but is improved to avail itself of the DIRECTORY_SEPARATOR constant, as #Qix and #FĂ©lixSaparelli suggested, and, more important, to trim each given element to avoid space-only folder names appearing in the final path (it was a requirement in my case).
Regarding the escape of directory separator inside the preg_replace() pattern, as you can see I used the preg_quote() function which does the job fine.
Furthermore, I would replace mutiple separators only (RegExp quantifier {2,}).
// PHP 7.+
function paths_join(string ...$parts): string {
$parts = array_map('trim', $parts);
$path = [];
foreach ($parts as $part) {
if ($part !== '') {
$path[] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $path);
return preg_replace(
'#' . preg_quote(DIRECTORY_SEPARATOR) . '{2,}#',
DIRECTORY_SEPARATOR,
$path
);
}
Elegant Python-inspired PHP one-liner way to join path.
This code doesn't use unnecessary array.
Multi-platform
function os_path_join(...$parts) {
return preg_replace('#'.DIRECTORY_SEPARATOR.'+#', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, array_filter($parts)));
}
Unix based systems
function os_path_join(...$parts) {
return preg_replace('#/+#', '/', implode('/', array_filter($parts)));
}
Unix based system without REST parameters (don't respect explicit PEP8 philosophy) :
function os_path_join() {
return preg_replace('#/+#', '/', implode('/', array_filter(func_get_args())));
}
Usage
$path = os_path_join("", "/", "mydir/", "/here/");
Bonus : if you want really follow Python os.path.join(). First argument is required :
function os_path_join($path=null, ...$paths) {
if (!is_null($path)) {
throw new Exception("TypeError: join() missing 1 required positional argument: 'path'", 1);
}
$path = rtrim($path, DIRECTORY_SEPARATOR);
foreach ($paths as $key => $current_path) {
$paths[$key] = $paths[$key] = trim($current_path, DIRECTORY_SEPARATOR);
}
return implode(DIRECTORY_SEPARATOR, array_merge([$path], array_filter($paths)));
}
Check os.path.join() source if you want : https://github.com/python/cpython/blob/master/Lib/ntpath.py
Warning : This solution is not suitable for urls.
for getting parts of paths you can use pathinfo
http://nz2.php.net/manual/en/function.pathinfo.php
for joining the response from #deceze looks fine
A different way of attacking this one:
function joinPaths() {
$paths = array_filter(func_get_args());
return preg_replace('#/{2,}#', '/', implode('/', $paths));
}
This is a corrected version of the function posted by deceze. Without this change, joinPaths('', 'foo.jpg') becomes '/foo.jpg'
function joinPaths() {
$args = func_get_args();
$paths = array();
foreach ($args as $arg)
$paths = array_merge($paths, (array)$arg);
$paths2 = array();
foreach ($paths as $i=>$path)
{ $path = trim($path, '/');
if (strlen($path))
$paths2[]= $path;
}
$result = join('/', $paths2); // If first element of old path was absolute, make this one absolute also
if (strlen($paths[0]) && substr($paths[0], 0, 1) == '/')
return '/'.$result;
return $result;
}
This seems to be work quite well, and looks reasonably neat to me.
private function JoinPaths() {
$slash = DIRECTORY_SEPARATOR;
$sections = preg_split(
"#[/\\\\]#",
implode('/', func_get_args()),
null,
PREG_SPLIT_NO_EMPTY);
return implode($slash, $sections);
}
Best solution found:
function joinPaths($leftHandSide, $rightHandSide) {
return rtrim($leftHandSide, '/') .'/'. ltrim($rightHandSide, '/');
}
NOTE: Copied from the comment by user89021
OS-independent version based on the answer by mpen but encapsulated into a single function and with the option to add a trailing path separator.
function joinPathParts($parts, $trailingSeparator = false){
return implode(
DIRECTORY_SEPARATOR,
array_map(
function($s){
return rtrim($s,DIRECTORY_SEPARATOR);
},
$parts)
)
.($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}
Or for you one-liner lovers:
function joinPathParts($parts, $trailingSeparator = false){
return implode(DIRECTORY_SEPARATOR, array_map(function($s){return rtrim($s,DIRECTORY_SEPARATOR);}, $parts)).($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}
Simply call it with an array of path parts:
// No trailing separator - ex. C:\www\logs\myscript.txt
$logFile = joinPathParts([getcwd(), 'logs', 'myscript.txt']);
// Trailing separator - ex. C:\www\download\images\user1234\
$dir = joinPathParts([getcwd(), 'download', 'images', 'user1234'], true);
Note OP is asking for something slightly different from https://docs.python.org/3/library/os.path.html#os.path.join which does more than just join paths with the right number of separators.
While what they have asked for has been answered, for anyone skim reading the Q&A, there will be the following differences and ambiguous cases between what was asked for and os.path.join():
Many of the above solutions don't work for the root only case ['/'] => '/'
os.path.join drop all args to the left of the rightmost absolute path e.g. ['a', 'b', '/c'] => '/c' which to be fair is probably not the behaviour you want if you are refactoring existing php which has a lot of path segments appear like they are absolute paths.
Another difference with os.path.join is it won't drop additional separators within a single string ['a///', 'b', 'c'] => 'a///b/c'
Another special case is one or more empty strings resulting in a trailing slash for os.path.join: ['a', ''] or ['a', '', ''] => 'a/'
Here's a function that behaves like Node's path.resolve:
function resolve_path() {
$working_dir = getcwd();
foreach(func_get_args() as $p) {
if($p === null || $p === '') continue;
elseif($p[0] === '/') $working_dir = $p;
else $working_dir .= "/$p";
}
$working_dir = preg_replace('~/{2,}~','/', $working_dir);
if($working_dir === '/') return '/';
$out = [];
foreach(explode('/',rtrim($working_dir,'/')) as $p) {
if($p === '.') continue;
if($p === '..') array_pop($out);
else $out[] = $p;
}
return implode('/',$out);
}
Test cases:
resolve_path('/foo/bar','./baz') # /foo/bar/baz
resolve_path('/foo/bar','/tmp/file/') # /tmp/file
resolve_path('/foo/bar','/tmp','file') # /tmp/file
resolve_path('/foo//bar/../baz') # /foo/baz
resolve_path('/','foo') # /foo
resolve_path('/','foo','/') # /
resolve_path('wwwroot', 'static_files/png/', '../gif/image.gif')
# __DIR__.'/wwwroot/static_files/gif/image.gif'
From the great answer of Ricardo Galli, a bit of improvement to avoid killing the protocol prefix.
The idea is to test for the presence of a protocol in one argument, and maintain it into the result. WARNING: this is a naive implementation!
For example:
array("http://domain.de","/a","/b/")
results to (keeping protocol)
"http://domain.de/a/b/"
instead of (killing protocol)
"http:/domain.de/a/b/"
But http://codepad.org/hzpWmpzk needs a better code writing skill.
I love Riccardo's answer and I think it is the best answer.
I am using it to join paths in url building, but with one small change to handle protocols' double slash:
function joinPath () {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
// Replace the slash with DIRECTORY_SEPARATOR
$paths = preg_replace('#/+#', '/', join('/', $paths));
return preg_replace('#:/#', '://', $paths);
}
function path_combine($paths) {
for ($i = 0; $i < count($paths); ++$i) {
$paths[$i] = trim($paths[$i]);
}
$dirty_paths = explode(DIRECTORY_SEPARATOR, join(DIRECTORY_SEPARATOR, $paths));
for ($i = 0; $i < count($dirty_paths); ++$i) {
$dirty_paths[$i] = trim($dirty_paths[$i]);
}
$unslashed_paths = array();
for ($i = 0; $i < count($dirty_paths); ++$i) {
$path = $dirty_paths[$i];
if (strlen($path) == 0) continue;
array_push($unslashed_paths, $path);
}
$first_not_empty_index = 0;
while(strlen($paths[$first_not_empty_index]) == 0) {
++$first_not_empty_index;
}
$starts_with_slash = $paths[$first_not_empty_index][0] == DIRECTORY_SEPARATOR;
return $starts_with_slash
? DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $unslashed_paths)
: join(DIRECTORY_SEPARATOR, $unslashed_paths);
}
Example usage:
$test = path_combine([' ', '/cosecheamo', 'pizze', '///// 4formaggi', 'GORGONZOLA']);
echo $test;
Will output:
/cosecheamo/pizze/4formaggi/GORGONZOLA
Here is my solution:
function joinPath(): string {
$path = '';
foreach (func_get_args() as $numArg => $arg) {
$arg = trim($arg);
$firstChar = substr($arg, 0, 1);
$lastChar = substr($arg, -1);
if ($numArg != 0 && $firstChar != '/') {
$arg = '/'.$arg;
}
# Eliminamos el slash del final
if ($lastChar == '/') {
$arg = rtrim($arg, '/');
}
$path .= $arg;
}
return $path;
}
Hmmm most seem a bit over complicated. Dunno, this is my take on it:
// Takes any amount of arguments, joins them, then replaces double slashes
function join_urls() {
$parts = func_get_args();
$url_part = implode("/", $parts);
return preg_replace('/\/{1,}/', '/', $url_part);
}
For people who want a join function that does the Windows backslash and the Linux forward slash.
Usage:
<?php
use App\Util\Paths
echo Paths::join('a','b'); //Prints 'a/b' on *nix, or 'a\\b' on Windows
Class file:
<?php
namespace App\Util;
class Paths
{
public static function join_with_separator($separator, $paths) {
$slash_delimited_path = preg_replace('#\\\\#','/', join('/', $paths));
$duplicates_cleaned_path = preg_replace('#/+#', $separator, $slash_delimited_path);
return $duplicates_cleaned_path;
}
public static function join() {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== '') { $paths[] = $arg; }
}
return Paths::join_with_separator(DIRECTORY_SEPARATOR, $paths);
}
}
Here's the test function:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use App\Util\Paths;
class PathsTest extends TestCase
{
public function testWindowsPaths()
{
$TEST_INPUTS = [
[],
['a'],
['a','b'],
['C:\\','blah.txt'],
['C:\\subdir','blah.txt'],
['C:\\subdir\\','blah.txt'],
['C:\\subdir','nested','1/2','blah.txt'],
];
$EXPECTED_OUTPUTS = [
'',
'a',
'a\\b',
'C:\\blah.txt',
'C:\\subdir\\blah.txt',
'C:\\subdir\\blah.txt',
'C:\\subdir\\nested\\1\\2\\blah.txt',
];
for ($i = 0; $i < count($TEST_INPUTS); $i++) {
$actualPath = Paths::join_with_separator('\\', $TEST_INPUTS[$i]);
$expectedPath = $EXPECTED_OUTPUTS[$i];
$this->assertEquals($expectedPath, $actualPath);
}
}
public function testNixPaths()
{
$TEST_INPUTS = [
[],
['a'],
['a','b'],
['/home','blah.txt'],
['/home/username','blah.txt'],
['/home/username/','blah.txt'],
['/home/subdir','nested','1\\2','blah.txt'],
];
$EXPECTED_OUTPUTS = [
'',
'a',
'a/b',
'/home/blah.txt',
'/home/username/blah.txt',
'/home/username/blah.txt',
'/home/subdir/nested/1/2/blah.txt',
];
for ($i = 0; $i < count($TEST_INPUTS); $i++) {
$actualPath = Paths::join_with_separator('/', $TEST_INPUTS[$i]);
$expectedPath = $EXPECTED_OUTPUTS[$i];
$this->assertEquals($expectedPath, $actualPath);
}
}
}
$args = [sys_get_temp_dir(), "path1","path2", "filename.pdf"];
$filename = implode( DIRECTORY_SEPARATOR, $args);
// output "C:\Users\User\AppData\Local\Temp\path1\path2\filename.pdf"
I liked several solutions presented. But those who does replacing all '/+' into '/' (regular expressions) are forgetting that os.path.join() from python can handle this kind of join:
os.path.join('http://example.com/parent/path', 'subdir/file.html')
Result: 'http://example.com/parent/path/subdir/file.html'

Categories