Skip to content

Commit

Permalink
[4.x] Fix assets being deleted when renaming snake_case folder to keb…
Browse files Browse the repository at this point in the history
…ab-case. (#8826)
  • Loading branch information
jasonvarga authored Oct 11, 2023
1 parent 5b996ef commit 8f5da38
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/Assets/AssetContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Statamic\Facades\Blueprint;
use Statamic\Facades\File;
use Statamic\Facades\Image;
use Statamic\Facades\Pattern;
use Statamic\Facades\Search;
use Statamic\Facades\Stache;
use Statamic\Facades\URL;
Expand Down Expand Up @@ -357,7 +358,7 @@ public function assets($folder = '/', $recursive = false)

if ($folder !== null) {
if ($recursive) {
$query->where('path', 'like', "{$folder}/%");
$query->where('path', 'like', Pattern::sqlLikeQuote($folder).'/%');
} else {
$query->where('folder', $folder);
}
Expand Down
33 changes: 33 additions & 0 deletions src/Facades/Endpoint/Pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Statamic\Facades\Endpoint;

use Statamic\Support\Str;

/**
* Regular expressions, et al.
*/
Expand Down Expand Up @@ -104,4 +106,35 @@ public function isUUID($value)
{
return (bool) preg_match($this->uuid(), $value);
}

/**
* Quotes (escapes) SQL LIKE syntax.
*
* Similar to the preg_quote method for regular expressions.
*/
public function sqlLikeQuote(string $like): string
{
return Str::of($like)
->replace('%', '\%')
->replace('_', '\_');
}

/**
* Converts SQL LIKE syntax to a regular expression.
*
* @return string The regular expression without delimiters.
*/
public function sqlLikeToRegex(string $like): string
{
return Str::of($like)
->replace('\_', $underscore = Str::random())
->replace('\%', $percent = Str::random())
->pipe(fn ($str) => preg_quote($str, '/'))
->replace('%', '.*')
->replace('_', '.')
->replace($underscore, '_')
->replace($percent, '%')
->prepend('^')
->append('$');
}
}
7 changes: 4 additions & 3 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use InvalidArgumentException;
use Statamic\Contracts\Query\Builder as Contract;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
use Statamic\Facades\Pattern;

abstract class Builder implements Contract
{
Expand Down Expand Up @@ -629,13 +630,13 @@ protected function filterTestGreaterThanOrEqualTo($item, $value)

protected function filterTestLike($item, $like)
{
$pattern = '/^'.str_replace(['%', '_'], ['.*', '.'], preg_quote($like, '/')).'$/im';

if (is_array($item)) {
$item = json_encode($item);
}

return preg_match($pattern, (string) $item);
$pattern = Pattern::sqlLikeToRegex($like);

return preg_match('/'.$pattern.'/im', (string) $item);
}

protected function filterTestNotLike($item, $like)
Expand Down
18 changes: 16 additions & 2 deletions tests/Assets/AssetContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,20 @@ public function it_gets_assets_in_a_folder_recursively()
});
}

/**
* @test
*
* @see https://github.com/statamic/cms/issues/8825
* @see https://github.com/statamic/cms/pull/8826
**/
public function it_doesnt_get_kebab_case_folder_assets_when_querying_snake_case_folder()
{
tap($this->containerWithDisk('snake-kebab')->assets('foo_bar', true), function ($assets) {
$this->assertCount(1, $assets);
$this->assertEquals('foo_bar/alfa.txt', $assets->first()->path());
});
}

