Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion config/l5-swagger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [],
Expand Down
16 changes: 8 additions & 8 deletions resources/views/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -154,18 +154,18 @@
],

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')),

})

window.ui = ui

@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
}
Expand Down
10 changes: 7 additions & 3 deletions src/Http/Controllers/SwaggerAssetController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ public function index(Request $request): Response
try {
$path = swagger_ui_dist_path($documentation, $asset);

$contentType = match (true) {
str_ends_with($asset, '.png') => 'image/png',
str_ends_with($asset, '.js') => 'application/javascript',
default => 'text/css',
};

return (new Response(
$fileSystem->get($path),
200,
[
'Content-Type' => (isset(pathinfo($asset)['extension']) && pathinfo($asset)['extension'] === 'css')
? 'text/css'
: 'application/javascript',
'Content-Type' => $contentType,
]
))->setSharedMaxAge(31536000)
->setMaxAge(31536000)
Expand Down
20 changes: 17 additions & 3 deletions src/Http/Controllers/SwaggerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,12 +46,16 @@ public function docs(Request $request): Response
);

if ($config['generate_always']) {
if (app()->environment('production')) {
logger()->warning('L5-Swagger: generate_always is enabled in production, which may impact performance');
}

$generator = $this->generatorFactory->make($documentation);

try {
$generator->generateDocs();
} catch (Exception $e) {
Log::error($e);
logger()->error($e->getMessage(), ['exception' => $e]);

abort(
404,
Expand Down Expand Up @@ -109,6 +112,17 @@ public function api(Request $request): Response
);
}

$configUrl = $config['additional_config_url'] ?? null;

if ($configUrl !== null) {
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;
}
}

$urlToDocs = $this->generateDocumentationFileURL($documentation, $config);
$urlsToDocs = $this->getAllDocumentationUrls();
$useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true);
Expand All @@ -122,7 +136,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,
]),
Expand Down
24 changes: 18 additions & 6 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,30 @@
);

if (! $asset) {
return realpath($path) ?: '';
$resolved = realpath($path);

if ($resolved === false) {
throw new L5SwaggerException(
sprintf('Swagger UI assets directory not found at: "%s"', e($path))

Check failure on line 37 in src/helpers.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/helpers.php#L37

All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found 'e'.
);
}

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) ?: '';
$fullPath = $path.$asset;

if (! file_exists($fullPath)) {

Check warning on line 50 in src/helpers.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/helpers.php#L50

Filesystem function file_exists() detected with dynamic parameter

Check warning on line 50 in src/helpers.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/helpers.php#L50

The use of function file_exists() is discouraged
throw new L5SwaggerException(
sprintf('Swagger UI asset not found at: "%s"', e($fullPath))

Check failure on line 52 in src/helpers.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/helpers.php#L52

All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found 'e'.
);
}

return $fullPath;
}
}

Expand All @@ -55,10 +71,6 @@
{
$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);
Expand Down
15 changes: 14 additions & 1 deletion tests/Unit/HelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
60 changes: 57 additions & 3 deletions tests/Unit/RoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -194,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);
Expand Down Expand Up @@ -259,6 +269,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
*
Expand Down