Skip to content
Merged
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
19 changes: 19 additions & 0 deletions app/Exceptions/FeatureDisabledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Exceptions;

use Symfony\Component\HttpFoundation\Response;

class FeatureDisabledException extends BaseLycheeException
{
public function __construct(string $feature)
{
parent::__construct(Response::HTTP_NOT_IMPLEMENTED, sprintf("Feature '%s' is disabled", $feature), null);
}
}
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,6 @@ class Kernel extends HttpKernel
'response_cache' => \App\Http\Middleware\Caching\ResponseCache::class,
'album_cache_refresher' => \App\Http\Middleware\Caching\AlbumRouteCacheRefresher::class,
'legacy_id_redirect' => \App\Http\Middleware\LegacyLocalIdRedirect::class,
'feature' => \App\Http\Middleware\FeatureEnabled::class,
];
}
48 changes: 48 additions & 0 deletions app/Http/Middleware/FeatureEnabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Http\Middleware;

use App\Exceptions\ConfigurationKeyMissingException;
use App\Exceptions\FeatureDisabledException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;

/**
* Class FeatureEnabled.
*
* This middleware checks whether a feature flag is enabled via the `features`
* config file. If the flag resolves to false, a 501 Not Implemented response
* is returned. Use as `feature:feature_name` in route definitions.
*/
class FeatureEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request the incoming request to serve
* @param \Closure $next the next operation to be applied to the request
* @param string $feature_name the key to look up in config('features')
*
* @throws FeatureDisabledException
*/
public function handle(Request $request, \Closure $next, string $feature_name): mixed
{
$key = 'features.' . $feature_name;

if (!Config::has($key)) {
throw new ConfigurationKeyMissingException(sprintf("Feature key '%s' does not exist in config", $key));
}

if (config($key) !== true) {
throw new FeatureDisabledException($feature_name);
}

return $next($request);
}
}
65 changes: 65 additions & 0 deletions tests/Unit/Middleware/FeatureEnabledTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

/**
* We don't care for unhandled exceptions in tests.
* It is the nature of a test to throw an exception.
* Without this suppression we had 100+ Linter warning in this file which
* don't help anything.
*
* @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection
*/

namespace Tests\Unit\Middleware;

use App\Exceptions\ConfigurationKeyMissingException;
use App\Exceptions\FeatureDisabledException;
use App\Http\Middleware\FeatureEnabled;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Request;
use Tests\AbstractTestCase;

class FeatureEnabledTest extends AbstractTestCase
{
use DatabaseTransactions;

public function testFeatureEnabled(): void
{
config(['features.some_feature' => true]);

$request = $this->mock(Request::class);
$middleware = new FeatureEnabled();

self::assertEquals(1, $middleware->handle($request, fn () => 1, 'some_feature'));
}

public function testFeatureDisabled(): void
{
config(['features.some_feature' => false]);

$request = $this->mock(Request::class);
$middleware = new FeatureEnabled();

$this->assertThrows(
fn () => $middleware->handle($request, fn () => 1, 'some_feature'),
FeatureDisabledException::class
);
}

public function testFeatureNotDefined(): void
{
$request = $this->mock(Request::class);
$middleware = new FeatureEnabled();

$this->assertThrows(
fn () => $middleware->handle($request, fn () => 1, 'undefined_feature'),
ConfigurationKeyMissingException::class
);
}
}
Loading