Skip to content

Commit d8b7f62

Browse files
alekLexisfabpot
authored andcommitted
[Validator] Add option to allow ANY protocol in Assert\Url constraint
1 parent 68487bf commit d8b7f62

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

src/Symfony/Component/Validator/Constraints/Url.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Url extends Constraint
4040
public $normalizer;
4141

4242
/**
43-
* @param string[]|null $protocols The protocols considered to be valid for the URL (e.g. http, https, ftp, etc.) (defaults to ['http', 'https']
43+
* @param string[]|null $protocols The protocols considered to be valid for the URL (e.g. http, https, ftp, etc.) (defaults to ['http', 'https']; use ['*'] to allow any protocol or regex patterns like ['.*'] for custom matching)
4444
* @param bool|null $relativeProtocol Whether to accept URL without the protocol (i.e. //example.com) (defaults to false)
4545
* @param string[]|null $groups
4646
* @param bool|null $requireTld Whether to require the URL to include a top-level domain (defaults to false)

src/Symfony/Component/Validator/Constraints/UrlValidator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,15 @@ public function validate(mixed $value, Constraint $constraint): void
7373
$value = ($constraint->normalizer)($value);
7474
}
7575

76+
if (['*'] === $constraint->protocols) {
77+
// Use RFC 3986 compliant scheme pattern: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
78+
$protocols = '[a-zA-Z][a-zA-Z0-9+.-]*';
79+
} else {
80+
$protocols = implode('|', $constraint->protocols);
81+
}
82+
7683
$pattern = $constraint->relativeProtocol ? str_replace('(%s):', '(?:(%s):)?', static::PATTERN) : static::PATTERN;
77-
$pattern = \sprintf($pattern, implode('|', $constraint->protocols));
84+
$pattern = sprintf($pattern, $protocols);
7885

7986
if (!preg_match($pattern, $value)) {
8087
$this->context->buildViolation($constraint->message)

src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,115 @@ public function testValidRelativeUrlWithNewLine(string $url)
109109
->assertRaised();
110110
}
111111

112+
/**
113+
* Test that protocols: ['*'] allows any protocol
114+
*/
115+
public function testProtocolsWildcardAllowsAnyProtocol()
116+
{
117+
$constraint = new Url(protocols: ['*'], requireTld: false);
118+
119+
$validUrls = [
120+
'http://example.com',
121+
'https://example.com',
122+
'ftp://example.com',
123+
'custom://example.com',
124+
'myapp://example.com/path?query=1',
125+
'git+ssh://[email protected]/repo.git',
126+
'file://path/to/file',
127+
'scheme123://example.com',
128+
'a://example.com',
129+
];
130+
131+
foreach ($validUrls as $url) {
132+
$this->validator->validate($url, $constraint);
133+
$this->assertNoViolation();
134+
}
135+
}
136+
137+
/**
138+
* Test that protocols: ['*'] still validates scheme format according to RFC 3986
139+
*/
140+
public function testProtocolsWildcardRejectsInvalidSchemes()
141+
{
142+
$constraint = new Url(protocols: ['*'], requireTld: true);
143+
144+
$invalidUrls = [
145+
'123://example.com',
146+
'+scheme://example.com',
147+
'-scheme://example.com',
148+
'.scheme://example.com',
149+
'example.com',
150+
'://example.com',
151+
];
152+
153+
foreach ($invalidUrls as $url) {
154+
$this->setUp();
155+
$this->validator->validate($url, $constraint);
156+
157+
$this->buildViolation($constraint->message)
158+
->setParameter('{{ value }}', '"'.$url.'"')
159+
->setCode(Url::INVALID_URL_ERROR)
160+
->assertRaised();
161+
}
162+
}
163+
164+
/**
165+
* Test that protocols: ['*'] works with relativeProtocol
166+
*/
167+
public function testProtocolsWildcardWithRelativeProtocol()
168+
{
169+
$constraint = new Url(protocols: ['*'], relativeProtocol: true, requireTld: true);
170+
171+
$this->validator->validate('custom://example.com', $constraint);
172+
$this->assertNoViolation();
173+
174+
$this->validator->validate('//example.com', $constraint);
175+
$this->assertNoViolation();
176+
}
177+
178+
/**
179+
* Test that protocols: ['*'] works with requireTld
180+
*/
181+
public function testProtocolsWildcardWithRequireTld()
182+
{
183+
$constraint = new Url(protocols: ['*'], requireTld: true);
184+
185+
$this->validator->validate('custom://example.com', $constraint);
186+
$this->assertNoViolation();
187+
188+
$this->validator->validate('custom://localhost', $constraint);
189+
$this->buildViolation($constraint->tldMessage)
190+
->setParameter('{{ value }}', '"custom://localhost"')
191+
->setCode(Url::MISSING_TLD_ERROR)
192+
->assertRaised();
193+
}
194+
195+
/**
196+
* Test that protocols accepts regex patterns
197+
*/
198+
public function testProtocolsSupportsRegexPatterns()
199+
{
200+
$constraint = new Url(protocols: ['https?', 'custom.*'], requireTld: true);
201+
202+
$validUrls = [
203+
'http://example.com',
204+
'https://example.com',
205+
'custom://example.com',
206+
'customapp://example.com',
207+
];
208+
209+
foreach ($validUrls as $url) {
210+
$this->validator->validate($url, $constraint);
211+
$this->assertNoViolation();
212+
}
213+
214+
$this->validator->validate('ftp://example.com', $constraint);
215+
$this->buildViolation($constraint->message)
216+
->setParameter('{{ value }}', '"ftp://example.com"')
217+
->setCode(Url::INVALID_URL_ERROR)
218+
->assertRaised();
219+
}
220+
112221
public static function getValidRelativeUrls()
113222
{
114223
return [

0 commit comments

Comments
 (0)