Skip to content

Commit 3538b06

Browse files
rvdsteegeCopilot
andcommitted
Replace regex-based icon validation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 18623e8 commit 3538b06

5 files changed

Lines changed: 178 additions & 10 deletions

File tree

src/AssetLocator.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ public function __construct( ImageService $image_service ) {
5151
public function locate( string $relative_path ): ?string {
5252
if (
5353
null === $this->methods_directory
54-
|| 1 !== \preg_match(
55-
'/\Amethods\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\/method-[a-z0-9-]+-640x360\.svg\z/D',
56-
$relative_path
57-
)
54+
|| ! IconValidator::is_valid_asset_path( $relative_path )
5855
) {
5956
return null;
6057
}

src/IconRegistrar.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function register(): void {
4848
$icons = [];
4949

5050
foreach ( IconCatalog::get_icons() as $slug => $icon ) {
51-
if ( 1 !== \preg_match( '/\A[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/D', $slug ) ) {
51+
if ( ! IconValidator::is_valid_slug( $slug ) ) {
5252
continue;
5353
}
5454

src/IconValidator.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Icon validator.
4+
*
5+
* @package Pronamic\Pay\Icons
6+
*/
7+
8+
namespace Pronamic\Pay\Icons;
9+
10+
/**
11+
* Validates icon slugs and asset paths.
12+
*/
13+
final class IconValidator {
14+
/**
15+
* Valid characters for icon slugs and asset names.
16+
*
17+
* @var string
18+
*/
19+
private const VALID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789-';
20+
21+
/**
22+
* Check whether an icon slug is valid.
23+
*
24+
* @param string $slug Icon slug.
25+
* @return bool True if the slug is valid.
26+
*/
27+
public static function is_valid_slug( string $slug ): bool {
28+
if (
29+
'' === $slug
30+
|| \str_starts_with( $slug, '-' )
31+
|| \str_ends_with( $slug, '-' )
32+
) {
33+
return false;
34+
}
35+
36+
return self::contains_only_valid_characters( $slug );
37+
}
38+
39+
/**
40+
* Check whether a package-relative icon asset path is valid.
41+
*
42+
* @param string $relative_path Package-relative asset path.
43+
* @return bool True if the asset path is valid.
44+
*/
45+
public static function is_valid_asset_path( string $relative_path ): bool {
46+
$parts = \explode( '/', $relative_path );
47+
48+
if (
49+
3 !== \count( $parts )
50+
|| 'methods' !== $parts[0]
51+
|| ! self::is_valid_slug( $parts[1] )
52+
) {
53+
return false;
54+
}
55+
56+
$filename = $parts[2];
57+
$prefix = 'method-';
58+
$suffix = '-640x360.svg';
59+
60+
if (
61+
! \str_starts_with( $filename, $prefix )
62+
|| ! \str_ends_with( $filename, $suffix )
63+
) {
64+
return false;
65+
}
66+
67+
$name = \substr(
68+
$filename,
69+
\strlen( $prefix ),
70+
-\strlen( $suffix )
71+
);
72+
73+
return self::contains_only_valid_characters( $name );
74+
}
75+
76+
/**
77+
* Check whether a value contains only valid characters.
78+
*
79+
* @param string $value Value.
80+
* @return bool True if the value is non-empty and contains only valid characters.
81+
*/
82+
private static function contains_only_valid_characters( string $value ): bool {
83+
return '' !== $value
84+
&& \strlen( $value ) === \strspn( $value, self::VALID_CHARACTERS );
85+
}
86+
}

tests/IconCatalogTest.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use PHPUnit\Framework\TestCase;
1111
use Pronamic\Pay\Icons\IconCatalog;
12+
use Pronamic\Pay\Icons\IconValidator;
1213

1314
/**
1415
* Tests the icon catalog.
@@ -28,12 +29,10 @@ public function test_catalog_is_deterministic_and_valid(): void {
2829
$this->assertSame( $sorted, $slugs );
2930

3031
foreach ( $icons as $slug => $icon ) {
31-
$this->assertMatchesRegularExpression( '/\A[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/D', $slug );
32+
$this->assertTrue( IconValidator::is_valid_slug( $slug ), $slug );
3233
$this->assertNotSame( '', $icon['label'] );
33-
$this->assertMatchesRegularExpression(
34-
'/\Amethods\/' . \preg_quote( $slug, '/' ) . '\/method-[a-z0-9-]+-640x360\.svg\z/D',
35-
$icon['path']
36-
);
34+
$this->assertTrue( IconValidator::is_valid_asset_path( $icon['path'] ), $icon['path'] );
35+
$this->assertSame( $slug, \explode( '/', $icon['path'] )[1] );
3736
}
3837
}
3938
}

tests/IconValidatorTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Icon validator tests.
4+
*
5+
* @package Pronamic\Pay\Icons
6+
*/
7+
8+
namespace Pronamic\Pay\Icons\Tests;
9+
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\TestCase;
12+
use Pronamic\Pay\Icons\IconValidator;
13+
14+
/**
15+
* Tests the icon validator.
16+
*/
17+
final class IconValidatorTest extends TestCase {
18+
/**
19+
* Test slug validation.
20+
*
21+
* @param string $slug Icon slug.
22+
* @param bool $expected Expected result.
23+
*/
24+
#[DataProvider( 'slug_provider' )]
25+
public function test_slug_validation( string $slug, bool $expected ): void {
26+
$this->assertSame( $expected, IconValidator::is_valid_slug( $slug ) );
27+
}
28+
29+
/**
30+
* Slug provider.
31+
*
32+
* @return array<string, array{string, bool}>
33+
*/
34+
public static function slug_provider(): array {
35+
return [
36+
'single letter' => [ 'a', true ],
37+
'single digit' => [ '0', true ],
38+
'hyphenated' => [ 'apple-pay', true ],
39+
'repeated hyphen' => [ 'apple--pay', true ],
40+
'empty' => [ '', false ],
41+
'leading hyphen' => [ '-apple', false ],
42+
'trailing hyphen' => [ 'apple-', false ],
43+
'uppercase' => [ 'Apple', false ],
44+
'underscore' => [ 'apple_pay', false ],
45+
'slash' => [ 'apple/pay', false ],
46+
'non-ASCII character' => [ 'äpple', false ],
47+
];
48+
}
49+
50+
/**
51+
* Test asset path validation.
52+
*
53+
* @param string $path Asset path.
54+
* @param bool $expected Expected result.
55+
*/
56+
#[DataProvider( 'asset_path_provider' )]
57+
public function test_asset_path_validation( string $path, bool $expected ): void {
58+
$this->assertSame( $expected, IconValidator::is_valid_asset_path( $path ) );
59+
}
60+
61+
/**
62+
* Asset path provider.
63+
*
64+
* @return array<string, array{string, bool}>
65+
*/
66+
public static function asset_path_provider(): array {
67+
return [
68+
'valid' => [ 'methods/apple-pay/method-apple-pay-640x360.svg', true ],
69+
'valid variant' => [ 'methods/ideal-in3/method-ideal-in3-on-white-640x360.svg', true ],
70+
'valid asset hyphen' => [ 'methods/a/method---640x360.svg', true ],
71+
'empty asset name' => [ 'methods/a/method--640x360.svg', false ],
72+
'empty method slug' => [ 'methods//method-apple-pay-640x360.svg', false ],
73+
'trailing slug hyphen' => [ 'methods/apple-/method-apple-640x360.svg', false ],
74+
'wrong directory' => [ 'providers/apple-pay/method-apple-pay-640x360.svg', false ],
75+
'extra path segment' => [ 'methods/apple-pay/icons/method-apple-pay-640x360.svg', false ],
76+
'uppercase asset name' => [ 'methods/apple-pay/method-Apple-Pay-640x360.svg', false ],
77+
'unsupported character' => [ 'methods/apple-pay/method-apple_pay-640x360.svg', false ],
78+
'wrong filename prefix' => [ 'methods/apple-pay/icon-apple-pay-640x360.svg', false ],
79+
'wrong dimensions' => [ 'methods/apple-pay/method-apple-pay-320x180.svg', false ],
80+
'wrong extension' => [ 'methods/apple-pay/method-apple-pay-640x360.png', false ],
81+
'extension suffix' => [ 'methods/apple-pay/method-apple-pay-640x360.svg.php', false ],
82+
'query string' => [ 'methods/apple-pay/method-apple-pay-640x360.svg?x=1', false ],
83+
'traversal' => [ 'methods/apple-pay/../method-apple-pay-640x360.svg', false ],
84+
];
85+
}
86+
}

0 commit comments

Comments
 (0)