From d7a3aeb0a26ae856c2c6b0a75660c4277f6c2003 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 13 Sep 2019 20:03:53 +0200 Subject: [PATCH 1/5] Created a config prost-processor which adds the base path on every applicable configuration --- config/autoload/router.global.php | 2 + config/config.php | 3 +- module/Core/src/Config/BasePathPrefixer.php | 39 +++++++++++++++++++ .../{ => Config}/SimplifiedConfigParser.php | 3 +- .../SimplifiedConfigParserTest.php | 9 ++++- 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 module/Core/src/Config/BasePathPrefixer.php rename module/Core/src/{ => Config}/SimplifiedConfigParser.php (96%) rename module/Core/test/{ => Config}/SimplifiedConfigParserTest.php (92%) 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/module/Core/src/Config/BasePathPrefixer.php b/module/Core/src/Config/BasePathPrefixer.php new file mode 100644 index 00000000..28d8c17e --- /dev/null +++ b/module/Core/src/Config/BasePathPrefixer.php @@ -0,0 +1,39 @@ +prefixRoutesWithBasePath($config, $basePath); + $config['middleware_pipeline'] = $this->prefixMiddlewarePathsWithBasePath($config, $basePath); + $config['url_shortener']['domain']['hostname'] .= $basePath; + + return $config; + } + + private function prefixRoutesWithBasePath(array $config, string $basePath): array + { + return map($config['routes'] ?? [], function (array $route) use ($basePath) { + $route['path'] = $basePath . $route['path']; + return $route; + }); + } + + private function prefixMiddlewarePathsWithBasePath(array $config, string $basePath): array + { + return map($config['middleware_pipeline'] ?? [], function (array $middleware) use ($basePath) { + if (! isset($middleware['path'])) { + return $middleware; + } + + $middleware['path'] = $basePath . $middleware['path']; + return $middleware; + }); + } +} 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/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)); From 6e38457655d3fd59f6a1edaed4bbe6315ab7e18e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 13 Sep 2019 20:17:30 +0200 Subject: [PATCH 2/5] Created BasePathPrefixerTest --- .../Core/test/Config/BasePathPrefixerTest.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 module/Core/test/Config/BasePathPrefixerTest.php diff --git a/module/Core/test/Config/BasePathPrefixerTest.php b/module/Core/test/Config/BasePathPrefixerTest.php new file mode 100644 index 00000000..93ae72db --- /dev/null +++ b/module/Core/test/Config/BasePathPrefixerTest.php @@ -0,0 +1,85 @@ +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 + { + yield 'without anything' => [[], [], [], '']; + yield 'with empty options' => [ + [ + 'routes' => [], + 'middleware_pipeline' => [], + 'url_shortener' => [], + ], + [], + [], + '', + ]; + 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', + ]; + } +} From bc07d77d065de0cd87c2133e718cb49d29c04d4c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 13 Sep 2019 20:22:41 +0200 Subject: [PATCH 3/5] Removed duplicated code from BasePathPrefixer --- module/Core/src/Config/BasePathPrefixer.php | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/module/Core/src/Config/BasePathPrefixer.php b/module/Core/src/Config/BasePathPrefixer.php index 28d8c17e..971916e4 100644 --- a/module/Core/src/Config/BasePathPrefixer.php +++ b/module/Core/src/Config/BasePathPrefixer.php @@ -7,33 +7,29 @@ use function Functional\map; class BasePathPrefixer { + private const ELEMENTS_WITH_PATH = ['routes', 'middleware_pipeline']; + public function __invoke(array $config): array { $basePath = $config['router']['base_path'] ?? ''; - $config['routes'] = $this->prefixRoutesWithBasePath($config, $basePath); - $config['middleware_pipeline'] = $this->prefixMiddlewarePathsWithBasePath($config, $basePath); $config['url_shortener']['domain']['hostname'] .= $basePath; + foreach (self::ELEMENTS_WITH_PATH as $configKey) { + $config[$configKey] = $this->prefixPathsWithBasePath($configKey, $config, $basePath); + } + return $config; } - private function prefixRoutesWithBasePath(array $config, string $basePath): array + private function prefixPathsWithBasePath(string $configKey, array $config, string $basePath): array { - return map($config['routes'] ?? [], function (array $route) use ($basePath) { - $route['path'] = $basePath . $route['path']; - return $route; - }); - } - - private function prefixMiddlewarePathsWithBasePath(array $config, string $basePath): array - { - return map($config['middleware_pipeline'] ?? [], function (array $middleware) use ($basePath) { - if (! isset($middleware['path'])) { - return $middleware; + return map($config[$configKey] ?? [], function (array $element) use ($basePath) { + if (! isset($element['path'])) { + return $element; } - $middleware['path'] = $basePath . $middleware['path']; - return $middleware; + $element['path'] = $basePath . $element['path']; + return $element; }); } } From 0a1786c89aacb5426ad99d11d3c2e468561761ab Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 13 Sep 2019 20:36:40 +0200 Subject: [PATCH 4/5] Added support for basepath on docker image --- CHANGELOG.md | 14 +++++++++++++- docker/README.md | 2 ++ docker/config/shlink_in_docker.local.php | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) 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/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', ''), + ], + ]; From 8d74e0c3ff6846849b865b343737317f0ca9eea3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 13 Sep 2019 20:46:49 +0200 Subject: [PATCH 5/5] Fixed undefined-index errors in BasePathPrefixerTest --- module/Core/test/Config/BasePathPrefixerTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/module/Core/test/Config/BasePathPrefixerTest.php b/module/Core/test/Config/BasePathPrefixerTest.php index 93ae72db..248d6594 100644 --- a/module/Core/test/Config/BasePathPrefixerTest.php +++ b/module/Core/test/Config/BasePathPrefixerTest.php @@ -43,12 +43,18 @@ class BasePathPrefixerTest extends TestCase public function provideConfig(): iterable { - yield 'without anything' => [[], [], [], '']; + $urlShortener = [ + 'domain' => [ + 'hostname' => null, + ], + ]; + + yield 'without anything' => [['url_shortener' => $urlShortener], [], [], '']; yield 'with empty options' => [ [ 'routes' => [], 'middleware_pipeline' => [], - 'url_shortener' => [], + 'url_shortener' => $urlShortener, ], [], [],