diff --git a/src/Phaseolies/Helpers/helpers.php b/src/Phaseolies/Helpers/helpers.php index 8b081e18..0617c08f 100644 --- a/src/Phaseolies/Helpers/helpers.php +++ b/src/Phaseolies/Helpers/helpers.php @@ -2,7 +2,7 @@ use Phaseolies\Utilities\Paginator; use Phaseolies\Translation\Translator; -use Phaseolies\Support\UrlGenerator; +use Phaseolies\Support\Contracts\UrlGeneratorInterface; use Phaseolies\Support\StringService; use Phaseolies\Support\Facades\Log; use Phaseolies\Support\Facades\Crypt; @@ -194,11 +194,11 @@ function lang(): Translator * @param string|null $path * @param mixed $parameters * @param bool|null $secure - * @return ($path is null ? \Phaseolies\Support\UrlGenerator : string) + * @return ($path is null ? \Phaseolies\Support\Contracts\UrlGeneratorInterface : string) */ function url(?string $path = null, $parameters = [], ?bool $secure = null) { - $urlGenerator = app(UrlGenerator::class); + $urlGenerator = app(UrlGeneratorInterface::class); if (is_null($path)) { return $urlGenerator; diff --git a/src/Phaseolies/Support/Contracts/UrlGeneratorInterface.php b/src/Phaseolies/Support/Contracts/UrlGeneratorInterface.php index e499af3a..30c0198f 100644 --- a/src/Phaseolies/Support/Contracts/UrlGeneratorInterface.php +++ b/src/Phaseolies/Support/Contracts/UrlGeneratorInterface.php @@ -14,7 +14,7 @@ interface UrlGeneratorInterface public function enqueue(string $path = '/', ?bool $secure = null): string; /** - * Generate the current full URL without query parameters. + * Generate the current full URL, including query parameters. * * @return string */ @@ -92,10 +92,10 @@ public function signed(string $path = '/', array|string|int $parameters = [], in /** * Determine whether a value is a valid URL. * - * @param string $path + * @param mixed $path * @return bool */ - public function isValid(string $path): bool; + public function isValid(mixed $path): bool; /** * Get the configured base URL. diff --git a/src/Phaseolies/Support/Facades/URL.php b/src/Phaseolies/Support/Facades/URL.php index 99635aaa..e43e417e 100644 --- a/src/Phaseolies/Support/Facades/URL.php +++ b/src/Phaseolies/Support/Facades/URL.php @@ -5,21 +5,21 @@ use Phaseolies\Facade\BaseFacade; /** - * @method static string enqueue(string $path = '/', $secure = null) + * @method static string enqueue(string $path = '/', ?bool $secure = null) * @method static string full() * @method static string current() * @method static string route(string $name, array|string|int $parameters = [], ?bool $secure = null) - * @method static \Phaseolies\Support\UrlGenerator to($path = '/') - * @method static \Phaseolies\Support\UrlGenerator withQuery(array|string $query = []) - * @method static \Phaseolies\Support\UrlGenerator withSignature($expiration = 3600) - * @method static \Phaseolies\Support\UrlGenerator withFragment($fragment = '') + * @method static \Phaseolies\Support\Contracts\UrlGeneratorInterface to(string $path = '/', array|string|int $parameters = [], ?bool $secure = null) + * @method static \Phaseolies\Support\Contracts\UrlGeneratorInterface withQuery(array|string $query = []) + * @method static \Phaseolies\Support\Contracts\UrlGeneratorInterface withSignature(int $expiration = 3600) + * @method static \Phaseolies\Support\Contracts\UrlGeneratorInterface withFragment(string $fragment = '') * @method static string make() * @method static string signed(string $path = '/', array|string|int $parameters = [], int $expiration = 3600, ?bool $secure = null) - * @method static bool isValid(string $url) + * @method static bool isValid(mixed $url) * @method static string base() - * @method static \Phaseolies\Support\UrlGenerator setSecure(bool $secure) + * @method static \Phaseolies\Support\Contracts\UrlGeneratorInterface setSecure(bool $secure) * - * @see \Phaseolies\Support\UrlGenerator + * @see \Phaseolies\Support\Contracts\UrlGeneratorInterface */ class URL extends BaseFacade { diff --git a/src/Phaseolies/Support/UrlGenerator.php b/src/Phaseolies/Support/UrlGenerator.php index 729da171..92289c82 100644 --- a/src/Phaseolies/Support/UrlGenerator.php +++ b/src/Phaseolies/Support/UrlGenerator.php @@ -104,7 +104,7 @@ public function enqueue(string $path = '/', ?bool $secure = null): string } /** - * Generate a full URL without query parameters. + * Generate the current full URL, including query parameters. * * @return string */ @@ -242,9 +242,24 @@ public function make(): string $url .= '#' . $this->fragment; } + $this->resetBuilderState(); + return $url; } + /** + * Reset the per-URL builder state + * + * @return void + */ + protected function resetBuilderState(): void + { + $this->path = '/'; + $this->query = []; + $this->fragment = ''; + $this->expiration = 0; + } + /** * Generate a signed URL. * @@ -282,11 +297,15 @@ protected function createSignature(array $parameters): string * This method is taken from PHP Laravel framework * Determine if the given path is a valid URL. * - * @param string $path + * @param mixed $path * @return bool */ - public function isValid(string $path): bool + public function isValid(mixed $path): bool { + if (! is_string($path)) { + return false; + } + if (! preg_match('~^(#|//|https?://|(mailto|tel|sms):)~', $path)) { return filter_var($path, FILTER_VALIDATE_URL) !== false; } diff --git a/tests/UrlGeneratorTest.php b/tests/UrlGeneratorTest.php index 4f48f5a3..ee2c4cf5 100644 --- a/tests/UrlGeneratorTest.php +++ b/tests/UrlGeneratorTest.php @@ -62,6 +62,37 @@ public function testApplicationBindsTheUrlContractAndLegacyServiceNameToTheSameS } } + public function testIsValidReturnsFalseForNonStringValues(): void + { + $this->assertFalse($this->urlGenerator->isValid(null)); + $this->assertFalse($this->urlGenerator->isValid(123)); + $this->assertFalse($this->urlGenerator->isValid(['https://example.com'])); + } + + public function testBuilderStateDoesNotLeakBetweenUrls(): void + { + $this->urlGenerator->to('first')->withQuery(['id' => 1])->withFragment('top')->make(); + + $this->assertEquals('http://localhost/second', $this->urlGenerator->to('second')->make()); + } + + public function testUrlHelperResolvesTheSharedSingleton(): void + { + $app = $this->getMockBuilder(Application::class)->disableOriginalConstructor()->onlyMethods([])->getMock(); + (new ReflectionClass(Container::class))->getConstructor()->invoke($app); + $previous = Container::getInstance(); + Container::setInstance($app); + + try { + putenv('APP_URL=http://localhost'); + (new FacadeLauncher($app))->register(); + + $this->assertSame($app->get('url'), url()); + } finally { + Container::setInstance($previous); + } + } + protected function tearDown(): void { unset($_SERVER['HTTP_HOST']);