/**
* @test
*
Expand Down Expand Up @@ -954,11 +968,11 @@ public function it_is_arrayable()
});
}

private function containerWithDisk()
private function containerWithDisk($fixture = 'container')
{
config(['filesystems.disks.test' => [
'driver' => 'local',
'root' => __DIR__.'/__fixtures__/container',
'root' => __DIR__.'/__fixtures__/'.$fixture,
]]);

$container = (new AssetContainer)->handle('test')->disk('test');
Expand Down
Empty file.
Empty file.
68 changes: 68 additions & 0 deletions tests/Data/Entries/EntryQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,4 +669,72 @@ public function entries_are_found_using_offset()
$this->assertCount(2, $entries);
$this->assertEquals(['Post 2', 'Post 3'], $entries->map->title->all());
}

/**
* @test
*
* @dataProvider likeProvider
*/
public function entries_are_found_using_like($like, $expected)
{
Collection::make('posts')->save();

collect([
'on',
'only',
'foo',
'food',
'boo',
'foo bar',
'foo_bar',
'foodbar',
'hello world',
'waterworld',
'world of warcraft',
'20%',
'20% of the time',
'20 something',
'Pi is 3.14159',
'Pi is not 3x14159',
'Use a [4.x] prefix for PRs',
'/',
'/ test',
'test /',
'test / test',
])->each(function ($val, $i) {
EntryFactory::id($i)
->slug('post-'.$i)
->collection('posts')
->data(['title' => $val])
->create();
});

$this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->get()->map->title->all());
}

public function likeProvider()
{
return collect([
'foo' => ['foo'],
'foo%' => ['foo', 'food', 'foo bar', 'foo_bar', 'foodbar'],
'%world' => ['hello world', 'waterworld'],
'%world%' => ['hello world', 'waterworld', 'world of warcraft'],
'_oo' => ['foo', 'boo'],
'o_' => ['on'],
'foo_bar' => ['foo bar', 'foo_bar', 'foodbar'],
'foo__bar' => [],
'fo__bar' => ['foo bar', 'foo_bar', 'foodbar'],
'foo\_bar' => ['foo_bar'],
'20\%' => ['20%'],
'20\%%' => ['20%', '20% of the time'],
'%3.14%' => ['Pi is 3.14159'],
'%[4%' => ['Use a [4.x] prefix for PRs'],
'/' => ['/'],
'%/' => ['/', 'test /'],
'/%' => ['/', '/ test'],
'%/%' => ['/', '/ test', 'test /', 'test / test'],
])->mapWithKeys(function ($expected, $like) {
return [$like => [$like, $expected]];
});
}
}
67 changes: 67 additions & 0 deletions tests/Facades/PatternTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Tests\Facades;

use Statamic\Facades\Pattern;
use Tests\TestCase;

class PatternTest extends TestCase
{
/**
* @test
*
* @dataProvider likeProvider
*/
public function it_escapes_sql_like_syntax($string, $expected)
{
$this->assertEquals($expected, Pattern::sqlLikeQuote($string));
}

public function likeProvider()
{
return collect([
'foo' => 'foo',
'%foo' => '\%foo',
'foo%' => 'foo\%',
'%foo%' => '\%foo\%',
'_foo' => '\_foo',
'foo_' => 'foo\_',
'_foo_' => '\_foo\_',
'f_o' => 'f\_o',
])->mapWithKeys(fn ($expected, $string) => [$string => [$string, $expected]])->all();
}

/**
* @test
*
* @dataProvider likeRegexProvider
*/
public function it_converts_sql_like_syntax_to_regex($string, $expected)
{
$this->assertEquals($expected, Pattern::sqlLikeToRegex($string));
}

public function likeRegexProvider()
{
return collect([
'foo' => '^foo$',
'foo%' => '^foo.*$',
'%world' => '^.*world$',
'%world%' => '^.*world.*$',
'_oo' => '^.oo$',
'o_' => '^o.$',
'foo_bar' => '^foo.bar$',
'foo__bar' => '^foo..bar$',
'fo__bar' => '^fo..bar$',
'foo\_bar' => '^foo_bar$',
'20\%' => '^20%$',
'20\%%' => '^20%.*$',
'%3.14%' => '^.*3\.14.*$',
'%[4%' => '^.*\[4.*$',
'/' => '^\/$',
'%/' => '^.*\/$',
'/%' => '^\/.*$',
'%/%' => '^.*\/.*$',
])->mapWithKeys(fn ($expected, $string) => [$string => [$string, $expected]])->all();
}
}

0 comments on commit 8f5da38

Please sign in to comment.