diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce4977c..9c341e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Added -* *Nothing* +* [#482](https://github.com/shlinkio/shlink/issues/482) Added support to serve shlink under a sub path. + + The `router.base_path` config option can be defined now to set the base path from which shlink is served. + + ```php + return [ + 'router' => [ + 'base_path' => '/foo/bar', + ], + ]; + ``` + + This option will also be available on shlink-installer 1.3.0, so the installer will ask for it. It can also be provided for the docker image as the `BASE_PATH` env var. #### Changed diff --git a/config/autoload/router.global.php b/config/autoload/router.global.php index deb875f3..24f40e71 100644 --- a/config/autoload/router.global.php +++ b/config/autoload/router.global.php @@ -6,6 +6,8 @@ use Zend\Expressive\Router\FastRouteRouter; return [ 'router' => [ + 'base_path' => '', + 'fastroute' => [ FastRouteRouter::CONFIG_CACHE_ENABLED => true, FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php', diff --git a/config/config.php b/config/config.php index 8cc7c38a..3a44fb17 100644 --- a/config/config.php +++ b/config/config.php @@ -28,5 +28,6 @@ return (new ConfigAggregator\ConfigAggregator([ ? new ConfigAggregator\PhpFileProvider('config/test/*.global.php') : new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'), ], 'data/cache/app_config.php', [ - Core\SimplifiedConfigParser::class, + Core\Config\SimplifiedConfigParser::class, + Core\Config\BasePathPrefixer::class, ]))->getMergedConfig(); diff --git a/docker/README.md b/docker/README.md index 2f2a4012..9f62c818 100644 --- a/docker/README.md +++ b/docker/README.md @@ -103,6 +103,7 @@ This is the complete list of supported env vars: * `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`. * `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`. * `NOT_FOUND_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page. +* `BASE_PATH`: The base path from which you plan to serve shlink, in case you don't want to serve it from the root of the domain. Defaults to `''`. * `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel). This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately. @@ -130,6 +131,7 @@ docker run \ -e VALIDATE_URLS=false \ -e "NOT_FOUND_REDIRECT_TO=https://www.google.com" \ -e "REDIS_SERVERS=tcp://172.20.0.1:6379,tcp://172.20.0.2:6379" \ + -e "BASE_PATH=/my-campaign" \ shlinkio/shlink ``` diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index e84a4467..1ee7eaed 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -168,4 +168,8 @@ return [ 'servers' => env('REDIS_SERVERS'), ], + 'router' => [ + 'base_path' => env('BASE_PATH', ''), + ], + ]; diff --git a/module/Core/src/Config/BasePathPrefixer.php b/module/Core/src/Config/BasePathPrefixer.php new file mode 100644 index 00000000..971916e4 --- /dev/null +++ b/module/Core/src/Config/BasePathPrefixer.php @@ -0,0 +1,35 @@ +prefixPathsWithBasePath($configKey, $config, $basePath); + } + + return $config; + } + + private function prefixPathsWithBasePath(string $configKey, array $config, string $basePath): array + { + return map($config[$configKey] ?? [], function (array $element) use ($basePath) { + if (! isset($element['path'])) { + return $element; + } + + $element['path'] = $basePath . $element['path']; + return $element; + }); + } +} diff --git a/module/Core/src/SimplifiedConfigParser.php b/module/Core/src/Config/SimplifiedConfigParser.php similarity index 96% rename from module/Core/src/SimplifiedConfigParser.php rename to module/Core/src/Config/SimplifiedConfigParser.php index 42c5d205..2fec5968 100644 --- a/module/Core/src/SimplifiedConfigParser.php +++ b/module/Core/src/Config/SimplifiedConfigParser.php @@ -1,7 +1,7 @@ ['entity_manager', 'connection'], 'delete_short_url_threshold' => ['delete_short_urls', 'visits_threshold'], 'redis_servers' => ['redis', 'servers'], + 'base_path' => ['router', 'base_path'], ]; private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ 'not_found_redirect_to' => [ diff --git a/module/Core/test/Config/BasePathPrefixerTest.php b/module/Core/test/Config/BasePathPrefixerTest.php new file mode 100644 index 00000000..248d6594 --- /dev/null +++ b/module/Core/test/Config/BasePathPrefixerTest.php @@ -0,0 +1,91 @@ +prefixer = new BasePathPrefixer(); + } + + /** + * @test + * @dataProvider provideConfig + */ + public function parsesConfigAsExpected( + array $originalConfig, + array $expectedRoutes, + array $expectedMiddlewares, + string $expectedHostname + ): void { + [ + 'routes' => $routes, + 'middleware_pipeline' => $middlewares, + 'url_shortener' => $urlShortener, + ] = ($this->prefixer)($originalConfig); + + $this->assertEquals($expectedRoutes, $routes); + $this->assertEquals($expectedMiddlewares, $middlewares); + $this->assertEquals([ + 'domain' => [ + 'hostname' => $expectedHostname, + ], + ], $urlShortener); + } + + public function provideConfig(): iterable + { + $urlShortener = [ + 'domain' => [ + 'hostname' => null, + ], + ]; + + yield 'without anything' => [['url_shortener' => $urlShortener], [], [], '']; + yield 'with empty options' => [ + [ + 'routes' => [], + 'middleware_pipeline' => [], + 'url_shortener' => $urlShortener, + ], + [], + [], + '', + ]; + yield 'with non-empty options' => [ + [ + 'routes' => [ + ['path' => '/something'], + ['path' => '/something-else'], + ], + 'middleware_pipeline' => [ + ['with' => 'no_path'], + ['path' => '/rest', 'middleware' => []], + ], + 'url_shortener' => [ + 'domain' => [ + 'hostname' => 'doma.in', + ], + ], + 'router' => ['base_path' => '/foo/bar'], + ], + [ + ['path' => '/foo/bar/something'], + ['path' => '/foo/bar/something-else'], + ], + [ + ['with' => 'no_path'], + ['path' => '/foo/bar/rest', 'middleware' => []], + ], + 'doma.in/foo/bar', + ]; + } +} diff --git a/module/Core/test/SimplifiedConfigParserTest.php b/module/Core/test/Config/SimplifiedConfigParserTest.php similarity index 92% rename from module/Core/test/SimplifiedConfigParserTest.php rename to module/Core/test/Config/SimplifiedConfigParserTest.php index 957c7a24..68361027 100644 --- a/module/Core/test/SimplifiedConfigParserTest.php +++ b/module/Core/test/Config/SimplifiedConfigParserTest.php @@ -1,10 +1,10 @@ 'bar', 'port' => '1234', ], + 'base_path' => '/foo/bar', ]; $expected = [ 'app_options' => [ @@ -96,6 +97,10 @@ class SimplifiedConfigParserTest extends TestCase 'tcp://1.2.2.2:2222', ], ], + + 'router' => [ + 'base_path' => '/foo/bar', + ], ]; $result = ($this->postProcessor)(array_merge($config, $simplified));