dragonrun1 / file_path_normalizer
File_path_normalizer是一个用于在不具备内置函数一些缺点的情况下标准化PHP文件路径的类。
Requires
- php: >=7.1 <7.4
- ext-ctype: *
Requires (Dev)
- leanphp/phpspec-code-coverage: >=4.2 <5.0.0
- phpspec/nyan-formatters: >=2.0.0 <3.0.0
- phpspec/phpspec: >=4.0 <5.0.0
- phpunit/php-code-coverage: >=5.0 <6.0.0
- roave/security-advisories: dev-master
Replaces
README
File_path_normalizer是一个用于在不具备内置函数一些缺点的情况下标准化PHP文件路径的类。
为什么你应该使用它
所以你可能想知道,为什么需要这样的类,因为PHP有很多内置函数可以让你完成几乎任何清理给定文件名和路径所需的操作。PHP确实有很多与路径和文件名一起工作的函数,以及一系列有用的字符串和正则表达式函数,然而,这些函数中有许多边缘情况和限制可能导致错误,而且在任何基于Web的应用程序中,文件系统意外暴露的部分可能导致安全问题。
就像在网页中,你必须防止JavaScript XSS和可能的数据库暴露一样,你也必须确保用户输入可能被用于形成文件系统路径或名称时是干净的,并且不会执行意外操作。
即使你不需要使用任何用户输入来形成路径,你为PHP使用的操作系统也会影响文件路径以及文件函数的工作方式。可能最著名的区别是Windows路径中使用反斜杠和驱动器字母与Linux/Unix和Mac OS X的统一文件路径中使用正斜杠。
以下是一些示例,以使问题更清晰。
问题示例
注意:这里使用的示例绝对不是应该在实际代码中使用的良好编写的代码,而是为了尽可能简单,同时展示一些可能的问题。
<?php
$path = 'C:\Windows\System32\cmd.exe /c dir';
print shell_exec($path);
在类Unix系统中,这可能会报告错误并导致PHP退出,但在Windows系统中,它通常会显示当前目录的目录列表。在大多数情况下,这可能不是一个大问题,但假设它是以下内容
<?php
$path = 'C:\\Windows\\System32\\cmd.exe /c del /q *.*';
print shell_exec($path);
此代码可能会导致在Windows系统上根据运行代码的用户权限删除当前目录中的所有文件。
以下是相同内容的Unix版本。
<?php
$path = '/bin/bash -c "rm -f *.*"';
print shell_exec($path);
现在,大多数优秀的开发者都会对文件路径进行一些检查,例如以下示例
<?php
$allowedPath = '/my/web/app/';
$path = '/bin/bash -c "ls -al"';
if (false === strpos($path, $path)) {
$mess = 'Illegal file path detected must be in ' . $allowedPath;
throw Exception($mess);
}
print shell_exec($path);
这看起来似乎是好的,但如果我们考虑不同的路径,如以下示例
<?php
$allowedPath = '/my/web/app/';
$path = '/my/web/app/../../../bin/bash -c "ls -al"';
if (false === strpos($path, $path)) {
$mess = 'Illegal file path detected must be in ' . $allowedPath;
throw Exception($mess);
}
print shell_exec($path);
防止这种情况是非常困难的。大多数程序员试图使用realpath()
来解决路径问题,但是有一些边缘情况它可能会返回一些意外结果。
在这个示例中,我将给出一些可能需要处理的已知边缘情况。
<?php
// The current directory is '/my/web/app/'
$path = '/my/web/app/../../../bin/bash';
print realpath($path) . PHP_EOL;
// result: /bin/bash
// The current directory is '/my/web/app/'
$path = '../../../../iDoNotExist';
print realpath($path) . PHP_EOL;
// result is boolean `false` which is turned into an empty string by print.
// The current directory is '/my/app/'
$path = '/my/app/existingFile';
print realpath($path) . PHP_EOL;
// result: /my/app/existingFile
unlink($path);
print realpath($path) . PHP_EOL;
/*
result: /my/app/existingFile
realpath() and several other functions trigger caching of the resulting paths
which are not updated by other PHP file operations and would ignore any changes
happening outside of PHP as well.
*/
/*
Given the follow directory structure:
/my/app/
/DoIExist
/OtherFile
and the current directory is '/my/app/'
*/
$path = '../../../DoIExist';
print realpath($path) . PHP_EOL;
/*
The result in this case is /DoIExist
but what if you have to following directory structure:
/my/app/
/OtherFile
In this case the result is false again. The reason is once realpath() resolves
up to the top of the directory structure it in effect treats any additional
'../' like they are './' instead which means they are basically ignored.
*/
如你所见,使用realpath()
时需要非常小心。其他一些文件函数也有类似的问题,尤其是在处理相对路径时。在下一节中,我将展示如何使用FilePathNormalizer
来解决这些边缘情况,并在许多情况下充当改进的realpath()
替代品。
这个类是如何帮助的
首先,这里有一些程序员使用相对路径的原因
- 相对路径通常更短,因此更容易处理。
- 路径全部或部分来自用户输入,可能是相对路径。
- 使用相对路径意味着应用程序实际安装的位置并不重要——只有内部应用程序目录结构始终相同。
可能还有其他原因,但上述原因是我经常在我和另一位开发者的代码中看到的一些最常见的原因。我们还知道,从上面的部分来看,虽然可以使用realpath()
,但如果你使用它,你必须解决一些问题。
解决这些问题的方法之一是将realpath()
包装在类中,并尝试添加代码来处理所有不希望的问题。相反,我选择了一种策略,根本不使用它,但具有我最喜欢的realpath()
的大部分功能,但没有其缺点。
我还想得到一个既适用于Windows和Unix路径,又允许我像在Unix路径下工作一样的解决方案。
我认为FilePathNormalizer
更出色的最好方式是,通过一些示例来展示它是如何处理上述一些边缘情况的。
<?php
$fpn = new FilePathNormalizer();
// The current directory is '/my/web/app/'
$path = '/my/web/app/../../../bin/bash';
print realpath($path) . PHP_EOL;
// result: /bin/bash
print $fpn->normalizeFile($path) . PHP_EOL;
// The same result as above.
print $fpn->normalizePath($path) . PHP_EOL;
// result: /bin/bash/
// Note the added end '/' when using normalizePath().
// The current directory is '/my/web/app/'
$path = '../../../../iDoNotExistFile';
print realpath($path) . PHP_EOL;
// result is boolean `false` which is turned into an empty string by print.
try {
print $fpn->normalizeFile($path) . PHP_EOL;
} catch (DomainException $e) {
print $e->getMessage() . PHP_EOL;
}
// results in a catchable exception because relative paths above the root
// directory are NOT allowed.
// The current directory is '/my/app/'
$path = '/my/app/existingFile';
print realpath($path) . PHP_EOL;
// result: /my/app/existingFile
print $fpn->normalizeFile($path) . PHP_EOL;
// Same result as above.
unlink($path);
print realpath($path) . PHP_EOL;
/*
result: /my/app/existingFile
realpath() and several other functions trigger caching of the resulting paths
which are not updated by other PHP file operations and would ignore any changes
happening outside of PHP as well.
*/
print $fpn->normalizeFile($path) . PHP_EOL;
/*
Same result as as realpath() but for a different reason. normalizeFile() and
normalizePath() don't look at the file system to see if the resulting path
exists or NOT. They are only meant to insure the path should be valid and clean
and leave to testing for is they exist in the file system up to the user. Using
functions like file_exists() which doesn't use the cache is recommended.
*/
上述其他几个案例也会导致DomainException
,因为它们也尝试“高于”根路径。接下来,我将概述类的一些其他功能,以使处理路径更容易。
处理路径分隔符
在Windows系统上运行时,PHP允许在路径中使用前斜杠(FS)和后斜杠(BS)作为目录分隔符。通常在单个路径中使用两者不是一个好主意,但似乎在某些情况下可以工作。在基于Unix的系统上,PHP将BS不视为分隔符,在某些情况下将其视为转义符或该路径部分的名称的一部分。由于PHP和Windows本身实际上允许您使用FSes,因此最好将所有BSes替换为FSes,而不用担心混合使用它们。
请注意,尽管Windows本身允许使用,但cmd.exe
和Windows Explorer
似乎只理解并允许使用BSes作为分隔符。FilePathNormalizer
所做的第一件事是将所有BSes转换为FSes,这使得处理路径总体上更容易。另一件事是normalizePath()
所做的,是将路径与尾随FS进行归一化,因此路径/my/web/app
将变为/my/web/app/
。当向路径添加文件名时,不再需要“我需要分隔符吗?”的问题。
不仅仅是本地
与许多其他编程语言相比,PHP中的文件函数在许多方面都是独特的,因为它们不仅仅用于访问本地系统上的文件,如果允许,它们还可以用于访问另一系统上的文件。使用类似https://github.com/Dragonrun1/file_path_normalizer/blob/master/FilePathNormalizer.php
是可能的。在PHP中,https://
部分被称为包装器,在某些情况下,您甚至可以使用多个包装器,每个包装器都将传递内部包装器的结果。例如,您可以这样做:zip://ftp://my/ftp/site/sample.zip
,PHP将使用FTP从站点获取sample.zip
文件,并将其解压缩,只要两个包装器都已安装。大多数包装器处理程序是用C代码编写的,但也可以用PHP编写。一个很好的例子是vsfStream,它可以用作单元测试的虚拟文件系统。
在下一节中,我将详细介绍包装器以及FilePathNormalizer
如何为您的基本验证它们。
包装器
包装器可用于多种用途,例如压缩和解压缩zip://
或确定默认端口以及连接到另一个系统的连接方式http://
与https://
。正如上节所述,在某些情况下,您允许嵌套使用。FilePathNormalizer
不会尝试进行大量的验证,但它确实确保任何包含包装器都遵循以下基本模式:以字母字符开头,后面跟一个或多个字母数字字符,并以://
结尾,并且允许嵌套包装器。有一点是,它仅在路径的开始处进行验证,所以像http://a.amazing.site/ftp://not.so.amazing.site
这样的东西,只会看到http://
部分作为包装器,其余部分只是正常路径的一部分。
在根目录
因此,对于路径的根部分,您有两个阵营。一个是Windows,另一个是Unix以及其他人。有一小部分原始的Mac用户使用名称和冒号,但由于OSX,苹果公司已经将他们大多数转移到与其他人相同的文件系统,所以我会忽略他们。
这部分路径实际上处理起来相当简单,因为它只能是一个字母后跟Windows上的:/
或其他人仅文件系统。所有的不合理部分都在这个点之前已经处理过了。
总结
因此,希望上面的文本和示例能帮助您更好地理解路径以及FilePathNormalizer
如何帮助您更好地处理它们。
安装
File Path Normalizer可通过Packagest获得,并可以使用composer进行安装
composer require dragonrun1/file_path_normalizer
向后兼容性破坏性更改
随着2.0.0版本的发布,应用开发者应了解一些BC更改
- 最低PHP版本现在是7.0.0。是时候开始引领未来了。
- normalizeFile()和normalizePath上对bool选项的旧版使用已被删除。
- 启用或允许使用绝对路径的VFSStream不再允许。这个异常比有用更令人困惑。
- 现在将验证选项,并且以前可能允许但现在未处理的许多组合会导致抛出异常。
- 由于对VFSStream和绝对路径的更改以及包装器和VFSStream允许的交互更改,默认值已更改为:
const MODE_DEFAULT = self::ABSOLUTE_REQUIRED | self::VFS_DISABLED | self::WRAPPER_ALLOWED;
- 更好地检查包装器以关闭一些额外的边缘情况。
新功能
随着2.0.0版本的发布,一些新功能已被添加。
- 已提取新的PathInfo类,现在应用开发者可以使用它来满足其他路径相关需求。它使内部使用的相同
wrappers
、root
和dirs
部分可用,因此您可以例如剥离所有包装器或剥离路径的绝对部分。 - 所有测试都已被重写,以使用PHPSpec示例,而不是混合PHPUnit测试和PHPSpec示例。
- 已添加新的PHPSpec示例,这大大提高了代码覆盖率和代码质量。
- 提供所有方面的库的新和更新接口。
- 提供新的PathInfoTrait,以便更容易将其添加到自己的代码中。
- FilePathNormalizerTrait::getFpn()现在是公共的。