Skip to content

Commit 18676b4

Browse files
authored
Adds Str::isUrl() and use it from the validator (#5914)
* Adds `Str::isUrl()` and use it from the validator * Update CHANGELOG-3.1.md
1 parent c863b0c commit 18676b4

File tree

3 files changed

+327
-0
lines changed

3 files changed

+327
-0
lines changed

src/Str.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,11 @@ public static function mask(string $string, int $offset = 0, int $length = 0, st
755755
return mb_substr($string, 0, max($stringLength - $hiddenLength - $absOffset, 0)) . str_repeat($replacement, $hiddenLength) . mb_substr($string, $offset);
756756
}
757757

758+
/**
759+
* Determine if a given value is a valid ULID.
760+
*
761+
* @param mixed $value
762+
*/
758763
public static function isUlid($value): bool
759764
{
760765
if (! is_string($value)) {
@@ -781,6 +786,46 @@ public static function ulid(?DateTimeInterface $time = null): Ulid
781786
return new Ulid(Ulid::generate($time));
782787
}
783788

789+
/**
790+
* Determine if a given value is a valid URL.
791+
*
792+
* @param mixed $value
793+
* @return bool
794+
*/
795+
public static function isUrl($value)
796+
{
797+
if (! is_string($value)) {
798+
return false;
799+
}
800+
/*
801+
* This pattern is derived from Symfony\Component\Validator\Constraints\UrlValidator (2.7.4).
802+
*
803+
* (c) Fabien Potencier <[email protected]> http://symfony.com
804+
*/
805+
$pattern = '~^
806+
((aaa|aaas|about|acap|acct|acr|adiumxtra|afp|afs|aim|apt|attachment|aw|barion|beshare|bitcoin|blob|bolo|callto|cap|chrome|chrome-extension|cid|coap|coaps|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-playcontainer|dlna-playsingle|dns|dntp|dtn|dvb|ed2k|example|facetime|fax|feed|feedready|file|filesystem|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|ham|hcp|http|https|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris.beep|iris.lwz|iris.xpc|iris.xpcs|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|ms-help|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|msnim|msrp|msrps|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|oid|opaquelocktoken|pack|palm|paparazzi|pkcs11|platform|pop|pres|prospero|proxy|psyc|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|secondlife|s3|service|session|sftp|sgn|shttp|sieve|sip|sips|skype|smb|sms|smtp|snews|snmp|soap.beep|soap.beeps|soldat|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|things|thismessage|tip|tn3270|turn|turns|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|videotex|view-source|wais|webcal|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s)):// # protocol
807+
(([\pL\pN-]+:)?([\pL\pN-]+)@)? # basic auth
808+
(
809+
([\pL\pN\pS\-\.])+(\.?([\pL]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
810+
| # or
811+
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
812+
| # or
813+
\[
814+
(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
815+
\] # an IPv6 address
816+
)
817+
(:[0-9]+)? # a port (optional)
818+
(/?|/\S+|\?\S*|\#\S*) # a /, nothing, a / with something, a query or a fragment
819+
$~ixu';
820+
821+
return preg_match($pattern, $value) > 0;
822+
}
823+
824+
/**
825+
* Determine if a given value is a valid UUID.
826+
*
827+
* @param mixed $value
828+
*/
784829
public static function isUuid($value): bool
785830
{
786831
if (! is_string($value)) {

src/Stringable.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ public function isUlid(): bool
319319
return Str::isUlid($this->value);
320320
}
321321

322+
public function isUrl(): bool
323+
{
324+
return Str::isUrl($this->value);
325+
}
326+
322327
public function isUuid(): bool
323328
{
324329
return Str::isUuid($this->value);

tests/StrTest.php

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,281 @@ public function testStudly()
284284
$this->assertSame('HelloWorld', StrCache::studly('hello world'));
285285
$this->assertSame('Hello-World', StrCache::studly('hello world', '-'));
286286
}
287+
288+
/**
289+
* @param mixed $validUrl
290+
*/
291+
#[\PHPUnit\Framework\Attributes\DataProvider('validUrls')]
292+
public function testValidUrls($url)
293+
{
294+
$this->assertTrue(Str::isUrl($url));
295+
}
296+
297+
/**
298+
* @param mixed $invalidUrl
299+
*/
300+
#[\PHPUnit\Framework\Attributes\DataProvider('invalidUrls')]
301+
public function testInvalidUrls($url)
302+
{
303+
$this->assertFalse(Str::isUrl($url));
304+
}
305+
306+
public static function validUrls()
307+
{
308+
return [
309+
['aaa://fully.qualified.domain/path'],
310+
['aaas://fully.qualified.domain/path'],
311+
['about://fully.qualified.domain/path'],
312+
['acap://fully.qualified.domain/path'],
313+
['acct://fully.qualified.domain/path'],
314+
['acr://fully.qualified.domain/path'],
315+
['adiumxtra://fully.qualified.domain/path'],
316+
['afp://fully.qualified.domain/path'],
317+
['afs://fully.qualified.domain/path'],
318+
['aim://fully.qualified.domain/path'],
319+
['apt://fully.qualified.domain/path'],
320+
['attachment://fully.qualified.domain/path'],
321+
['aw://fully.qualified.domain/path'],
322+
['barion://fully.qualified.domain/path'],
323+
['beshare://fully.qualified.domain/path'],
324+
['bitcoin://fully.qualified.domain/path'],
325+
['blob://fully.qualified.domain/path'],
326+
['bolo://fully.qualified.domain/path'],
327+
['callto://fully.qualified.domain/path'],
328+
['cap://fully.qualified.domain/path'],
329+
['chrome://fully.qualified.domain/path'],
330+
['chrome-extension://fully.qualified.domain/path'],
331+
['cid://fully.qualified.domain/path'],
332+
['coap://fully.qualified.domain/path'],
333+
['coaps://fully.qualified.domain/path'],
334+
['com-eventbrite-attendee://fully.qualified.domain/path'],
335+
['content://fully.qualified.domain/path'],
336+
['crid://fully.qualified.domain/path'],
337+
['cvs://fully.qualified.domain/path'],
338+
['data://fully.qualified.domain/path'],
339+
['dav://fully.qualified.domain/path'],
340+
['dict://fully.qualified.domain/path'],
341+
['dlna-playcontainer://fully.qualified.domain/path'],
342+
['dlna-playsingle://fully.qualified.domain/path'],
343+
['dns://fully.qualified.domain/path'],
344+
['dntp://fully.qualified.domain/path'],
345+
['dtn://fully.qualified.domain/path'],
346+
['dvb://fully.qualified.domain/path'],
347+
['ed2k://fully.qualified.domain/path'],
348+
['example://fully.qualified.domain/path'],
349+
['facetime://fully.qualified.domain/path'],
350+
['fax://fully.qualified.domain/path'],
351+
['feed://fully.qualified.domain/path'],
352+
['feedready://fully.qualified.domain/path'],
353+
['file://fully.qualified.domain/path'],
354+
['filesystem://fully.qualified.domain/path'],
355+
['finger://fully.qualified.domain/path'],
356+
['fish://fully.qualified.domain/path'],
357+
['ftp://fully.qualified.domain/path'],
358+
['geo://fully.qualified.domain/path'],
359+
['gg://fully.qualified.domain/path'],
360+
['git://fully.qualified.domain/path'],
361+
['gizmoproject://fully.qualified.domain/path'],
362+
['go://fully.qualified.domain/path'],
363+
['gopher://fully.qualified.domain/path'],
364+
['gtalk://fully.qualified.domain/path'],
365+
['h323://fully.qualified.domain/path'],
366+
['ham://fully.qualified.domain/path'],
367+
['hcp://fully.qualified.domain/path'],
368+
['http://fully.qualified.domain/path'],
369+
['https://fully.qualified.domain/path'],
370+
['iax://fully.qualified.domain/path'],
371+
['icap://fully.qualified.domain/path'],
372+
['icon://fully.qualified.domain/path'],
373+
['im://fully.qualified.domain/path'],
374+
['imap://fully.qualified.domain/path'],
375+
['info://fully.qualified.domain/path'],
376+
['iotdisco://fully.qualified.domain/path'],
377+
['ipn://fully.qualified.domain/path'],
378+
['ipp://fully.qualified.domain/path'],
379+
['ipps://fully.qualified.domain/path'],
380+
['irc://fully.qualified.domain/path'],
381+
['irc6://fully.qualified.domain/path'],
382+
['ircs://fully.qualified.domain/path'],
383+
['iris://fully.qualified.domain/path'],
384+
['iris.beep://fully.qualified.domain/path'],
385+
['iris.lwz://fully.qualified.domain/path'],
386+
['iris.xpc://fully.qualified.domain/path'],
387+
['iris.xpcs://fully.qualified.domain/path'],
388+
['itms://fully.qualified.domain/path'],
389+
['jabber://fully.qualified.domain/path'],
390+
['jar://fully.qualified.domain/path'],
391+
['jms://fully.qualified.domain/path'],
392+
['keyparc://fully.qualified.domain/path'],
393+
['lastfm://fully.qualified.domain/path'],
394+
['ldap://fully.qualified.domain/path'],
395+
['ldaps://fully.qualified.domain/path'],
396+
['magnet://fully.qualified.domain/path'],
397+
['mailserver://fully.qualified.domain/path'],
398+
['mailto://fully.qualified.domain/path'],
399+
['maps://fully.qualified.domain/path'],
400+
['market://fully.qualified.domain/path'],
401+
['message://fully.qualified.domain/path'],
402+
['mid://fully.qualified.domain/path'],
403+
['mms://fully.qualified.domain/path'],
404+
['modem://fully.qualified.domain/path'],
405+
['ms-help://fully.qualified.domain/path'],
406+
['ms-settings://fully.qualified.domain/path'],
407+
['ms-settings-airplanemode://fully.qualified.domain/path'],
408+
['ms-settings-bluetooth://fully.qualified.domain/path'],
409+
['ms-settings-camera://fully.qualified.domain/path'],
410+
['ms-settings-cellular://fully.qualified.domain/path'],
411+
['ms-settings-cloudstorage://fully.qualified.domain/path'],
412+
['ms-settings-emailandaccounts://fully.qualified.domain/path'],
413+
['ms-settings-language://fully.qualified.domain/path'],
414+
['ms-settings-location://fully.qualified.domain/path'],
415+
['ms-settings-lock://fully.qualified.domain/path'],
416+
['ms-settings-nfctransactions://fully.qualified.domain/path'],
417+
['ms-settings-notifications://fully.qualified.domain/path'],
418+
['ms-settings-power://fully.qualified.domain/path'],
419+
['ms-settings-privacy://fully.qualified.domain/path'],
420+
['ms-settings-proximity://fully.qualified.domain/path'],
421+
['ms-settings-screenrotation://fully.qualified.domain/path'],
422+
['ms-settings-wifi://fully.qualified.domain/path'],
423+
['ms-settings-workplace://fully.qualified.domain/path'],
424+
['msnim://fully.qualified.domain/path'],
425+
['msrp://fully.qualified.domain/path'],
426+
['msrps://fully.qualified.domain/path'],
427+
['mtqp://fully.qualified.domain/path'],
428+
['mumble://fully.qualified.domain/path'],
429+
['mupdate://fully.qualified.domain/path'],
430+
['mvn://fully.qualified.domain/path'],
431+
['news://fully.qualified.domain/path'],
432+
['nfs://fully.qualified.domain/path'],
433+
['ni://fully.qualified.domain/path'],
434+
['nih://fully.qualified.domain/path'],
435+
['nntp://fully.qualified.domain/path'],
436+
['notes://fully.qualified.domain/path'],
437+
['oid://fully.qualified.domain/path'],
438+
['opaquelocktoken://fully.qualified.domain/path'],
439+
['pack://fully.qualified.domain/path'],
440+
['palm://fully.qualified.domain/path'],
441+
['paparazzi://fully.qualified.domain/path'],
442+
['pkcs11://fully.qualified.domain/path'],
443+
['platform://fully.qualified.domain/path'],
444+
['pop://fully.qualified.domain/path'],
445+
['pres://fully.qualified.domain/path'],
446+
['prospero://fully.qualified.domain/path'],
447+
['proxy://fully.qualified.domain/path'],
448+
['psyc://fully.qualified.domain/path'],
449+
['query://fully.qualified.domain/path'],
450+
['redis://fully.qualified.domain/path'],
451+
['rediss://fully.qualified.domain/path'],
452+
['reload://fully.qualified.domain/path'],
453+
['res://fully.qualified.domain/path'],
454+
['resource://fully.qualified.domain/path'],
455+
['rmi://fully.qualified.domain/path'],
456+
['rsync://fully.qualified.domain/path'],
457+
['rtmfp://fully.qualified.domain/path'],
458+
['rtmp://fully.qualified.domain/path'],
459+
['rtsp://fully.qualified.domain/path'],
460+
['rtsps://fully.qualified.domain/path'],
461+
['rtspu://fully.qualified.domain/path'],
462+
['s3://fully.qualified.domain/path'],
463+
['secondlife://fully.qualified.domain/path'],
464+
['service://fully.qualified.domain/path'],
465+
['session://fully.qualified.domain/path'],
466+
['sftp://fully.qualified.domain/path'],
467+
['sgn://fully.qualified.domain/path'],
468+
['shttp://fully.qualified.domain/path'],
469+
['sieve://fully.qualified.domain/path'],
470+
['sip://fully.qualified.domain/path'],
471+
['sips://fully.qualified.domain/path'],
472+
['skype://fully.qualified.domain/path'],
473+
['smb://fully.qualified.domain/path'],
474+
['sms://fully.qualified.domain/path'],
475+
['smtp://fully.qualified.domain/path'],
476+
['snews://fully.qualified.domain/path'],
477+
['snmp://fully.qualified.domain/path'],
478+
['soap.beep://fully.qualified.domain/path'],
479+
['soap.beeps://fully.qualified.domain/path'],
480+
['soldat://fully.qualified.domain/path'],
481+
['spotify://fully.qualified.domain/path'],
482+
['ssh://fully.qualified.domain/path'],
483+
['steam://fully.qualified.domain/path'],
484+
['stun://fully.qualified.domain/path'],
485+
['stuns://fully.qualified.domain/path'],
486+
['submit://fully.qualified.domain/path'],
487+
['svn://fully.qualified.domain/path'],
488+
['tag://fully.qualified.domain/path'],
489+
['teamspeak://fully.qualified.domain/path'],
490+
['tel://fully.qualified.domain/path'],
491+
['teliaeid://fully.qualified.domain/path'],
492+
['telnet://fully.qualified.domain/path'],
493+
['tftp://fully.qualified.domain/path'],
494+
['things://fully.qualified.domain/path'],
495+
['thismessage://fully.qualified.domain/path'],
496+
['tip://fully.qualified.domain/path'],
497+
['tn3270://fully.qualified.domain/path'],
498+
['turn://fully.qualified.domain/path'],
499+
['turns://fully.qualified.domain/path'],
500+
['tv://fully.qualified.domain/path'],
501+
['udp://fully.qualified.domain/path'],
502+
['unreal://fully.qualified.domain/path'],
503+
['urn://fully.qualified.domain/path'],
504+
['ut2004://fully.qualified.domain/path'],
505+
['vemmi://fully.qualified.domain/path'],
506+
['ventrilo://fully.qualified.domain/path'],
507+
['videotex://fully.qualified.domain/path'],
508+
['view-source://fully.qualified.domain/path'],
509+
['wais://fully.qualified.domain/path'],
510+
['webcal://fully.qualified.domain/path'],
511+
['ws://fully.qualified.domain/path'],
512+
['wss://fully.qualified.domain/path'],
513+
['wtai://fully.qualified.domain/path'],
514+
['wyciwyg://fully.qualified.domain/path'],
515+
['xcon://fully.qualified.domain/path'],
516+
['xcon-userid://fully.qualified.domain/path'],
517+
['xfire://fully.qualified.domain/path'],
518+
['xmlrpc.beep://fully.qualified.domain/path'],
519+
['xmlrpc.beeps://fully.qualified.domain/path'],
520+
['xmpp://fully.qualified.domain/path'],
521+
['xri://fully.qualified.domain/path'],
522+
['ymsgr://fully.qualified.domain/path'],
523+
['z39.50://fully.qualified.domain/path'],
524+
['z39.50r://fully.qualified.domain/path'],
525+
['z39.50s://fully.qualified.domain/path'],
526+
['http://a.pl'],
527+
['http://localhost/url.php'],
528+
['http://local.dev'],
529+
['http://google.com'],
530+
['http://www.google.com'],
531+
['https://google.com'],
532+
['http://illuminate.dev'],
533+
['http://localhost'],
534+
['https://laravel.com/?'],
535+
['http://президент.рф/'],
536+
['http://스타벅스코리아.com'],
537+
['http://xn--d1abbgf6aiiy.xn--p1ai/'],
538+
['https://laravel.com?'],
539+
['https://laravel.com?q=1'],
540+
['https://laravel.com/?q=1'],
541+
['https://laravel.com#'],
542+
['https://laravel.com#fragment'],
543+
['https://laravel.com/#fragment'],
544+
];
545+
}
546+
547+
public static function invalidUrls()
548+
{
549+
return [
550+
['aslsdlks'],
551+
['google.com'],
552+
['://google.com'],
553+
['http ://google.com'],
554+
['http:/google.com'],
555+
['http://goog_le.com'],
556+
['http://google.com::aa'],
557+
['http://google.com:aa'],
558+
['http://127.0.0.1:aa'],
559+
['http://[::1'],
560+
['foo://bar'],
561+
['javascript://test%0Aalert(321)'],
562+
];
563+
}
287564
}

0 commit comments

Comments
 (0)