Skip to content

Commit

Permalink
Add StringHelper::matchWildcard() (yiisoft#15389)
Browse files Browse the repository at this point in the history
`StringHelper::matchWildcard()` added
  • Loading branch information
klimov-paul authored Dec 20, 2017
1 parent e29245d commit 1b8da6d
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 12 deletions.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Yii Framework 2 Change Log
- Enh #8752: Allow specify `$attributeNames` as a string for `yii\base\Model` `validate()` method (developeruz)
- Enh #9137: Added `Access-Control-Allow-Method` header for the OPTIONS request (developeruz)
- Enh #9253: Allow `variations` to be a string for `yii\filters\PageCache` and `yii\widgets\FragmentCache` (schojniak, developeruz)
- Enh #12623: Added `yii\helpers\StringHelper::matchWildcard()` replacing usage of `fnmatch()`, which may be unreliable (klimov-paul)
- Enh #14043: Added `yii\helpers\IpHelper` (silverfire, cebe)
- Enh #7996: Short syntax for verb in GroupUrlRule (schojniak, developeruz)
- Enh #14568: Refactored migration templates to use `safeUp()` and `safeDown()` methods (Kolyunya)
Expand Down
5 changes: 3 additions & 2 deletions framework/base/ActionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

namespace yii\base;
use yii\helpers\StringHelper;

/**
* ActionFilter is the base class for action filters.
Expand Down Expand Up @@ -149,7 +150,7 @@ protected function isActive($action)
} else {
$onlyMatch = false;
foreach ($this->only as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
$onlyMatch = true;
break;
}
Expand All @@ -158,7 +159,7 @@ protected function isActive($action)

$exceptMatch = false;
foreach ($this->except as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
$exceptMatch = true;
break;
}
Expand Down
3 changes: 2 additions & 1 deletion framework/filters/AccessRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use yii\base\Component;
use yii\base\Controller;
use yii\base\InvalidConfigException;
use yii\helpers\StringHelper;
use yii\web\Request;
use yii\web\User;

Expand Down Expand Up @@ -198,7 +199,7 @@ protected function matchController($controller)

$id = $controller->getUniqueId();
foreach ($this->controllers as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
return true;
}
}
Expand Down
3 changes: 2 additions & 1 deletion framework/filters/HostControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Yii;
use yii\base\ActionFilter;
use yii\helpers\StringHelper;
use yii\web\NotFoundHttpException;

/**
Expand Down Expand Up @@ -135,7 +136,7 @@ public function beforeAction($action)
$currentHost = Yii::$app->getRequest()->getHostName();

foreach ($allowedHosts as $allowedHost) {
if (fnmatch($allowedHost, $currentHost)) {
if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
return true;
}
}
Expand Down
3 changes: 2 additions & 1 deletion framework/filters/auth/AuthMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Yii;
use yii\base\Action;
use yii\base\ActionFilter;
use yii\helpers\StringHelper;
use yii\web\Request;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
Expand Down Expand Up @@ -104,7 +105,7 @@ protected function isOptional($action)
{
$id = $this->getActionId($action);
foreach ($this->optional as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
return true;
}
}
Expand Down
14 changes: 8 additions & 6 deletions framework/helpers/BaseFileHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -591,12 +591,12 @@ private static function matchBasename($baseName, $pattern, $firstWildcard, $flag
}
}

$fnmatchFlags = 0;
$matchOptions = [];
if ($flags & self::PATTERN_CASE_INSENSITIVE) {
$fnmatchFlags |= FNM_CASEFOLD;
$matchOptions['caseSensitive'] = false;
}

return fnmatch($pattern, $baseName, $fnmatchFlags);
return StringHelper::matchWildcard($pattern, $baseName, $matchOptions);
}

/**
Expand Down Expand Up @@ -645,12 +645,14 @@ private static function matchPathname($path, $basePath, $pattern, $firstWildcard
}
}

$fnmatchFlags = FNM_PATHNAME;
$matchOptions = [
'filePath' => true
];
if ($flags & self::PATTERN_CASE_INSENSITIVE) {
$fnmatchFlags |= FNM_CASEFOLD;
$matchOptions['caseSensitive'] = false;
}

return fnmatch($pattern, $name, $fnmatchFlags);
return StringHelper::matchWildcard($pattern, $name, $matchOptions);
}

/**
Expand Down
53 changes: 53 additions & 0 deletions framework/helpers/BaseStringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,57 @@ public static function floatToString($number)
// so its safe to call str_replace here
return str_replace(',', '.', (string) $number);
}

/**
* Checks if the passed string would match the given shell wildcard pattern.
* This function emulates [[fnmatch()]], which may be unavailable at certain environment, using PCRE.
* @param string $pattern the shell wildcard pattern.
* @param string $string the tested string.
* @param array $options options for matching. Valid options are:
*
* - caseSensitive: bool, whether pattern should be case sensitive. Defaults to `true`.
* - escape: bool, whether backslash escaping is enabled. Defaults to `true`.
* - filePath: bool, whether slashes in string only matches slashes in the given pattern. Defaults to `false`.
*
* @return bool whether the string matches pattern or not.
* @since 2.0.14
*/
public static function matchWildcard($pattern, $string, $options = [])
{
if ($pattern === '*' && empty($options['filePath'])) {
return true;
}

$replacements = [
'\\\\\\\\' => '\\\\',
'\\\\\\*' => '[*]',
'\\\\\\?' => '[?]',
'\*' => '.*',
'\?' => '.',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
'\-' => '-',
];

if (isset($options['escape']) && !$options['escape']) {
unset($replacements['\\\\\\\\']);
unset($replacements['\\\\\\*']);
unset($replacements['\\\\\\?']);
}

if (!empty($options['filePath'])) {
$replacements['\*'] = '[^/\\\\]*';
$replacements['\?'] = '[^/\\\\]';
}

$pattern = strtr(preg_quote($pattern, '#'), $replacements);
$pattern = '#^' . $pattern . '$#us';

if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
$pattern .= 'i';
}

return preg_match($pattern, $string) === 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Yii;
use yii\console\controllers\BaseMigrateController;
use yii\helpers\FileHelper;
use yii\helpers\StringHelper;
use yiiunit\TestCase;

/**
Expand Down Expand Up @@ -190,7 +191,7 @@ protected function assertMigrationHistory(array $expectedMigrations, $message =
$appliedMigrations = $migrationHistory;
foreach ($expectedMigrations as $expectedMigrationName) {
$appliedMigration = array_shift($appliedMigrations);
if (!fnmatch(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) {
if (!StringHelper::matchWildcard(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) {
$success = false;
break;
}
Expand Down
87 changes: 87 additions & 0 deletions tests/framework/helpers/StringHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,91 @@ public function base64UrlEncodedStringsProvider()
['Это закодированная строка', '0K3RgtC-INC30LDQutC-0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw'],
];
}

/**
* Data provider for [[testMatchWildcard()]]
* @return array test data.
*/
public function dataProviderMatchWildcard()
{
return [
// *
['*', 'any', true],
['*', '', true],
['begin*end', 'begin-middle-end', true],
['begin*end', 'beginend', true],
['begin*end', 'begin-d', false],
['*end', 'beginend', true],
['*end', 'begin', false],
['begin*', 'begin-end', true],
['begin*', 'end', false],
['begin*', 'before-begin', false],
// ?
['begin?end', 'begin1end', true],
['begin?end', 'beginend', false],
['begin??end', 'begin12end', true],
['begin??end', 'begin1end', false],
// []
['gr[ae]y', 'gray', true],
['gr[ae]y', 'grey', true],
['gr[ae]y', 'groy', false],
['a[2-8]', 'a1', false],
['a[2-8]', 'a3', true],
['[][!]', ']', true],
['[-1]', '-', true],
// [!]
['gr[!ae]y', 'gray', false],
['gr[!ae]y', 'grey', false],
['gr[!ae]y', 'groy', true],
['a[!2-8]', 'a1', true],
['a[!2-8]', 'a3', false],
// -
['a-z', 'a-z', true],
['a-z', 'a-c', false],
// slashes
['begin/*/end', 'begin/middle/end', true],
['begin/*/end', 'begin/two/steps/end', true],
['begin/*/end', 'begin/end', false],
['begin\\\\*\\\\end', 'begin\middle\end', true],
['begin\\\\*\\\\end', 'begin\two\steps\end', true],
['begin\\\\*\\\\end', 'begin\end', false],
// dots
['begin.*.end', 'begin.middle.end', true],
['begin.*.end', 'begin.two.steps.end', true],
['begin.*.end', 'begin.end', false],
// case
['begin*end', 'BEGIN-middle-END', false],
['begin*end', 'BEGIN-middle-END', true, ['caseSensitive' => false]],
// file path
['begin/*/end', 'begin/middle/end', true, ['filePath' => true]],
['begin/*/end', 'begin/two/steps/end', false, ['filePath' => true]],
['begin\\\\*\\\\end', 'begin\middle\end', true, ['filePath' => true]],
['begin\\\\*\\\\end', 'begin\two\steps\end', false, ['filePath' => true]],
['*', 'any', true, ['filePath' => true]],
['*', 'any/path', false, ['filePath' => true]],
['[.-0]', 'any/path', false, ['filePath' => true]],
['*', '.dotenv', true, ['filePath' => true]],
// escaping
['\*\?', '*?', true],
['\*\?', 'zz', false],
['begin\*\end', 'begin\middle\end', true, ['escape' => false]],
['begin\*\end', 'begin\two\steps\end', true, ['escape' => false]],
['begin\*\end', 'begin\end', false, ['escape' => false]],
['begin\*\end', 'begin\middle\end', true, ['filePath' => true, 'escape' => false]],
['begin\*\end', 'begin\two\steps\end', false, ['filePath' => true, 'escape' => false]],
];
}

/**
* @dataProvider dataProviderMatchWildcard
*
* @param string $pattern
* @param string $string
* @param bool $expectedResult
* @param array $options
*/
public function testMatchWildcard($pattern, $string, $expectedResult, $options = [])
{
$this->assertSame($expectedResult, StringHelper::matchWildcard($pattern, $string, $options));
}
}

0 comments on commit 1b8da6d

Please sign in to comment.