Skip to content

Commit a2b7416

Browse files
Fix: Sanitize cache keys to avoid reserved characters validation error
1 parent e11223d commit a2b7416

File tree

5 files changed

+197
-10
lines changed

5 files changed

+197
-10
lines changed

src/Storage/CacheKeySanitizer.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JoliCode\MediaBundle\Storage;
6+
7+
use Symfony\Contracts\Cache\ItemInterface;
8+
9+
/**
10+
* Sanitizes cache keys to ensure they don't contain PSR-6 reserved characters.
11+
*
12+
* @see ItemInterface::RESERVED_CHARACTERS
13+
*/
14+
final class CacheKeySanitizer
15+
{
16+
/**
17+
* Sanitizes a string by replacing PSR-6 reserved characters with underscores.
18+
*
19+
* PSR-6 reserved characters are: {}()/\@:
20+
*
21+
* @param string $value The value to sanitize
22+
*
23+
* @return string The sanitized value safe to use in cache keys
24+
*/
25+
public static function sanitize(string $value): string
26+
{
27+
return str_replace(str_split(ItemInterface::RESERVED_CHARACTERS), '_', $value);
28+
}
29+
}

src/Storage/MediaPropertyAccessor.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ private function getCacheKey(string $path, string $property): string
8787
{
8888
return \sprintf(
8989
'joli_media_property_%s_%s_%s_%s',
90-
$this->libraryName,
91-
Resolver::normalizePath($path),
90+
CacheKeySanitizer::sanitize($this->libraryName),
91+
CacheKeySanitizer::sanitize(Resolver::normalizePath($path)),
9292
$this->getLastModified($path),
9393
$property,
9494
);
@@ -98,8 +98,8 @@ private function getLastModifiedCacheKey(string $path): string
9898
{
9999
return \sprintf(
100100
'joli_media_property_%s_%s_lastModified',
101-
$this->libraryName,
102-
Resolver::normalizePath($path),
101+
CacheKeySanitizer::sanitize($this->libraryName),
102+
CacheKeySanitizer::sanitize(Resolver::normalizePath($path)),
103103
);
104104
}
105105

src/Storage/MediaVariationPropertyAccessor.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ private function getCacheKey(string $path, Variation $variation, string $propert
8787
{
8888
return \sprintf(
8989
'joli_media_property_%s_%s_%s_%s_%s',
90-
$this->libraryName,
91-
$variation->getName(),
92-
Resolver::normalizePath($path),
90+
CacheKeySanitizer::sanitize($this->libraryName),
91+
CacheKeySanitizer::sanitize($variation->getName()),
92+
CacheKeySanitizer::sanitize(Resolver::normalizePath($path)),
9393
$this->getLastModified($path, $variation),
9494
$property,
9595
);
@@ -99,9 +99,9 @@ private function getLastModifiedCacheKey(string $path, Variation $variation): st
9999
{
100100
return \sprintf(
101101
'joli_media_property_%s_%s_%s_lastModified',
102-
$this->libraryName,
103-
$variation->getName(),
104-
Resolver::normalizePath($path),
102+
CacheKeySanitizer::sanitize($this->libraryName),
103+
CacheKeySanitizer::sanitize($variation->getName()),
104+
CacheKeySanitizer::sanitize(Resolver::normalizePath($path)),
105105
);
106106
}
107107

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JoliCode\MediaBundle\Tests\Storage;
6+
7+
use JoliCode\MediaBundle\Storage\CacheKeySanitizer;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class CacheKeySanitizerTest extends TestCase
12+
{
13+
#[DataProvider('provideSanitizationCases')]
14+
public function testSanitize(string $input, string $expected): void
15+
{
16+
$this->assertSame($expected, CacheKeySanitizer::sanitize($input));
17+
}
18+
19+
public static function provideSanitizationCases(): array
20+
{
21+
return [
22+
'simple path with slash' => ['folder/file.jpg', 'folder_file.jpg'],
23+
'path with all reserved characters' => ['path/with@special:chars{test}(1).jpg', 'path_with_special_chars_test__1_.jpg'],
24+
'backslash' => ['path\\file.jpg', 'path_file.jpg'],
25+
'colon' => ['namespace:key', 'namespace_key'],
26+
'at symbol' => ['user@host', 'user_host'],
27+
'curly braces' => ['prefix{suffix}', 'prefix_suffix_'],
28+
'parentheses' => ['name(variant)', 'name_variant_'],
29+
'mixed slashes' => ['folder/subfolder\\file.jpg', 'folder_subfolder_file.jpg'],
30+
'no reserved characters' => ['simple_key', 'simple_key'],
31+
'empty string' => ['', ''],
32+
];
33+
}
34+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JoliCode\MediaBundle\Tests\Storage;
6+
7+
use JoliCode\MediaBundle\Binary\MimeTypeGuesser;
8+
use JoliCode\MediaBundle\Model\Format;
9+
use JoliCode\MediaBundle\Storage\MediaPropertyAccessor;
10+
use JoliCode\MediaBundle\Storage\MediaVariationPropertyAccessor;
11+
use JoliCode\MediaBundle\Storage\Strategy\FolderStorageStrategy;
12+
use JoliCode\MediaBundle\Transformer\TransformerChain;
13+
use JoliCode\MediaBundle\Variation\Variation;
14+
use League\Flysystem\Filesystem;
15+
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
18+
use Symfony\Component\Mime\FileBinaryMimeTypeGuesser;
19+
use Symfony\Component\Mime\MimeTypes;
20+
21+
/**
22+
* Tests that cache keys are properly sanitized when using paths with reserved PSR-6 characters.
23+
*
24+
* This test ensures that files in subfolders (containing forward slashes) don't cause
25+
* cache key validation errors.
26+
*/
27+
class CacheKeyWithSubfolderTest extends TestCase
28+
{
29+
private ArrayAdapter $cache;
30+
private Filesystem $filesystem;
31+
private MimeTypeGuesser $mimeTypeGuesser;
32+
33+
protected function setUp(): void
34+
{
35+
parent::setUp();
36+
37+
$this->cache = new ArrayAdapter();
38+
$this->filesystem = new Filesystem(new InMemoryFilesystemAdapter());
39+
$this->mimeTypeGuesser = new MimeTypeGuesser(new MimeTypes(), new FileBinaryMimeTypeGuesser());
40+
}
41+
42+
public function testSubfolderPath(): void
43+
{
44+
// Create a file in a subfolder (path contains '/' which is a reserved PSR-6 character)
45+
$path = 'subfolder/test.jpg';
46+
$this->filesystem->write($path, 'dummy image content');
47+
48+
$accessor = new MediaPropertyAccessor(
49+
'default',
50+
$this->filesystem,
51+
$this->mimeTypeGuesser,
52+
$this->cache,
53+
);
54+
55+
// This should NOT throw an exception about reserved characters
56+
$mimeType = $accessor->getMimeType($path);
57+
$this->assertIsString($mimeType);
58+
59+
$format = $accessor->getFormat($path);
60+
$this->assertIsString($format);
61+
62+
$fileSize = $accessor->getFileSize($path);
63+
$this->assertGreaterThan(0, $fileSize);
64+
65+
// Test clearCache doesn't throw either
66+
$accessor->clearCache($path);
67+
$this->assertTrue(true); // If we reach here, no exception was thrown
68+
}
69+
70+
public function testVariationSubfolderPath(): void
71+
{
72+
$strategy = new FolderStorageStrategy();
73+
74+
// Create a file in a nested subfolder
75+
$path = 'folder/subfolder/image.png';
76+
$this->filesystem->write($path, 'dummy image content');
77+
78+
$variation = new Variation('thumbnail', Format::PNG, new TransformerChain([]));
79+
80+
$accessor = new MediaVariationPropertyAccessor(
81+
'my-library',
82+
$strategy,
83+
$this->filesystem,
84+
$this->mimeTypeGuesser,
85+
$this->cache,
86+
);
87+
88+
// This should NOT throw an exception about reserved characters
89+
$mimeType = $accessor->getMimeType($path, $variation);
90+
$this->assertIsString($mimeType);
91+
92+
$format = $accessor->getFormat($path, $variation);
93+
$this->assertIsString($format);
94+
95+
$fileSize = $accessor->getFileSize($path, $variation);
96+
$this->assertIsInt($fileSize);
97+
98+
// Test clearCache doesn't throw either
99+
$accessor->clearCache($path, $variation);
100+
$this->assertTrue(true); // If we reach here, no exception was thrown
101+
}
102+
103+
public function testNestedSubfolderPath(): void
104+
{
105+
// Path with forward slash (subfolder)
106+
$path = 'user-uploads/2024/document.pdf';
107+
$this->filesystem->write($path, 'dummy pdf content');
108+
109+
$accessor = new MediaPropertyAccessor(
110+
'default',
111+
$this->filesystem,
112+
$this->mimeTypeGuesser,
113+
$this->cache,
114+
);
115+
116+
// Should work without throwing PSR-6 cache key validation errors
117+
$mimeType = $accessor->getMimeType($path);
118+
$this->assertIsString($mimeType);
119+
120+
// Verify the cache was actually used by calling again
121+
$mimeType2 = $accessor->getMimeType($path);
122+
$this->assertSame($mimeType, $mimeType2);
123+
}
124+
}

0 commit comments

Comments
 (0)