From 31bdf72577e852c376b4501e7f346c1904e2378c Mon Sep 17 00:00:00 2001 From: Darius Matulionis Date: Fri, 12 Jun 2026 13:54:35 +0200 Subject: [PATCH 1/4] Small improvements --- config/l5-swagger.php | 7 ++- resources/views/index.blade.php | 16 +++---- src/Generator.php | 1 + .../Controllers/SwaggerAssetController.php | 12 +++-- src/Http/Controllers/SwaggerController.php | 19 +++++++- src/helpers.php | 20 +++++--- tests/Unit/HelpersTest.php | 15 +++++- tests/Unit/RoutesTest.php | 46 +++++++++++++++++++ 8 files changed, 116 insertions(+), 20 deletions(-) diff --git a/config/l5-swagger.php b/config/l5-swagger.php index ff4c8a4..b31e01a 100644 --- a/config/l5-swagger.php +++ b/config/l5-swagger.php @@ -62,7 +62,12 @@ 'oauth2_callback' => 'api/oauth2-callback', /* - * Middleware allows to prevent unexpected access to API documentation + * Middleware allows to prevent unexpected access to API documentation. + * + * WARNING: By default these are empty, meaning your API docs are publicly + * accessible. For production deployments, add authentication middleware + * to restrict access, e.g.: + * 'api' => ['auth:sanctum'], */ 'middleware' => [ 'api' => [], diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index 4f57040..605658a 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -126,17 +126,17 @@ const urls = []; @foreach($urlsToDocs as $title => $url) - urls.push({name: "{{ $title }}", url: "{{ $url }}"}); + urls.push({name: @json($title), url: @json($url)}); @endforeach // Build a system const ui = SwaggerUIBundle({ dom_id: '#swagger-ui', urls: urls, - "urls.primaryName": "{{ $documentationTitle }}", - operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!}, - configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!}, - validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!}, + "urls.primaryName": @json($documentationTitle), + operationsSorter: @json($operationsSorter ?? null), + configUrl: @json($configUrl ?? null), + validatorUrl: @json($validatorUrl ?? null), oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}", requestInterceptor: function(request) { @@ -154,10 +154,10 @@ ], layout: "StandaloneLayout", - docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}", + docExpansion : @json(config('l5-swagger.defaults.ui.display.doc_expansion', 'none')), deepLinking: true, filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!}, - persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}", + persistAuthorization: @json((bool) config('l5-swagger.defaults.ui.authorization.persist_authorization')), }) @@ -165,7 +165,7 @@ @if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type'))) ui.initOAuth({ - usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}" + usePkceWithAuthorizationCodeGrant: @json((bool) config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant')) }) @endif } diff --git a/src/Generator.php b/src/Generator.php index 9a14a20..a3ef4cf 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; use L5Swagger\Exceptions\L5SwaggerException; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Server; diff --git a/src/Http/Controllers/SwaggerAssetController.php b/src/Http/Controllers/SwaggerAssetController.php index fbbeaff..327a3e4 100644 --- a/src/Http/Controllers/SwaggerAssetController.php +++ b/src/Http/Controllers/SwaggerAssetController.php @@ -32,13 +32,19 @@ public function index(Request $request): Response try { $path = swagger_ui_dist_path($documentation, $asset); + $mimeTypes = [ + 'css' => 'text/css', + 'js' => 'application/javascript', + 'png' => 'image/png', + 'html' => 'text/html', + ]; + $ext = pathinfo($asset, PATHINFO_EXTENSION); + return (new Response( $fileSystem->get($path), 200, [ - 'Content-Type' => (isset(pathinfo($asset)['extension']) && pathinfo($asset)['extension'] === 'css') - ? 'text/css' - : 'application/javascript', + 'Content-Type' => $mimeTypes[$ext] ?? 'application/octet-stream', ] ))->setSharedMaxAge(31536000) ->setMaxAge(31536000) diff --git a/src/Http/Controllers/SwaggerController.php b/src/Http/Controllers/SwaggerController.php index 4dca308..acf4fca 100644 --- a/src/Http/Controllers/SwaggerController.php +++ b/src/Http/Controllers/SwaggerController.php @@ -47,6 +47,10 @@ public function docs(Request $request): Response ); if ($config['generate_always']) { + if (app()->environment('production')) { + Log::warning('L5-Swagger: generate_always is enabled in production, which may impact performance'); + } + $generator = $this->generatorFactory->make($documentation); try { @@ -109,6 +113,19 @@ public function api(Request $request): Response ); } + $configUrl = $config['additional_config_url'] ?? null; + + if ($configUrl !== null) { + $scheme = parse_url($configUrl, PHP_URL_SCHEME); + + if (! in_array($scheme, ['http', 'https'], true)) { + Log::warning('L5-Swagger: additional_config_url has an invalid scheme and was ignored', [ + 'url' => $configUrl, + ]); + $configUrl = null; + } + } + $urlToDocs = $this->generateDocumentationFileURL($documentation, $config); $urlsToDocs = $this->getAllDocumentationUrls(); $useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true); @@ -122,7 +139,7 @@ public function api(Request $request): Response 'urlToDocs' => $urlToDocs, // Is not used in the view, but still passed for backwards compatibility 'urlsToDocs' => $urlsToDocs, 'operationsSorter' => $config['operations_sort'], - 'configUrl' => $config['additional_config_url'], + 'configUrl' => $configUrl, 'validatorUrl' => $config['validator_url'], 'useAbsolutePath' => $useAbsolutePath, ]), diff --git a/src/helpers.php b/src/helpers.php index 33b9498..88bf817 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -30,14 +30,26 @@ function swagger_ui_dist_path(string $documentation, ?string $asset = null): str ); if (! $asset) { - return realpath($path) ?: ''; + $resolved = realpath($path); + + if ($resolved === false) { + throw new L5SwaggerException(sprintf('Swagger UI assets directory not found at: "%s"', $path)); + } + + return $resolved; } if (! in_array($asset, $allowedFiles, true)) { throw new L5SwaggerException(sprintf('(%s) - this L5 Swagger asset is not allowed', $asset)); } - return realpath($path.$asset) ?: ''; + $resolved = realpath($path.$asset); + + if ($resolved === false) { + throw new L5SwaggerException(sprintf('Swagger UI asset not found at: "%s"', $path.$asset)); + } + + return $resolved; } } @@ -55,10 +67,6 @@ function l5_swagger_asset(string $documentation, string $asset): string { $file = swagger_ui_dist_path($documentation, $asset); - if (! file_exists($file)) { - throw new L5SwaggerException(sprintf('Requested L5 Swagger asset file (%s) does not exists', $asset)); - } - $useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true); return route('l5-swagger.'.$documentation.'.asset', $asset, $useAbsolutePath).'?v='.md5_file($file); diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index 0abac0c..967de00 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -26,13 +26,26 @@ public function testAssetFunctionReturnsRoute(): void public function testAssetFunctionThrowsExceptionIfFileNotFound(): void { $this->expectException(L5SwaggerException::class); - $this->expectExceptionMessage('Requested L5 Swagger asset file (swagger-ui.css) does not exists'); + $this->expectExceptionMessage('Swagger UI asset not found at:'); $this->deleteAssets(); l5_swagger_asset('default', 'swagger-ui.css'); } + /** + * @throws L5SwaggerException + */ + public function testItThrowsExceptionWhenAssetsDirectoryNotFound(): void + { + config(['l5-swagger.documentations.default.paths.swagger_ui_assets_path' => 'nonexistent/path/']); + + $this->expectException(L5SwaggerException::class); + $this->expectExceptionMessage('Swagger UI assets directory not found at:'); + + swagger_ui_dist_path('default'); + } + /** * @throws L5SwaggerException */ diff --git a/tests/Unit/RoutesTest.php b/tests/Unit/RoutesTest.php index 19e62fe..cac9878 100644 --- a/tests/Unit/RoutesTest.php +++ b/tests/Unit/RoutesTest.php @@ -2,7 +2,9 @@ namespace Tests\Unit; +use Illuminate\Foundation\Application; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use L5Swagger\Exceptions\L5SwaggerException; use L5Swagger\Generator; use L5Swagger\GeneratorFactory; @@ -259,6 +261,50 @@ public function testItWillReturn404WhenDocGenerationFails(): void $this->get($jsonUrl)->assertNotFound(); } + public function testItLogsWarningWhenGenerateAlwaysInProduction(): void + { + Log::shouldReceive('warning') + ->once() + ->with('L5-Swagger: generate_always is enabled in production, which may impact performance'); + + Log::shouldReceive('error')->andReturnSelf(); + + if (! $this->app instanceof Application) { + throw new \RuntimeException('Application is not set'); + } + + $this->app->detectEnvironment(fn () => 'production'); + + config(['l5-swagger' => [ + 'default' => 'default', + 'documentations' => config('l5-swagger.documentations'), + 'defaults' => array_merge(config('l5-swagger.defaults'), ['generate_always' => true]), + ]]); + + $this->get(route('l5-swagger.default.docs')); + } + + public function testItNullifiesConfigUrlWithInvalidScheme(): void + { + Log::shouldReceive('warning') + ->once() + ->with('L5-Swagger: additional_config_url has an invalid scheme and was ignored', [ + 'url' => 'javascript:alert(1)', + ]); + + config(['l5-swagger' => [ + 'default' => 'default', + 'documentations' => config('l5-swagger.documentations'), + 'defaults' => array_merge(config('l5-swagger.defaults'), [ + 'additional_config_url' => 'javascript:alert(1)', + ]), + ]]); + + $this->get(route('l5-swagger.default.api')) + ->assertDontSee('javascript:alert(1)') + ->assertStatus(200); + } + /** * @return Generator&MockObject * From e62c81277d6243d5457dcb548c56ca5ec07b00c6 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 12 Jun 2026 11:54:49 +0000 Subject: [PATCH 2/4] Apply fixes from StyleCI --- src/Generator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Generator.php b/src/Generator.php index a3ef4cf..9a14a20 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Log; use L5Swagger\Exceptions\L5SwaggerException; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Server; From 5735d852222378ee250a3713fdab210697ecf9a0 Mon Sep 17 00:00:00 2001 From: Darius Matulionis Date: Fri, 12 Jun 2026 14:32:19 +0200 Subject: [PATCH 3/4] Small improvements --- src/Http/Controllers/SwaggerAssetController.php | 14 ++++++-------- src/Http/Controllers/SwaggerController.php | 11 ++++------- src/helpers.php | 14 +++++++++----- tests/Unit/RoutesTest.php | 14 +++++++++++--- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Http/Controllers/SwaggerAssetController.php b/src/Http/Controllers/SwaggerAssetController.php index 327a3e4..18d9002 100644 --- a/src/Http/Controllers/SwaggerAssetController.php +++ b/src/Http/Controllers/SwaggerAssetController.php @@ -32,19 +32,17 @@ public function index(Request $request): Response try { $path = swagger_ui_dist_path($documentation, $asset); - $mimeTypes = [ - 'css' => 'text/css', - 'js' => 'application/javascript', - 'png' => 'image/png', - 'html' => 'text/html', - ]; - $ext = pathinfo($asset, PATHINFO_EXTENSION); + $contentType = match (true) { + str_ends_with($asset, '.css') => 'text/css', + str_ends_with($asset, '.png') => 'image/png', + str_ends_with($asset, '.js') => 'application/javascript', + }; return (new Response( $fileSystem->get($path), 200, [ - 'Content-Type' => $mimeTypes[$ext] ?? 'application/octet-stream', + 'Content-Type' => $contentType, ] ))->setSharedMaxAge(31536000) ->setMaxAge(31536000) diff --git a/src/Http/Controllers/SwaggerController.php b/src/Http/Controllers/SwaggerController.php index acf4fca..eeb430b 100644 --- a/src/Http/Controllers/SwaggerController.php +++ b/src/Http/Controllers/SwaggerController.php @@ -8,7 +8,6 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Routing\Controller as BaseController; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Request as RequestFacade; use L5Swagger\ConfigFactory; use L5Swagger\Exceptions\L5SwaggerException; @@ -48,7 +47,7 @@ public function docs(Request $request): Response if ($config['generate_always']) { if (app()->environment('production')) { - Log::warning('L5-Swagger: generate_always is enabled in production, which may impact performance'); + logger()->warning('L5-Swagger: generate_always is enabled in production, which may impact performance'); } $generator = $this->generatorFactory->make($documentation); @@ -56,7 +55,7 @@ public function docs(Request $request): Response try { $generator->generateDocs(); } catch (Exception $e) { - Log::error($e); + logger()->error($e->getMessage(), ['exception' => $e]); abort( 404, @@ -116,10 +115,8 @@ public function api(Request $request): Response $configUrl = $config['additional_config_url'] ?? null; if ($configUrl !== null) { - $scheme = parse_url($configUrl, PHP_URL_SCHEME); - - if (! in_array($scheme, ['http', 'https'], true)) { - Log::warning('L5-Swagger: additional_config_url has an invalid scheme and was ignored', [ + if (! str_starts_with($configUrl, 'https://') && ! str_starts_with($configUrl, 'http://')) { + logger()->warning('L5-Swagger: additional_config_url has an invalid scheme and was ignored', [ 'url' => $configUrl, ]); $configUrl = null; diff --git a/src/helpers.php b/src/helpers.php index 88bf817..841047c 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -33,7 +33,9 @@ function swagger_ui_dist_path(string $documentation, ?string $asset = null): str $resolved = realpath($path); if ($resolved === false) { - throw new L5SwaggerException(sprintf('Swagger UI assets directory not found at: "%s"', $path)); + throw new L5SwaggerException( + sprintf('Swagger UI assets directory not found at: "%s"', e($path)) + ); } return $resolved; @@ -43,13 +45,15 @@ function swagger_ui_dist_path(string $documentation, ?string $asset = null): str throw new L5SwaggerException(sprintf('(%s) - this L5 Swagger asset is not allowed', $asset)); } - $resolved = realpath($path.$asset); + $fullPath = $path.$asset; - if ($resolved === false) { - throw new L5SwaggerException(sprintf('Swagger UI asset not found at: "%s"', $path.$asset)); + if (! file_exists($fullPath)) { + throw new L5SwaggerException( + sprintf('Swagger UI asset not found at: "%s"', e($fullPath)) + ); } - return $resolved; + return $fullPath; } } diff --git a/tests/Unit/RoutesTest.php b/tests/Unit/RoutesTest.php index cac9878..c47d08f 100644 --- a/tests/Unit/RoutesTest.php +++ b/tests/Unit/RoutesTest.php @@ -196,13 +196,21 @@ public static function provideProxies(): \Generator /** * @throws L5SwaggerException */ - public function testItCanServeAssets(): void + #[DataProvider('provideAssets')] + public function testItCanServeAssets(string $file, string $contentType): void { - $this->get(l5_swagger_asset('default', 'swagger-ui.css')) - ->assertSee('.swagger-ui') + $this->get(l5_swagger_asset('default', $file)) + ->assertHeader('Content-Type', $contentType) ->isOk(); } + public static function provideAssets(): \Generator + { + yield 'css' => ['file' => 'swagger-ui.css', 'contentType' => 'text/css; charset=utf-8']; + yield 'js' => ['file' => 'swagger-ui-bundle.js', 'contentType' => 'application/javascript']; + yield 'png' => ['file' => 'favicon-32x32.png', 'contentType' => 'image/png']; + } + public function testItWillThrowExceptionForIncorrectAsset(): void { $this->expectException(L5SwaggerException::class); From 72a88228dc48067162bf245f1460cf7207387cd2 Mon Sep 17 00:00:00 2001 From: Darius Matulionis Date: Fri, 12 Jun 2026 14:34:21 +0200 Subject: [PATCH 4/4] Small improvements --- src/Http/Controllers/SwaggerAssetController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/SwaggerAssetController.php b/src/Http/Controllers/SwaggerAssetController.php index 18d9002..838b9dc 100644 --- a/src/Http/Controllers/SwaggerAssetController.php +++ b/src/Http/Controllers/SwaggerAssetController.php @@ -33,9 +33,9 @@ public function index(Request $request): Response $path = swagger_ui_dist_path($documentation, $asset); $contentType = match (true) { - str_ends_with($asset, '.css') => 'text/css', str_ends_with($asset, '.png') => 'image/png', str_ends_with($asset, '.js') => 'application/javascript', + default => 'text/css', }; return (new Response(