From 5536d05e99d6bf290855614a0cf1abf56a89267e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 16:14:57 +0200 Subject: [PATCH 01/86] Added idea folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5aedee0f..9695be68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea build composer.lock vendor/ From 5eefaf30719e86fcbff4519611c7432b8525a9e0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 16:30:48 +0200 Subject: [PATCH 02/86] Added config manager package --- composer.json | 6 +++-- config/autoload/zend-expressive.global.php | 2 +- config/config.php | 28 +++++++--------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/composer.json b/composer.json index 374fed26..9965ebe6 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "acelaya/url-shortener", + "name": "shlinkio/shlink", "type": "project", - "homepage": "https://github.com/acelaya/url-shortener", + "homepage": "http://shlink.io", "license": "MIT", "authors": [ { @@ -19,6 +19,8 @@ "zendframework/zend-stdlib": "^2.7", "zendframework/zend-servicemanager": "^3.0", "zendframework/zend-paginator": "^2.6", + "zendframework/zend-config": "^2.6", + "mtymek/expressive-config-manager": "^0.4", "doctrine/orm": "^2.5", "guzzlehttp/guzzle": "^6.2", "acelaya/zsm-annotated-services": "^0.2.0", diff --git a/config/autoload/zend-expressive.global.php b/config/autoload/zend-expressive.global.php index db5e3f38..b2102394 100644 --- a/config/autoload/zend-expressive.global.php +++ b/config/autoload/zend-expressive.global.php @@ -3,7 +3,7 @@ return [ 'debug' => false, - 'config_cache_enabled' => false, + 'config_cache_enabled' => true, 'zend-expressive' => [ 'error_handler' => [ diff --git a/config/config.php b/config/config.php index a3d0e7ac..f835a11c 100644 --- a/config/config.php +++ b/config/config.php @@ -1,6 +1,6 @@ getMergedConfig(); +}); From 95d0beea3c254cc1a5f29f627297aad36fc3feb6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 16:50:02 +0200 Subject: [PATCH 03/86] Created CLI module --- composer.json | 11 +++++++++-- config/autoload/services.global.php | 2 +- cli-config.php => config/cli-config.php | 2 +- config/config.php | 2 ++ .../CLI/config/cli.config.php | 2 +- .../CLI/src}/Command/GenerateShortcodeCommand.php | 2 +- .../CLI/src}/Command/GetVisitsCommand.php | 2 +- .../CLI/src}/Command/ListShortcodesCommand.php | 2 +- .../CLI/src}/Command/ResolveUrlCommand.php | 2 +- module/CLI/src/Config/ConfigProvider.php | 13 +++++++++++++ .../CLI/src}/Factory/ApplicationFactory.php | 2 +- phpcs.xml | 3 +-- 12 files changed, 33 insertions(+), 12 deletions(-) rename cli-config.php => config/cli-config.php (83%) rename config/autoload/cli.global.php => module/CLI/config/cli.config.php (87%) rename {src/CLI => module/CLI/src}/Command/GenerateShortcodeCommand.php (98%) rename {src/CLI => module/CLI/src}/Command/GetVisitsCommand.php (98%) rename {src/CLI => module/CLI/src}/Command/ListShortcodesCommand.php (98%) rename {src/CLI => module/CLI/src}/Command/ResolveUrlCommand.php (98%) create mode 100644 module/CLI/src/Config/ConfigProvider.php rename {src/CLI => module/CLI/src}/Factory/ApplicationFactory.php (96%) diff --git a/composer.json b/composer.json index 9965ebe6..91f340c1 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,7 @@ "name": "shlinkio/shlink", "type": "project", "homepage": "http://shlink.io", + "description": "A PHP-based URL shortener application with analytics and management", "license": "MIT", "authors": [ { @@ -36,12 +37,18 @@ }, "autoload": { "psr-4": { - "Acelaya\\UrlShortener\\": "src" + "Acelaya\\UrlShortener\\": "src", + "Shlinkio\\Shlink\\CLI\\": "module/CLI/src", + "Shlinkio\\Shlink\\Rest\\": "module/Rest/src", + "Shlinkio\\Shlink\\Core\\": "module/Core/src" } }, "autoload-dev": { "psr-4": { - "AcelayaTest\\UrlShortener\\": "tests" + "AcelayaTest\\UrlShortener\\": "tests", + "ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test", + "ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test", + "ShlinkioTest\\Shlink\\Core\\": "module/Core/test" } }, "scripts": { diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 4d6cc40e..3277159e 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,5 +1,5 @@ get(EntityManager::class); diff --git a/config/config.php b/config/config.php index f835a11c..b27d8173 100644 --- a/config/config.php +++ b/config/config.php @@ -1,4 +1,5 @@ - src - tests + module config public/index.php From 55f954f50f1688e3413906b382103c8fa4359b40 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 17:07:59 +0200 Subject: [PATCH 04/86] Created Rest module --- .../autoload/middleware-pipeline.global.php | 10 ----- config/autoload/routes.global.php | 33 ---------------- config/autoload/services.global.php | 7 ---- config/config.php | 4 +- module/CLI/src/Config/ConfigProvider.php | 13 ------- module/CLI/src/ConfigProvider.php | 13 +++++++ .../config/middleware-pipeline.global.php | 16 ++++++++ .../Rest/config/rest.config.php | 0 module/Rest/config/routes.global.php | 39 +++++++++++++++++++ module/Rest/config/services.global.php | 22 +++++++++++ .../src/Action}/AbstractRestMiddleware.php | 2 +- .../src/Action}/AuthenticateMiddleware.php | 8 ++-- .../src/Action}/CreateShortcodeMiddleware.php | 4 +- .../Rest/src/Action}/GetVisitsMiddleware.php | 4 +- .../src/Action}/ListShortcodesMiddleware.php | 5 +-- .../Rest/src/Action}/ResolveUrlMiddleware.php | 4 +- module/Rest/src/ConfigProvider.php | 13 +++++++ .../CheckAuthenticationMiddleware.php | 8 ++-- .../src}/Middleware/CrossDomainMiddleware.php | 2 +- .../Rest/src}/Service/RestTokenService.php | 2 +- .../Service/RestTokenServiceInterface.php | 2 +- {src => module/Rest/src}/Util/RestUtils.php | 2 +- 22 files changed, 127 insertions(+), 86 deletions(-) delete mode 100644 module/CLI/src/Config/ConfigProvider.php create mode 100644 module/CLI/src/ConfigProvider.php create mode 100644 module/Rest/config/middleware-pipeline.global.php rename config/autoload/rest.global.php => module/Rest/config/rest.config.php (100%) create mode 100644 module/Rest/config/routes.global.php create mode 100644 module/Rest/config/services.global.php rename {src/Middleware/Rest => module/Rest/src/Action}/AbstractRestMiddleware.php (97%) rename {src/Middleware/Rest => module/Rest/src/Action}/AuthenticateMiddleware.php (89%) rename {src/Middleware/Rest => module/Rest/src/Action}/CreateShortcodeMiddleware.php (96%) rename {src/Middleware/Rest => module/Rest/src/Action}/GetVisitsMiddleware.php (95%) rename {src/Middleware/Rest => module/Rest/src/Action}/ListShortcodesMiddleware.php (93%) rename {src/Middleware/Rest => module/Rest/src/Action}/ResolveUrlMiddleware.php (95%) create mode 100644 module/Rest/src/ConfigProvider.php rename {src => module/Rest/src}/Middleware/CheckAuthenticationMiddleware.php (94%) rename {src => module/Rest/src}/Middleware/CrossDomainMiddleware.php (97%) rename {src => module/Rest/src}/Service/RestTokenService.php (98%) rename {src => module/Rest/src}/Service/RestTokenServiceInterface.php (95%) rename {src => module/Rest/src}/Util/RestUtils.php (96%) diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index fc6f85f0..ca116a95 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -1,5 +1,4 @@ 10, ], - 'rest' => [ - 'path' => '/rest', - 'middleware' => [ - Middleware\CheckAuthenticationMiddleware::class, - Middleware\CrossDomainMiddleware::class, - ], - 'priority' => 5, - ], - 'post-routing' => [ 'middleware' => [ Helper\UrlHelperMiddleware::class, diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 87133f09..40a3d20b 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -1,6 +1,5 @@ Routable\RedirectMiddleware::class, 'allowed_methods' => ['GET'], ], - - // Rest - [ - 'name' => 'rest-authenticate', - 'path' => '/rest/authenticate', - 'middleware' => Rest\AuthenticateMiddleware::class, - 'allowed_methods' => ['POST', 'OPTIONS'], - ], - [ - 'name' => 'rest-create-shortcode', - 'path' => '/rest/short-codes', - 'middleware' => Rest\CreateShortcodeMiddleware::class, - 'allowed_methods' => ['POST', 'OPTIONS'], - ], - [ - 'name' => 'rest-resolve-url', - 'path' => '/rest/short-codes/{shortCode}', - 'middleware' => Rest\ResolveUrlMiddleware::class, - 'allowed_methods' => ['GET', 'OPTIONS'], - ], - [ - 'name' => 'rest-list-shortened-url', - 'path' => '/rest/short-codes', - 'middleware' => Rest\ListShortcodesMiddleware::class, - 'allowed_methods' => ['GET'], - ], - [ - 'name' => 'rest-get-visits', - 'path' => '/rest/visits/{shortCode}', - 'middleware' => Rest\GetVisitsMiddleware::class, - 'allowed_methods' => ['GET', 'OPTIONS'], - ], ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 3277159e..bbf816d0 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -51,13 +51,6 @@ return [ // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, - Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class, - Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class, - Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class, - Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class, - Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class, - Middleware\CrossDomainMiddleware::class => InvokableFactory::class, - Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/config/config.php b/config/config.php index b27d8173..850f5e4f 100644 --- a/config/config.php +++ b/config/config.php @@ -1,5 +1,6 @@ [ + 'rest' => [ + 'path' => '/rest', + 'middleware' => [ + Middleware\CheckAuthenticationMiddleware::class, + Middleware\CrossDomainMiddleware::class, + ], + 'priority' => 5, + ], + ], +]; diff --git a/config/autoload/rest.global.php b/module/Rest/config/rest.config.php similarity index 100% rename from config/autoload/rest.global.php rename to module/Rest/config/rest.config.php diff --git a/module/Rest/config/routes.global.php b/module/Rest/config/routes.global.php new file mode 100644 index 00000000..e8abb6fe --- /dev/null +++ b/module/Rest/config/routes.global.php @@ -0,0 +1,39 @@ + [ + [ + 'name' => 'rest-authenticate', + 'path' => '/rest/authenticate', + 'middleware' => Action\AuthenticateMiddleware::class, + 'allowed_methods' => ['POST', 'OPTIONS'], + ], + [ + 'name' => 'rest-create-shortcode', + 'path' => '/rest/short-codes', + 'middleware' => Action\CreateShortcodeMiddleware::class, + 'allowed_methods' => ['POST', 'OPTIONS'], + ], + [ + 'name' => 'rest-resolve-url', + 'path' => '/rest/short-codes/{shortCode}', + 'middleware' => Action\ResolveUrlMiddleware::class, + 'allowed_methods' => ['GET', 'OPTIONS'], + ], + [ + 'name' => 'rest-lActionist-shortened-url', + 'path' => '/rest/short-codes', + 'middleware' => Action\ListShortcodesMiddleware::class, + 'allowed_methods' => ['GET'], + ], + [ + 'name' => 'rest-get-visits', + 'path' => '/rest/visits/{shortCode}', + 'middleware' => Action\GetVisitsMiddleware::class, + 'allowed_methods' => ['GET', 'OPTIONS'], + ], + ], + +]; diff --git a/module/Rest/config/services.global.php b/module/Rest/config/services.global.php new file mode 100644 index 00000000..d0975ec8 --- /dev/null +++ b/module/Rest/config/services.global.php @@ -0,0 +1,22 @@ + [ + 'factories' => [ + Action\AuthenticateMiddleware::class => AnnotatedFactory::class, + Action\CreateShortcodeMiddleware::class => AnnotatedFactory::class, + Action\ResolveUrlMiddleware::class => AnnotatedFactory::class, + Action\GetVisitsMiddleware::class => AnnotatedFactory::class, + Action\ListShortcodesMiddleware::class => AnnotatedFactory::class, + + Middleware\CrossDomainMiddleware::class => InvokableFactory::class, + Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, + ], + ], + +]; diff --git a/src/Middleware/Rest/AbstractRestMiddleware.php b/module/Rest/src/Action/AbstractRestMiddleware.php similarity index 97% rename from src/Middleware/Rest/AbstractRestMiddleware.php rename to module/Rest/src/Action/AbstractRestMiddleware.php index 1168ff60..9ef870f4 100644 --- a/src/Middleware/Rest/AbstractRestMiddleware.php +++ b/module/Rest/src/Action/AbstractRestMiddleware.php @@ -1,5 +1,5 @@ Date: Tue, 19 Jul 2016 17:12:50 +0200 Subject: [PATCH 05/86] Fixed config files names --- config/autoload/services.global.php | 1 - config/config.php | 2 +- ...ware-pipeline.global.php => middleware-pipeline.config.php} | 0 module/Rest/config/{routes.global.php => routes.config.php} | 0 .../Rest/config/{services.global.php => services.config.php} | 3 +++ 5 files changed, 4 insertions(+), 2 deletions(-) rename module/Rest/config/{middleware-pipeline.global.php => middleware-pipeline.config.php} (100%) rename module/Rest/config/{routes.global.php => routes.config.php} (100%) rename module/Rest/config/{services.global.php => services.config.php} (88%) diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index bbf816d0..81c5669f 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -40,7 +40,6 @@ return [ Service\UrlShortener::class => AnnotatedFactory::class, Service\VisitsTracker::class => AnnotatedFactory::class, Service\ShortUrlService::class => AnnotatedFactory::class, - Service\RestTokenService::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, // Cli commands diff --git a/config/config.php b/config/config.php index 850f5e4f..0fad2300 100644 --- a/config/config.php +++ b/config/config.php @@ -15,9 +15,9 @@ use Zend\Expressive\ConfigManager\ZendConfigProvider; return call_user_func(function () { $configManager = new ConfigManager([ + new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'), CLI\ConfigProvider::class, Rest\ConfigProvider::class, - new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php') ], 'data/cache/app_config.php'); return $configManager->getMergedConfig(); diff --git a/module/Rest/config/middleware-pipeline.global.php b/module/Rest/config/middleware-pipeline.config.php similarity index 100% rename from module/Rest/config/middleware-pipeline.global.php rename to module/Rest/config/middleware-pipeline.config.php diff --git a/module/Rest/config/routes.global.php b/module/Rest/config/routes.config.php similarity index 100% rename from module/Rest/config/routes.global.php rename to module/Rest/config/routes.config.php diff --git a/module/Rest/config/services.global.php b/module/Rest/config/services.config.php similarity index 88% rename from module/Rest/config/services.global.php rename to module/Rest/config/services.config.php index d0975ec8..aff4cc96 100644 --- a/module/Rest/config/services.global.php +++ b/module/Rest/config/services.config.php @@ -2,12 +2,15 @@ use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Shlinkio\Shlink\Rest\Action; use Shlinkio\Shlink\Rest\Middleware; +use Shlinkio\Shlink\Rest\Service; use Zend\ServiceManager\Factory\InvokableFactory; return [ 'services' => [ 'factories' => [ + Service\RestTokenService::class => AnnotatedFactory::class, + Action\AuthenticateMiddleware::class => AnnotatedFactory::class, Action\CreateShortcodeMiddleware::class => AnnotatedFactory::class, Action\ResolveUrlMiddleware::class => AnnotatedFactory::class, From 7efb3b3a86b81a092f69334f45025dca130118a9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 17:17:37 +0200 Subject: [PATCH 06/86] Created cli-specific services config file --- config/autoload/services.global.php | 11 +---------- module/CLI/config/services.config.php | 20 ++++++++++++++++++++ src/Service/UrlShortener.php | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 module/CLI/config/services.config.php diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 81c5669f..bf5b28e4 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,5 +1,4 @@ [ 'factories' => [ Expressive\Application::class => Container\ApplicationFactory::class, - Console\Application::class => CLI\Factory\ApplicationFactory::class, // Url helpers Helper\UrlHelper::class => Helper\UrlHelperFactory::class, @@ -42,12 +39,6 @@ return [ Service\ShortUrlService::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, - // Cli commands - CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, - CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, - CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, - CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class, - // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, ], @@ -56,7 +47,7 @@ return [ 'httpClient' => GuzzleHttp\Client::class, Router\RouterInterface::class => Router\FastRouteRouter::class, AnnotatedFactory::CACHE_SERVICE => Cache::class, - ] + ], ], ]; diff --git a/module/CLI/config/services.config.php b/module/CLI/config/services.config.php new file mode 100644 index 00000000..5c10f5a9 --- /dev/null +++ b/module/CLI/config/services.config.php @@ -0,0 +1,20 @@ + [ + 'factories' => [ + Console\Application::class => CLI\Factory\ApplicationFactory::class, + + CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, + CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, + CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, + CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class, + ], + ], + +]; diff --git a/src/Service/UrlShortener.php b/src/Service/UrlShortener.php index 3c15a0f6..c538110d 100644 --- a/src/Service/UrlShortener.php +++ b/src/Service/UrlShortener.php @@ -44,7 +44,7 @@ class UrlShortener implements UrlShortenerInterface ) { $this->httpClient = $httpClient; $this->em = $em; - $this->chars = $chars; + $this->chars = empty($chars) ? self::DEFAULT_CHARS : $chars; } /** From 8fc88171ee96df00230c3f0d594f7bd2b915dcd2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 17:27:55 +0200 Subject: [PATCH 07/86] Moved AuthenticationException to Rest module --- composer.json | 6 ++++-- module/Rest/src/Action/AuthenticateMiddleware.php | 2 +- .../Rest/src}/Exception/AuthenticationException.php | 4 +++- module/Rest/src/Service/RestTokenService.php | 2 +- .../Rest/src/Service/RestTokenServiceInterface.php | 2 +- module/Rest/src/Util/RestUtils.php | 13 +++++++------ 6 files changed, 17 insertions(+), 12 deletions(-) rename {src => module/Rest/src}/Exception/AuthenticationException.php (74%) diff --git a/composer.json b/composer.json index 91f340c1..a64197a6 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "Acelaya\\UrlShortener\\": "src", "Shlinkio\\Shlink\\CLI\\": "module/CLI/src", "Shlinkio\\Shlink\\Rest\\": "module/Rest/src", - "Shlinkio\\Shlink\\Core\\": "module/Core/src" + "Shlinkio\\Shlink\\Core\\": "module/Core/src", + "Shlinkio\\Shlink\\Common\\": "module/Common/src" } }, "autoload-dev": { @@ -48,7 +49,8 @@ "AcelayaTest\\UrlShortener\\": "tests", "ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test", "ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test", - "ShlinkioTest\\Shlink\\Core\\": "module/Core/test" + "ShlinkioTest\\Shlink\\Core\\": "module/Core/test", + "ShlinkioTest\\Shlink\\Common\\": "module/Common/test" } }, "scripts": { diff --git a/module/Rest/src/Action/AuthenticateMiddleware.php b/module/Rest/src/Action/AuthenticateMiddleware.php index 97b6ad9f..cbea1360 100644 --- a/module/Rest/src/Action/AuthenticateMiddleware.php +++ b/module/Rest/src/Action/AuthenticateMiddleware.php @@ -1,10 +1,10 @@ Date: Tue, 19 Jul 2016 17:38:41 +0200 Subject: [PATCH 08/86] Created Common module --- config/autoload/services.global.php | 10 -------- config/config.php | 2 ++ .../CLI/src/Command/ListShortcodesCommand.php | 4 +-- module/Common/config/services.config.php | 25 +++++++++++++++++++ module/Common/src/ConfigProvider.php | 13 ++++++++++ .../Common/src}/Factory/CacheFactory.php | 2 +- .../src}/Factory/EntityManagerFactory.php | 2 +- .../Adapter/PaginableRepositoryAdapter.php | 4 +-- .../Paginator/Util/PaginatorUtilsTrait.php | 2 +- .../PaginableRepositoryInterface.php | 2 +- .../Common/src}/Util/StringUtilsTrait.php | 2 +- .../Common/test}/Factory/CacheFactoryTest.php | 4 +-- .../Factory/EntityManagerFactoryTest.php | 4 +-- .../src/Action/ListShortcodesMiddleware.php | 2 +- src/Entity/RestToken.php | 2 +- .../ShortUrlRepositoryInterface.php | 1 + src/Service/ShortUrlService.php | 2 +- 17 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 module/Common/config/services.config.php create mode 100644 module/Common/src/ConfigProvider.php rename {src => module/Common/src}/Factory/CacheFactory.php (96%) rename {src => module/Common/src}/Factory/EntityManagerFactory.php (97%) rename {src => module/Common/src}/Paginator/Adapter/PaginableRepositoryAdapter.php (91%) rename {src => module/Common/src}/Paginator/Util/PaginatorUtilsTrait.php (93%) rename {src => module/Common/src}/Repository/PaginableRepositoryInterface.php (92%) rename {src => module/Common/src}/Util/StringUtilsTrait.php (96%) rename {tests => module/Common/test}/Factory/CacheFactoryTest.php (91%) rename {tests => module/Common/test}/Factory/EntityManagerFactoryTest.php (88%) diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index bf5b28e4..00304e5f 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,11 +1,7 @@ Twig\TwigRendererFactory::class, // Services - EntityManager::class => EntityManagerFactory::class, - GuzzleHttp\Client::class => InvokableFactory::class, Service\UrlShortener::class => AnnotatedFactory::class, Service\VisitsTracker::class => AnnotatedFactory::class, Service\ShortUrlService::class => AnnotatedFactory::class, - Cache::class => CacheFactory::class, // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ - 'em' => EntityManager::class, - 'httpClient' => GuzzleHttp\Client::class, Router\RouterInterface::class => Router\FastRouteRouter::class, - AnnotatedFactory::CACHE_SERVICE => Cache::class, ], ], diff --git a/config/config.php b/config/config.php index 0fad2300..5b1fedf9 100644 --- a/config/config.php +++ b/config/config.php @@ -1,5 +1,6 @@ [ + 'factories' => [ + EntityManager::class => EntityManagerFactory::class, + GuzzleHttp\Client::class => InvokableFactory::class, + Cache::class => CacheFactory::class, + ], + 'aliases' => [ + 'em' => EntityManager::class, + 'httpClient' => GuzzleHttp\Client::class, + AnnotatedFactory::CACHE_SERVICE => Cache::class, + ], + ], + +]; diff --git a/module/Common/src/ConfigProvider.php b/module/Common/src/ConfigProvider.php new file mode 100644 index 00000000..9af040c2 --- /dev/null +++ b/module/Common/src/ConfigProvider.php @@ -0,0 +1,13 @@ + Date: Tue, 19 Jul 2016 18:01:39 +0200 Subject: [PATCH 09/86] Created Core module --- composer.json | 2 -- config/autoload/services.global.php | 11 ---------- config/config.php | 2 ++ .../src/Command/GenerateShortcodeCommand.php | 7 +++---- module/CLI/src/Command/GetVisitsCommand.php | 4 ++-- .../CLI/src/Command/ListShortcodesCommand.php | 4 ++-- module/CLI/src/Command/ResolveUrlCommand.php | 6 +++--- .../Common/src}/Entity/AbstractEntity.php | 2 +- .../Core/config/routes.config.php | 4 ++-- module/Core/config/services.config.php | 20 +++++++++++++++++++ .../Core/src/Action}/RedirectMiddleware.php | 10 +++++----- module/Core/src/ConfigProvider.php | 13 ++++++++++++ {src => module/Core/src}/Entity/RestToken.php | 3 ++- {src => module/Core/src}/Entity/ShortUrl.php | 5 +++-- {src => module/Core/src}/Entity/Visit.php | 3 ++- .../Core/src/Exception/ExceptionInterface.php | 6 ++++++ .../Exception/InvalidArgumentException.php | 2 +- .../Exception/InvalidShortCodeException.php | 2 +- .../src}/Exception/InvalidUrlException.php | 2 +- .../Core/src}/Exception/RuntimeException.php | 2 +- .../src}/Repository/ShortUrlRepository.php | 5 ++--- .../ShortUrlRepositoryInterface.php | 2 +- .../Core/src}/Service/ShortUrlService.php | 6 +++--- .../src}/Service/ShortUrlServiceInterface.php | 4 ++-- .../Core/src}/Service/UrlShortener.php | 10 +++++----- .../src}/Service/UrlShortenerInterface.php | 8 ++++---- .../Core/src}/Service/VisitsTracker.php | 9 ++++----- .../src}/Service/VisitsTrackerInterface.php | 4 ++-- .../test}/Service/ShortUrlServiceTest.php | 8 ++++---- .../Core/test}/Service/UrlShortenerTest.php | 7 +++---- .../Core/test}/Service/VisitsTrackerTest.php | 6 +++--- .../src/Action/CreateShortcodeMiddleware.php | 6 +++--- .../Rest/src/Action/GetVisitsMiddleware.php | 6 +++--- .../src/Action/ListShortcodesMiddleware.php | 4 ++-- .../Rest/src/Action/ResolveUrlMiddleware.php | 6 +++--- .../src/Exception/AuthenticationException.php | 2 +- .../CheckAuthenticationMiddleware.php | 2 +- module/Rest/src/Service/RestTokenService.php | 4 ++-- .../src/Service/RestTokenServiceInterface.php | 4 ++-- module/Rest/src/Util/RestUtils.php | 2 +- src/Exception/ExceptionInterface.php | 6 ------ 41 files changed, 121 insertions(+), 100 deletions(-) rename {src => module/Common/src}/Entity/AbstractEntity.php (92%) rename config/autoload/routes.global.php => module/Core/config/routes.config.php (63%) create mode 100644 module/Core/config/services.config.php rename {src/Middleware/Routable => module/Core/src/Action}/RedirectMiddleware.php (91%) create mode 100644 module/Core/src/ConfigProvider.php rename {src => module/Core/src}/Entity/RestToken.php (95%) rename {src => module/Core/src}/Entity/ShortUrl.php (95%) rename {src => module/Core/src}/Entity/Visit.php (97%) create mode 100644 module/Core/src/Exception/ExceptionInterface.php rename {src => module/Core/src}/Exception/InvalidArgumentException.php (71%) rename {src => module/Core/src}/Exception/InvalidShortCodeException.php (90%) rename {src => module/Core/src}/Exception/InvalidUrlException.php (88%) rename {src => module/Core/src}/Exception/RuntimeException.php (68%) rename {src => module/Core/src}/Repository/ShortUrlRepository.php (93%) rename {src => module/Core/src}/Repository/ShortUrlRepositoryInterface.php (83%) rename {src => module/Core/src}/Service/ShortUrlService.php (88%) rename {src => module/Core/src}/Service/ShortUrlServiceInterface.php (70%) rename {src => module/Core/src}/Service/UrlShortener.php (94%) rename {src => module/Core/src}/Service/UrlShortenerInterface.php (74%) rename {src => module/Core/src}/Service/VisitsTracker.php (90%) rename {src => module/Core/src}/Service/VisitsTrackerInterface.php (86%) rename {tests => module/Core/test}/Service/ShortUrlServiceTest.php (86%) rename {tests => module/Core/test}/Service/UrlShortenerTest.php (96%) rename {tests => module/Core/test}/Service/VisitsTrackerTest.php (85%) delete mode 100644 src/Exception/ExceptionInterface.php diff --git a/composer.json b/composer.json index a64197a6..0211ed05 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,6 @@ }, "autoload": { "psr-4": { - "Acelaya\\UrlShortener\\": "src", "Shlinkio\\Shlink\\CLI\\": "module/CLI/src", "Shlinkio\\Shlink\\Rest\\": "module/Rest/src", "Shlinkio\\Shlink\\Core\\": "module/Core/src", @@ -46,7 +45,6 @@ }, "autoload-dev": { "psr-4": { - "AcelayaTest\\UrlShortener\\": "tests", "ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test", "ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test", "ShlinkioTest\\Shlink\\Core\\": "module/Core/test", diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 00304e5f..dc99033c 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,7 +1,4 @@ Container\TemplatedErrorHandlerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, - - // Services - Service\UrlShortener::class => AnnotatedFactory::class, - Service\VisitsTracker::class => AnnotatedFactory::class, - Service\ShortUrlService::class => AnnotatedFactory::class, - - // Middleware - Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ Router\RouterInterface::class => Router\FastRouteRouter::class, diff --git a/config/config.php b/config/config.php index 5b1fedf9..736b8fd4 100644 --- a/config/config.php +++ b/config/config.php @@ -1,6 +1,7 @@ 'long-url-redirect', 'path' => '/{shortCode}', - 'middleware' => Routable\RedirectMiddleware::class, + 'middleware' => RedirectMiddleware::class, 'allowed_methods' => ['GET'], ], ], diff --git a/module/Core/config/services.config.php b/module/Core/config/services.config.php new file mode 100644 index 00000000..cab7ca41 --- /dev/null +++ b/module/Core/config/services.config.php @@ -0,0 +1,20 @@ + [ + 'factories' => [ + // Services + Service\UrlShortener::class => AnnotatedFactory::class, + Service\VisitsTracker::class => AnnotatedFactory::class, + Service\ShortUrlService::class => AnnotatedFactory::class, + + // Middleware + RedirectMiddleware::class => AnnotatedFactory::class, + ], + ], + +]; diff --git a/src/Middleware/Routable/RedirectMiddleware.php b/module/Core/src/Action/RedirectMiddleware.php similarity index 91% rename from src/Middleware/Routable/RedirectMiddleware.php rename to module/Core/src/Action/RedirectMiddleware.php index 85b42eb6..5b1907ca 100644 --- a/src/Middleware/Routable/RedirectMiddleware.php +++ b/module/Core/src/Action/RedirectMiddleware.php @@ -1,13 +1,13 @@ Date: Tue, 19 Jul 2016 18:05:06 +0200 Subject: [PATCH 10/86] Fixed elements broken on module separation --- module/Common/src/Factory/EntityManagerFactory.php | 2 +- module/Core/test/Service/UrlShortenerTest.php | 6 +++--- phpunit.xml.dist | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/module/Common/src/Factory/EntityManagerFactory.php b/module/Common/src/Factory/EntityManagerFactory.php index d6de017c..e42abb40 100644 --- a/module/Common/src/Factory/EntityManagerFactory.php +++ b/module/Common/src/Factory/EntityManagerFactory.php @@ -33,7 +33,7 @@ class EntityManagerFactory implements FactoryInterface $dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : []; return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration( - ['src/Entity'], + ['module/Core/src/Entity'], $isDevMode, 'data/proxies', $cache, diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index 0c4bae4d..f67086b5 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -65,7 +65,7 @@ class UrlShortenerTest extends TestCase /** * @test - * @expectedException \Acelaya\UrlShortener\Exception\RuntimeException + * @expectedException \Shlinkio\Shlink\Core\Exception\RuntimeException */ public function exceptionIsThrownWhenOrmThrowsException() { @@ -81,7 +81,7 @@ class UrlShortenerTest extends TestCase /** * @test - * @expectedException \Acelaya\UrlShortener\Exception\InvalidUrlException + * @expectedException \Shlinkio\Shlink\Core\Exception\InvalidUrlException */ public function exceptionIsThrownWhenUrlDoesNotExist() { @@ -126,7 +126,7 @@ class UrlShortenerTest extends TestCase /** * @test - * @expectedException \Acelaya\UrlShortener\Exception\InvalidShortCodeException + * @expectedException \Shlinkio\Shlink\Core\Exception\InvalidShortCodeException */ public function invalidCharSetThrowsException() { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0721b191..61fefa13 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,10 @@ - - ./tests + + ./module/Common/test + + + ./module/Core/test From f917697b8eb677b72fc6ec85d7abe4ff5d538e11 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 18:19:05 +0200 Subject: [PATCH 11/86] Added first tests to Rest module --- .../Middleware/CrossDomainMiddlewareTest.php | 54 +++++++++++++++++++ phpunit.xml.dist | 3 ++ 2 files changed, 57 insertions(+) create mode 100644 module/Rest/test/Middleware/CrossDomainMiddlewareTest.php diff --git a/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php new file mode 100644 index 00000000..92656c82 --- /dev/null +++ b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php @@ -0,0 +1,54 @@ +middleware = new CrossDomainMiddleware(); + } + + /** + * @test + */ + public function anyRequestIncludesTheAllowAccessHeader() + { + $response = $this->middleware->__invoke( + ServerRequestFactory::fromGlobals(), + new Response(), + function ($req, $resp) { + return $resp; + } + ); + + $headers = $response->getHeaders(); + $this->assertArrayHasKey('Access-Control-Allow-Origin', $headers); + $this->assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + } + + /** + * @test + */ + public function optionsRequestIncludesMoreHeaders() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_METHOD' => 'OPTIONS']); + + $response = $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + + $headers = $response->getHeaders(); + $this->assertArrayHasKey('Access-Control-Allow-Origin', $headers); + $this->assertArrayHasKey('Access-Control-Allow-Headers', $headers); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 61fefa13..2c43e876 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,6 +6,9 @@ ./module/Core/test + + ./module/Rest/test + From 923abdf4d2c88eb6797350e2b9bb5a4e99abc20f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 18:28:21 +0200 Subject: [PATCH 12/86] Added first tests for CLI module --- .../test/Factory/ApplicationFactoryTest.php | 60 +++++++++++++++++++ phpunit.xml.dist | 3 + 2 files changed, 63 insertions(+) create mode 100644 module/CLI/test/Factory/ApplicationFactoryTest.php diff --git a/module/CLI/test/Factory/ApplicationFactoryTest.php b/module/CLI/test/Factory/ApplicationFactoryTest.php new file mode 100644 index 00000000..1c486e55 --- /dev/null +++ b/module/CLI/test/Factory/ApplicationFactoryTest.php @@ -0,0 +1,60 @@ +factory = new ApplicationFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke($this->createServiceManager(), ''); + $this->assertInstanceOf(Application::class, $instance); + } + + /** + * @test + */ + public function allCommandsWhichAreServicesAreAdded() + { + $sm = $this->createServiceManager([ + 'commands' => [ + 'foo', + 'bar', + 'baz', + ], + ]); + $sm->setService('foo', $this->prophesize(Command::class)->reveal()); + $sm->setService('baz', $this->prophesize(Command::class)->reveal()); + + /** @var Application $instance */ + $instance = $this->factory->__invoke($sm, ''); + $this->assertInstanceOf(Application::class, $instance); + $this->assertCount(2, $instance->all()); + } + + protected function createServiceManager($config = []) + { + return new ServiceManager(['services' => [ + 'config' => [ + 'cli' => $config, + ], + ]]); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2c43e876..65d36865 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,9 @@ ./module/Rest/test + + ./module/CLI/test + From 39598d8608c34af4ec47b1622777b80c9f624bbc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 18:32:59 +0200 Subject: [PATCH 13/86] Moved templates and templates config to Core module --- config/autoload/templates.global.php | 6 ------ config/autoload/zend-expressive.global.php | 8 +------- module/Core/config/templates.config.php | 11 +++++++++++ module/Core/config/zend-expressive.config.php | 12 ++++++++++++ .../Core/templates/core}/error/404.html.twig | 2 +- .../Core/templates/core}/error/error.html.twig | 2 +- .../Core/templates/core}/layout/default.html.twig | 0 7 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 module/Core/config/templates.config.php create mode 100644 module/Core/config/zend-expressive.config.php rename {templates => module/Core/templates/core}/error/404.html.twig (89%) rename {templates => module/Core/templates/core}/error/error.html.twig (91%) rename {templates => module/Core/templates/core}/layout/default.html.twig (100%) diff --git a/config/autoload/templates.global.php b/config/autoload/templates.global.php index 2beb9918..f102baa0 100644 --- a/config/autoload/templates.global.php +++ b/config/autoload/templates.global.php @@ -2,12 +2,6 @@ return [ - 'templates' => [ - 'paths' => [ - 'templates' - ], - ], - 'twig' => [ 'cache_dir' => 'data/cache/twig', 'extensions' => [ diff --git a/config/autoload/zend-expressive.global.php b/config/autoload/zend-expressive.global.php index b2102394..aa2e9d3b 100644 --- a/config/autoload/zend-expressive.global.php +++ b/config/autoload/zend-expressive.global.php @@ -1,14 +1,8 @@ false, + 'debug' => false, 'config_cache_enabled' => true, - 'zend-expressive' => [ - 'error_handler' => [ - 'template_404' => 'error/404.html.twig', - 'template_error' => 'error/error.html.twig', - ], - ], ]; diff --git a/module/Core/config/templates.config.php b/module/Core/config/templates.config.php new file mode 100644 index 00000000..da3623c1 --- /dev/null +++ b/module/Core/config/templates.config.php @@ -0,0 +1,11 @@ + [ + 'paths' => [ + 'module/Core/templates', + ], + ], + +]; diff --git a/module/Core/config/zend-expressive.config.php b/module/Core/config/zend-expressive.config.php new file mode 100644 index 00000000..c5fefe8f --- /dev/null +++ b/module/Core/config/zend-expressive.config.php @@ -0,0 +1,12 @@ + [ + 'error_handler' => [ + 'template_404' => 'core/error/404.html.twig', + 'template_error' => 'core/error/error.html.twig', + ], + ], + +]; diff --git a/templates/error/404.html.twig b/module/Core/templates/core/error/404.html.twig similarity index 89% rename from templates/error/404.html.twig rename to module/Core/templates/core/error/404.html.twig index 0e591e2a..0c28f29d 100644 --- a/templates/error/404.html.twig +++ b/module/Core/templates/core/error/404.html.twig @@ -1,4 +1,4 @@ -{% extends 'layout/default.html.twig' %} +{% extends 'core/layout/default.html.twig' %} {% block title %}URL Not Found{% endblock %} diff --git a/templates/error/error.html.twig b/module/Core/templates/core/error/error.html.twig similarity index 91% rename from templates/error/error.html.twig rename to module/Core/templates/core/error/error.html.twig index cd54354e..c93b3b7e 100644 --- a/templates/error/error.html.twig +++ b/module/Core/templates/core/error/error.html.twig @@ -1,4 +1,4 @@ -{% extends 'layout/default.html.twig' %} +{% extends 'core/layout/default.html.twig' %} {% block title %}{{ status }} {{ reason }}{% endblock %} diff --git a/templates/layout/default.html.twig b/module/Core/templates/core/layout/default.html.twig similarity index 100% rename from templates/layout/default.html.twig rename to module/Core/templates/core/layout/default.html.twig From 7ca52ecff9b9c2f685cb18d7a20e2f55f8c2e184 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 18:33:45 +0200 Subject: [PATCH 14/86] Removed wrong use statement from old namespace --- module/CLI/config/services.config.php | 1 - module/Common/config/services.config.php | 1 - 2 files changed, 2 deletions(-) diff --git a/module/CLI/config/services.config.php b/module/CLI/config/services.config.php index 5c10f5a9..839e92f8 100644 --- a/module/CLI/config/services.config.php +++ b/module/CLI/config/services.config.php @@ -1,5 +1,4 @@ Date: Tue, 19 Jul 2016 20:20:18 +0200 Subject: [PATCH 15/86] Improved CrossDomainMiddleware preventing headers to be injected on non-CORS requests --- .../src/Action/AbstractRestMiddleware.php | 2 +- .../CheckAuthenticationMiddleware.php | 2 +- .../src/Middleware/CrossDomainMiddleware.php | 29 +++++++++++------ .../Middleware/CrossDomainMiddlewareTest.php | 32 ++++++++++++++++--- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/module/Rest/src/Action/AbstractRestMiddleware.php b/module/Rest/src/Action/AbstractRestMiddleware.php index 9ef870f4..a273d248 100644 --- a/module/Rest/src/Action/AbstractRestMiddleware.php +++ b/module/Rest/src/Action/AbstractRestMiddleware.php @@ -34,7 +34,7 @@ abstract class AbstractRestMiddleware implements MiddlewareInterface */ public function __invoke(Request $request, Response $response, callable $out = null) { - if (strtolower($request->getMethod()) === 'options') { + if ($request->getMethod() === 'OPTIONS') { return $response; } diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index 0de25f8f..2910aad8 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -63,7 +63,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface /** @var RouteResult $routeResult */ $routeResult = $request->getAttribute(RouteResult::class); if ((isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') - || strtolower($request->getMethod()) === 'options' + || $request->getMethod() === 'OPTIONS' ) { return $out($request, $response); } diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index ed3d333e..4fba6944 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -36,17 +36,26 @@ class CrossDomainMiddleware implements MiddlewareInterface { /** @var Response $response */ $response = $out($request, $response); - - if (strtolower($request->getMethod()) === 'options') { - $response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') - ->withHeader('Access-Control-Max-Age', '1000') - ->withHeader( - // Allow all requested headers - 'Access-Control-Allow-Headers', - $request->getHeaderLine('Access-Control-Request-Headers') - ); + if (! $request->hasHeader('Origin')) { + return $response; } - return $response->withHeader('Access-Control-Allow-Origin', '*'); + // Add Allow-Origin header + $response = $response->withHeader('Access-Control-Allow-Origin', '*'); + if ($request->getMethod() !== 'OPTIONS') { + return $response; + } + + // Add OPTIONS-specific headers + $headers = [ + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', // TODO Should be based on path + 'Access-Control-Max-Age' => '1000', + 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), + ]; + foreach ($headers as $key => $value) { + $response = $response->withHeader($key, $value); + } + + return $response; } } diff --git a/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php index 92656c82..780de834 100644 --- a/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php +++ b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php @@ -21,15 +21,37 @@ class CrossDomainMiddlewareTest extends TestCase /** * @test */ - public function anyRequestIncludesTheAllowAccessHeader() + public function nonCrossDomainRequestsAreNotAffected() { + $originalResponse = new Response(); $response = $this->middleware->__invoke( ServerRequestFactory::fromGlobals(), - new Response(), + $originalResponse, function ($req, $resp) { return $resp; } ); + $this->assertSame($originalResponse, $response); + + $headers = $response->getHeaders(); + $this->assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + $this->assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + } + + /** + * @test + */ + public function anyRequestIncludesTheAllowAccessHeader() + { + $originalResponse = new Response(); + $response = $this->middleware->__invoke( + ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'), + $originalResponse, + function ($req, $resp) { + return $resp; + } + ); + $this->assertNotSame($originalResponse, $response); $headers = $response->getHeaders(); $this->assertArrayHasKey('Access-Control-Allow-Origin', $headers); @@ -41,11 +63,13 @@ class CrossDomainMiddlewareTest extends TestCase */ public function optionsRequestIncludesMoreHeaders() { - $request = ServerRequestFactory::fromGlobals(['REQUEST_METHOD' => 'OPTIONS']); + $originalResponse = new Response(); + $request = ServerRequestFactory::fromGlobals(['REQUEST_METHOD' => 'OPTIONS'])->withHeader('Origin', 'local'); - $response = $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + $response = $this->middleware->__invoke($request, $originalResponse, function ($req, $resp) { return $resp; }); + $this->assertNotSame($originalResponse, $response); $headers = $response->getHeaders(); $this->assertArrayHasKey('Access-Control-Allow-Origin', $headers); From e28e984278d8020ff7b67f711bf418668ac51f24 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 22:38:14 +0200 Subject: [PATCH 16/86] Improved CrossDomainMiddleware by allowing the same origin that was requested --- .../Rest/src/Middleware/CrossDomainMiddleware.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index 4fba6944..3019badf 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -41,18 +41,17 @@ class CrossDomainMiddleware implements MiddlewareInterface } // Add Allow-Origin header - $response = $response->withHeader('Access-Control-Allow-Origin', '*'); + $response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')); if ($request->getMethod() !== 'OPTIONS') { return $response; } // Add OPTIONS-specific headers - $headers = [ - 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', // TODO Should be based on path - 'Access-Control-Max-Age' => '1000', - 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), - ]; - foreach ($headers as $key => $value) { + foreach ([ + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', // TODO Should be based on path + 'Access-Control-Max-Age' => '1000', + 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), + ] as $key => $value) { $response = $response->withHeader($key, $value); } From aaf4f1dfe51b6e4fe1cd3e5c5fb12c898a36c790 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 23:30:23 +0200 Subject: [PATCH 17/86] Improved config loading so that autoloaded overrides module-specific --- README.md | 3 ++- config/config.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dafb7c27..767a2605 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# url-shortener +# Shlink + A PHP-based URL shortener application with analytics and management diff --git a/config/config.php b/config/config.php index 736b8fd4..812d7653 100644 --- a/config/config.php +++ b/config/config.php @@ -17,11 +17,11 @@ use Zend\Expressive\ConfigManager\ZendConfigProvider; return call_user_func(function () { $configManager = new ConfigManager([ - new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'), Common\ConfigProvider::class, Core\ConfigProvider::class, CLI\ConfigProvider::class, Rest\ConfigProvider::class, + new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'), ], 'data/cache/app_config.php'); return $configManager->getMergedConfig(); From 31af0eea04c603e3344e1971e4fd8411cfa82da7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 19 Jul 2016 23:35:47 +0200 Subject: [PATCH 18/86] Improved main config file and fixed tests whitelist --- config/config.php | 18 +++++++----------- phpunit.xml.dist | 5 ++++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/config/config.php b/config/config.php index 812d7653..f5010bd0 100644 --- a/config/config.php +++ b/config/config.php @@ -15,14 +15,10 @@ use Zend\Expressive\ConfigManager\ZendConfigProvider; * Obviously, if you use closures in your config you can't cache it. */ -return call_user_func(function () { - $configManager = new ConfigManager([ - Common\ConfigProvider::class, - Core\ConfigProvider::class, - CLI\ConfigProvider::class, - Rest\ConfigProvider::class, - new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'), - ], 'data/cache/app_config.php'); - - return $configManager->getMergedConfig(); -}); +return (new ConfigManager([ + Common\ConfigProvider::class, + Core\ConfigProvider::class, + CLI\ConfigProvider::class, + Rest\ConfigProvider::class, + new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'), +], 'data/cache/app_config.php'))->getMergedConfig(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 65d36865..06e1e939 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,10 @@ - src + ./module/Common/src + ./module/Core/src + ./module/Rest/src + ./module/CLI/src From c290bed3549f550bbe683739119f34270e6b45da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Jul 2016 09:35:46 +0200 Subject: [PATCH 19/86] Created VisitLocation entity --- module/Core/src/Entity/Visit.php | 25 +++ module/Core/src/Entity/VisitLocation.php | 267 +++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 module/Core/src/Entity/VisitLocation.php diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 04cfbe68..0775804c 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -40,6 +40,12 @@ class Visit extends AbstractEntity implements \JsonSerializable * @ORM\JoinColumn(name="short_url_id", referencedColumnName="id") */ protected $shortUrl; + /** + * @var VisitLocation + * @ORM\ManyToOne(targetEntity=VisitLocation::class) + * @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true) + */ + protected $visitLocation; public function __construct() { @@ -136,6 +142,24 @@ class Visit extends AbstractEntity implements \JsonSerializable return $this; } + /** + * @return VisitLocation + */ + public function getVisitLocation() + { + return $this->visitLocation; + } + + /** + * @param VisitLocation $visitLocation + * @return $this + */ + public function setVisitLocation($visitLocation) + { + $this->visitLocation = $visitLocation; + return $this; + } + /** * Specify data which should be serialized to JSON * @link http://php.net/manual/en/jsonserializable.jsonserialize.php @@ -150,6 +174,7 @@ class Visit extends AbstractEntity implements \JsonSerializable 'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null, 'remoteAddr' => $this->remoteAddr, 'userAgent' => $this->userAgent, + 'visitLocation' => $this->visitLocation, ]; } } diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php new file mode 100644 index 00000000..0ca3685c --- /dev/null +++ b/module/Core/src/Entity/VisitLocation.php @@ -0,0 +1,267 @@ +countryCode; + } + + /** + * @param string $countryCode + * @return $this + */ + public function setCountryCode($countryCode) + { + $this->countryCode = $countryCode; + return $this; + } + + /** + * @return string + */ + public function getCountryName() + { + return $this->countryName; + } + + /** + * @param string $countryName + * @return $this + */ + public function setCountryName($countryName) + { + $this->countryName = $countryName; + return $this; + } + + /** + * @return string + */ + public function getRegionName() + { + return $this->regionName; + } + + /** + * @param string $regionName + * @return $this + */ + public function setRegionName($regionName) + { + $this->regionName = $regionName; + return $this; + } + + /** + * @return string + */ + public function getCityName() + { + return $this->cityName; + } + + /** + * @param string $cityName + * @return $this + */ + public function setCityName($cityName) + { + $this->cityName = $cityName; + return $this; + } + + /** + * @return string + */ + public function getLatitude() + { + return $this->latitude; + } + + /** + * @param string $latitude + * @return $this + */ + public function setLatitude($latitude) + { + $this->latitude = $latitude; + return $this; + } + + /** + * @return string + */ + public function getLongitude() + { + return $this->longitude; + } + + /** + * @param string $longitude + * @return $this + */ + public function setLongitude($longitude) + { + $this->longitude = $longitude; + return $this; + } + + /** + * @return string + */ + public function getAreaCode() + { + return $this->areaCode; + } + + /** + * @param string $areaCode + * @return $this + */ + public function setAreaCode($areaCode) + { + $this->areaCode = $areaCode; + return $this; + } + + /** + * @return string + */ + public function getTimezone() + { + return $this->timezone; + } + + /** + * @param string $timezone + * @return $this + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone; + return $this; + } + + /** + * Exchange internal values from provided array + * + * @param array $array + * @return void + */ + public function exchangeArray(array $array) + { + if (array_key_exists('countryCode', $array)) { + $this->setCountryCode($array['countryCode']); + } + if (array_key_exists('countryName', $array)) { + $this->setCountryName($array['countryName']); + } + if (array_key_exists('regionName', $array)) { + $this->setRegionName($array['regionName']); + } + if (array_key_exists('cityName', $array)) { + $this->setCityName($array['cityName']); + } + if (array_key_exists('latitude', $array)) { + $this->setLatitude($array['latitude']); + } + if (array_key_exists('longitude', $array)) { + $this->setLongitude($array['longitude']); + } + if (array_key_exists('areaCode', $array)) { + $this->setAreaCode($array['areaCode']); + } + if (array_key_exists('timezone', $array)) { + $this->setTimezone($array['timezone']); + } + } + + /** + * Return an array representation of the object + * + * @return array + */ + public function getArrayCopy() + { + return [ + 'countryCode' => $this->countryCode, + 'countryName' => $this->countryName, + 'regionName' => $this->regionName, + 'cityName' => $this->cityName, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + 'areaCode' => $this->areaCode, + 'timezone' => $this->timezone, + ]; + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize() + { + return $this->getArrayCopy(); + } +} From 06fa33877bc53f898cbe23d050086d3f30273b65 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Jul 2016 10:13:53 +0200 Subject: [PATCH 20/86] Moved some exceptions from core to common --- module/Common/config/services.config.php | 2 ++ module/Common/src/Exception/ExceptionInterface.php | 6 ++++++ .../src/Exception/InvalidArgumentException.php | 2 +- module/{Core => Common}/src/Exception/RuntimeException.php | 2 +- module/Core/src/Exception/ExceptionInterface.php | 6 ------ module/Core/src/Exception/InvalidShortCodeException.php | 2 ++ module/Core/src/Exception/InvalidUrlException.php | 2 ++ module/Core/src/Service/UrlShortener.php | 2 +- module/Core/src/Service/UrlShortenerInterface.php | 2 +- module/Core/src/Service/VisitsTracker.php | 2 +- module/Rest/src/Action/GetVisitsMiddleware.php | 2 +- module/Rest/src/Exception/AuthenticationException.php | 2 +- .../Rest/src/Middleware/CheckAuthenticationMiddleware.php | 2 +- module/Rest/src/Service/RestTokenService.php | 2 +- module/Rest/src/Service/RestTokenServiceInterface.php | 2 +- module/Rest/src/Util/RestUtils.php | 3 ++- 16 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 module/Common/src/Exception/ExceptionInterface.php rename module/{Core => Common}/src/Exception/InvalidArgumentException.php (70%) rename module/{Core => Common}/src/Exception/RuntimeException.php (67%) delete mode 100644 module/Core/src/Exception/ExceptionInterface.php diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php index b3c4e14d..838b7896 100644 --- a/module/Common/config/services.config.php +++ b/module/Common/config/services.config.php @@ -4,6 +4,7 @@ use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManager; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; +use Shlinkio\Shlink\Common\Service\IpLocationResolver; use Zend\ServiceManager\Factory\InvokableFactory; return [ @@ -13,6 +14,7 @@ return [ EntityManager::class => EntityManagerFactory::class, GuzzleHttp\Client::class => InvokableFactory::class, Cache::class => CacheFactory::class, + IpLocationResolver::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Common/src/Exception/ExceptionInterface.php b/module/Common/src/Exception/ExceptionInterface.php new file mode 100644 index 00000000..81255f6a --- /dev/null +++ b/module/Common/src/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ + Date: Wed, 20 Jul 2016 12:37:48 +0200 Subject: [PATCH 21/86] Created service to resolve IP locations --- .../Common/src/Exception/WrongIpException.php | 10 +++++ .../Common/src/Service/IpLocationResolver.php | 42 +++++++++++++++++++ .../Service/IpLocationResolverInterface.php | 11 +++++ 3 files changed, 63 insertions(+) create mode 100644 module/Common/src/Exception/WrongIpException.php create mode 100644 module/Common/src/Service/IpLocationResolver.php create mode 100644 module/Common/src/Service/IpLocationResolverInterface.php diff --git a/module/Common/src/Exception/WrongIpException.php b/module/Common/src/Exception/WrongIpException.php new file mode 100644 index 00000000..8aa3a7bf --- /dev/null +++ b/module/Common/src/Exception/WrongIpException.php @@ -0,0 +1,10 @@ +httpClient = $httpClient; + } + + /** + * @param $ipAddress + * @return array + */ + public function resolveIpLocation($ipAddress) + { + try { + $response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress)); + return json_decode($response->getBody(), true); + } catch (GuzzleException $e) { + throw WrongIpException::fromIpAddress($ipAddress, $e); + } + } +} diff --git a/module/Common/src/Service/IpLocationResolverInterface.php b/module/Common/src/Service/IpLocationResolverInterface.php new file mode 100644 index 00000000..350c2b9c --- /dev/null +++ b/module/Common/src/Service/IpLocationResolverInterface.php @@ -0,0 +1,11 @@ + Date: Wed, 20 Jul 2016 19:00:23 +0200 Subject: [PATCH 22/86] Created services and command to process visits --- module/CLI/config/cli.config.php | 1 + module/CLI/config/services.config.php | 1 + .../CLI/src/Command/ProcessVisitsCommand.php | 74 +++++++++++++++++++ module/Core/config/services.config.php | 1 + module/Core/src/Entity/Visit.php | 4 +- module/Core/src/Entity/VisitLocation.php | 61 +++++---------- .../Core/src/Repository/VisitRepository.php | 19 +++++ .../Repository/VisitRepositoryInterface.php | 13 ++++ module/Core/src/Service/VisitService.php | 45 +++++++++++ .../src/Service/VisitServiceInterface.php | 17 +++++ 10 files changed, 190 insertions(+), 46 deletions(-) create mode 100644 module/CLI/src/Command/ProcessVisitsCommand.php create mode 100644 module/Core/src/Repository/VisitRepository.php create mode 100644 module/Core/src/Repository/VisitRepositoryInterface.php create mode 100644 module/Core/src/Service/VisitService.php create mode 100644 module/Core/src/Service/VisitServiceInterface.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 6c34f19c..6a6e4122 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -9,6 +9,7 @@ return [ Command\ResolveUrlCommand::class, Command\ListShortcodesCommand::class, Command\GetVisitsCommand::class, + Command\ProcessVisitsCommand::class, ] ], diff --git a/module/CLI/config/services.config.php b/module/CLI/config/services.config.php index 839e92f8..5c5931a4 100644 --- a/module/CLI/config/services.config.php +++ b/module/CLI/config/services.config.php @@ -13,6 +13,7 @@ return [ CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class, + CLI\Command\ProcessVisitsCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/ProcessVisitsCommand.php b/module/CLI/src/Command/ProcessVisitsCommand.php new file mode 100644 index 00000000..19692e85 --- /dev/null +++ b/module/CLI/src/Command/ProcessVisitsCommand.php @@ -0,0 +1,74 @@ +visitService = $visitService; + $this->ipLocationResolver = $ipLocationResolver; + } + + public function configure() + { + $this->setName('visit:process') + ->setDescription('Processes visits where location is not set already'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $visits = $this->visitService->getUnlocatedVisits(); + + foreach ($visits as $visit) { + $ipAddr = $visit->getRemoteAddr(); + $output->write(sprintf('Processing IP %s', $ipAddr)); + if ($ipAddr === self::LOCALHOST) { + $output->writeln(' (Ignored localhost address)'); + continue; + } + + try { + $result = $this->ipLocationResolver->resolveIpLocation($ipAddr); + $location = new VisitLocation(); + $location->exchangeArray($result); + $visit->setVisitLocation($location); + $this->visitService->saveVisit($visit); + $output->writeln(sprintf(' (Address located at "%s")', $location->getCityName())); + } catch (WrongIpException $e) { + continue; + } + } + + $output->writeln('Finished processing all IPs'); + } +} diff --git a/module/Core/config/services.config.php b/module/Core/config/services.config.php index cab7ca41..00de6a67 100644 --- a/module/Core/config/services.config.php +++ b/module/Core/config/services.config.php @@ -11,6 +11,7 @@ return [ Service\UrlShortener::class => AnnotatedFactory::class, Service\VisitsTracker::class => AnnotatedFactory::class, Service\ShortUrlService::class => AnnotatedFactory::class, + Service\VisitService::class => AnnotatedFactory::class, // Middleware RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 0775804c..a95c61b0 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -9,7 +9,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity; * @author * @link * - * @ORM\Entity + * @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\VisitRepository") * @ORM\Table(name="visits") */ class Visit extends AbstractEntity implements \JsonSerializable @@ -42,7 +42,7 @@ class Visit extends AbstractEntity implements \JsonSerializable protected $shortUrl; /** * @var VisitLocation - * @ORM\ManyToOne(targetEntity=VisitLocation::class) + * @ORM\ManyToOne(targetEntity=VisitLocation::class, cascade={"persist"}) * @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true) */ protected $visitLocation; diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php index 0ca3685c..3b9851ac 100644 --- a/module/Core/src/Entity/VisitLocation.php +++ b/module/Core/src/Entity/VisitLocation.php @@ -17,42 +17,37 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface { /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $countryCode; /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $countryName; /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $regionName; /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $cityName; /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $latitude; /** * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $longitude; /** * @var string - * @ORM\Column() - */ - protected $areaCode; - /** - * @var string - * @ORM\Column() + * @ORM\Column(nullable=true) */ protected $timezone; @@ -164,24 +159,6 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface return $this; } - /** - * @return string - */ - public function getAreaCode() - { - return $this->areaCode; - } - - /** - * @param string $areaCode - * @return $this - */ - public function setAreaCode($areaCode) - { - $this->areaCode = $areaCode; - return $this; - } - /** * @return string */ @@ -208,17 +185,17 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface */ public function exchangeArray(array $array) { - if (array_key_exists('countryCode', $array)) { - $this->setCountryCode($array['countryCode']); + if (array_key_exists('country_code', $array)) { + $this->setCountryCode($array['country_code']); } - if (array_key_exists('countryName', $array)) { - $this->setCountryName($array['countryName']); + if (array_key_exists('country_name', $array)) { + $this->setCountryName($array['country_name']); } - if (array_key_exists('regionName', $array)) { - $this->setRegionName($array['regionName']); + if (array_key_exists('region_name', $array)) { + $this->setRegionName($array['region_name']); } - if (array_key_exists('cityName', $array)) { - $this->setCityName($array['cityName']); + if (array_key_exists('city', $array)) { + $this->setCityName($array['city']); } if (array_key_exists('latitude', $array)) { $this->setLatitude($array['latitude']); @@ -226,11 +203,8 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface if (array_key_exists('longitude', $array)) { $this->setLongitude($array['longitude']); } - if (array_key_exists('areaCode', $array)) { - $this->setAreaCode($array['areaCode']); - } - if (array_key_exists('timezone', $array)) { - $this->setTimezone($array['timezone']); + if (array_key_exists('time_zone', $array)) { + $this->setTimezone($array['time_zone']); } } @@ -248,7 +222,6 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface 'cityName' => $this->cityName, 'latitude' => $this->latitude, 'longitude' => $this->longitude, - 'areaCode' => $this->areaCode, 'timezone' => $this->timezone, ]; } diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php new file mode 100644 index 00000000..ef7d7935 --- /dev/null +++ b/module/Core/src/Repository/VisitRepository.php @@ -0,0 +1,19 @@ +createQueryBuilder('v'); + $qb->where($qb->expr()->isNull('v.visitLocation')); + + return $qb->getQuery()->getResult(); + } +} diff --git a/module/Core/src/Repository/VisitRepositoryInterface.php b/module/Core/src/Repository/VisitRepositoryInterface.php new file mode 100644 index 00000000..6534d7ea --- /dev/null +++ b/module/Core/src/Repository/VisitRepositoryInterface.php @@ -0,0 +1,13 @@ +em = $em; + } + + /** + * @return Visit[] + */ + public function getUnlocatedVisits() + { + /** @var VisitRepository $repo */ + $repo = $this->em->getRepository(Visit::class); + return $repo->findUnlocatedVisits(); + } + + /** + * @param Visit $visit + */ + public function saveVisit(Visit $visit) + { + $this->em->persist($visit); + $this->em->flush(); + } +} diff --git a/module/Core/src/Service/VisitServiceInterface.php b/module/Core/src/Service/VisitServiceInterface.php new file mode 100644 index 00000000..8347fdb3 --- /dev/null +++ b/module/Core/src/Service/VisitServiceInterface.php @@ -0,0 +1,17 @@ + Date: Wed, 20 Jul 2016 19:04:38 +0200 Subject: [PATCH 23/86] Fixed wrong exception name --- module/Core/test/Service/UrlShortenerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index f67086b5..8298a8cc 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -65,7 +65,7 @@ class UrlShortenerTest extends TestCase /** * @test - * @expectedException \Shlinkio\Shlink\Core\Exception\RuntimeException + * @expectedException \Shlinkio\Shlink\Common\Exception\RuntimeException */ public function exceptionIsThrownWhenOrmThrowsException() { From d97287ab0c91e5825259b3edb9a98123bf1dfc33 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 09:36:38 +0200 Subject: [PATCH 24/86] Added option to filter by date the visits list --- module/CLI/src/Command/GetVisitsCommand.php | 6 ++- module/Common/src/Util/DateRange.php | 36 ++++++++++++++++++ .../Core/src/Repository/VisitRepository.php | 37 +++++++++++++++++++ .../Repository/VisitRepositoryInterface.php | 9 +++++ module/Core/src/Service/VisitsTracker.php | 18 ++++----- .../src/Service/VisitsTrackerInterface.php | 9 +++-- 6 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 module/Common/src/Util/DateRange.php diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php index ee4cf3ad..ae6dc4b7 100644 --- a/module/CLI/src/Command/GetVisitsCommand.php +++ b/module/CLI/src/Command/GetVisitsCommand.php @@ -70,7 +70,11 @@ class GetVisitsCommand extends Command ]); foreach ($visits as $row) { - $table->addRow(array_values($row->jsonSerialize())); + $rowData = $row->jsonSerialize(); + // Unset location info + unset($rowData['visitLocation']); + + $table->addRow(array_values($rowData)); } $table->render(); } diff --git a/module/Common/src/Util/DateRange.php b/module/Common/src/Util/DateRange.php new file mode 100644 index 00000000..8e156a20 --- /dev/null +++ b/module/Common/src/Util/DateRange.php @@ -0,0 +1,36 @@ +startDate = $startDate; + $this->endDate = $endDate; + } + + /** + * @return \DateTimeInterface + */ + public function getStartDate() + { + return $this->startDate; + } + + /** + * @return \DateTimeInterface + */ + public function getEndDate() + { + return $this->endDate; + } +} diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index ef7d7935..7b98c5d5 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -2,6 +2,8 @@ namespace Shlinkio\Shlink\Core\Repository; use Doctrine\ORM\EntityRepository; +use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; class VisitRepository extends EntityRepository implements VisitRepositoryInterface @@ -16,4 +18,39 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa return $qb->getQuery()->getResult(); } + + /** + * @param ShortUrl|int $shortUrl + * @param DateRange|null $dateRange + * @return Visit[] + */ + public function findVisitsByShortUrl($shortUrl, DateRange $dateRange = null) + { + $shortUrl = $shortUrl instanceof ShortUrl + ? $shortUrl + : $this->getEntityManager()->find(ShortUrl::class, $shortUrl); + if (! isset($dateRange)) { + $startDate = $shortUrl->getDateCreated(); + $endDate = clone $startDate; + $endDate->add(new \DateInterval('P2D')); + $dateRange = new DateRange($startDate, $endDate); + } + + $qb = $this->createQueryBuilder('v'); + $qb->where($qb->expr()->eq('v.shortUrl', ':shortUrl')) + ->setParameter('shortUrl', $shortUrl) + ->orderBy('v.date', 'DESC') ; + + // Apply date range filtering + if (! empty($dateRange->getStartDate())) { + $qb->andWhere($qb->expr()->gte('v.date', ':startDate')) + ->setParameter('startDate', $dateRange->getStartDate()); + } + if (! empty($dateRange->getEndDate())) { + $qb->andWhere($qb->expr()->lte('v.date', ':endDate')) + ->setParameter('endDate', $dateRange->getEndDate()); + } + + return $qb->getQuery()->getResult(); + } } diff --git a/module/Core/src/Repository/VisitRepositoryInterface.php b/module/Core/src/Repository/VisitRepositoryInterface.php index 6534d7ea..c65f495d 100644 --- a/module/Core/src/Repository/VisitRepositoryInterface.php +++ b/module/Core/src/Repository/VisitRepositoryInterface.php @@ -2,6 +2,8 @@ namespace Shlinkio\Shlink\Core\Repository; use Doctrine\Common\Persistence\ObjectRepository; +use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; interface VisitRepositoryInterface extends ObjectRepository @@ -10,4 +12,11 @@ interface VisitRepositoryInterface extends ObjectRepository * @return Visit[] */ public function findUnlocatedVisits(); + + /** + * @param ShortUrl|int $shortUrl + * @param DateRange|null $dateRange + * @return Visit[] + */ + public function findVisitsByShortUrl($shortUrl, DateRange $dateRange = null); } diff --git a/module/Core/src/Service/VisitsTracker.php b/module/Core/src/Service/VisitsTracker.php index 2fd053c1..87187aad 100644 --- a/module/Core/src/Service/VisitsTracker.php +++ b/module/Core/src/Service/VisitsTracker.php @@ -4,9 +4,10 @@ namespace Shlinkio\Shlink\Core\Service; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; -use Zend\Paginator\Paginator; +use Shlinkio\Shlink\Core\Repository\VisitRepository; class VisitsTracker implements VisitsTrackerInterface { @@ -62,12 +63,13 @@ class VisitsTracker implements VisitsTrackerInterface } /** - * Returns the visits on certain shortcode + * Returns the visits on certain short code * * @param $shortCode - * @return Paginator|Visit[] + * @param DateRange $dateRange + * @return Visit[] */ - public function info($shortCode) + public function info($shortCode, DateRange $dateRange = null) { /** @var ShortUrl $shortUrl */ $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ @@ -77,10 +79,8 @@ class VisitsTracker implements VisitsTrackerInterface throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode)); } - return $this->em->getRepository(Visit::class)->findBy([ - 'shortUrl' => $shortUrl, - ], [ - 'date' => 'DESC' - ]); + /** @var VisitRepository $repo */ + $repo = $this->em->getRepository(Visit::class); + return $repo->findVisitsByShortUrl($shortUrl, $dateRange); } } diff --git a/module/Core/src/Service/VisitsTrackerInterface.php b/module/Core/src/Service/VisitsTrackerInterface.php index 04966403..cec254d3 100644 --- a/module/Core/src/Service/VisitsTrackerInterface.php +++ b/module/Core/src/Service/VisitsTrackerInterface.php @@ -1,8 +1,8 @@ Date: Thu, 21 Jul 2016 09:46:12 +0200 Subject: [PATCH 25/86] Fixed typo --- module/CLI/src/Command/GetVisitsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php index ae6dc4b7..43d87de5 100644 --- a/module/CLI/src/Command/GetVisitsCommand.php +++ b/module/CLI/src/Command/GetVisitsCommand.php @@ -65,7 +65,7 @@ class GetVisitsCommand extends Command $table->setHeaders([ 'Referer', 'Date', - 'Temote Address', + 'Remote Address', 'User agent', ]); From 45d194acedb41c22f68283b93ee700db15c30d98 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 09:58:33 +0200 Subject: [PATCH 26/86] Added option to filter by date in shortcode:views CLI command --- module/CLI/src/Command/GetVisitsCommand.php | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php index 43d87de5..e7221e5e 100644 --- a/module/CLI/src/Command/GetVisitsCommand.php +++ b/module/CLI/src/Command/GetVisitsCommand.php @@ -2,6 +2,7 @@ namespace Shlinkio\Shlink\CLI\Command; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Symfony\Component\Console\Command\Command; @@ -9,6 +10,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; @@ -35,7 +37,19 @@ class GetVisitsCommand extends Command { $this->setName('shortcode:visits') ->setDescription('Returns the detailed visits information for provided short code') - ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get'); + ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get') + ->addOption( + 'startDate', + 's', + InputOption::VALUE_OPTIONAL, + 'Allows to filter visits, returning only those older than start date' + ) + ->addOption( + 'endDate', + 'e', + InputOption::VALUE_OPTIONAL, + 'Allows to filter visits, returning only those newer than end date' + ); } public function interact(InputInterface $input, OutputInterface $output) @@ -60,7 +74,10 @@ class GetVisitsCommand extends Command public function execute(InputInterface $input, OutputInterface $output) { $shortCode = $input->getArgument('shortCode'); - $visits = $this->visitsTracker->info($shortCode); + $startDate = $this->getDateOption($input, 'startDate'); + $endDate = $this->getDateOption($input, 'endDate'); + + $visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate)); $table = new Table($output); $table->setHeaders([ 'Referer', @@ -78,4 +95,14 @@ class GetVisitsCommand extends Command } $table->render(); } + + protected function getDateOption(InputInterface $input, $key) + { + $value = $input->getOption($key); + if (isset($value)) { + $value = new \DateTime($value); + } + + return $value; + } } From bdd2d6f8b245c986e60bd4b1a967d29e204f7bb3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 10:03:37 +0200 Subject: [PATCH 27/86] Improved DateRange to check if both wrapped dates are empty --- module/Common/src/Util/DateRange.php | 8 ++++++++ module/Core/src/Repository/VisitRepository.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/module/Common/src/Util/DateRange.php b/module/Common/src/Util/DateRange.php index 8e156a20..c87f402a 100644 --- a/module/Common/src/Util/DateRange.php +++ b/module/Common/src/Util/DateRange.php @@ -33,4 +33,12 @@ class DateRange { return $this->endDate; } + + /** + * @return bool + */ + public function isEmpty() + { + return is_null($this->startDate) && is_null($this->endDate); + } } diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index 7b98c5d5..743427f6 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -29,7 +29,7 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa $shortUrl = $shortUrl instanceof ShortUrl ? $shortUrl : $this->getEntityManager()->find(ShortUrl::class, $shortUrl); - if (! isset($dateRange)) { + if (! isset($dateRange) || $dateRange->isEmpty()) { $startDate = $shortUrl->getDateCreated(); $endDate = clone $startDate; $endDate->add(new \DateInterval('P2D')); From 0ef9db0bdfd3c13ac71a51cc97125c0e8f5f15be Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 10:13:09 +0200 Subject: [PATCH 28/86] Added option to filter by date in visits REST endpoint --- data/docs/rest.md | 3 +++ .../Rest/src/Action/GetVisitsMiddleware.php | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/data/docs/rest.md b/data/docs/rest.md index f93f6c48..d006c6c5 100644 --- a/data/docs/rest.md +++ b/data/docs/rest.md @@ -225,6 +225,9 @@ Posible errors: * `GET` -> `/rest/visits/{shortCode}` * Route params: * shortCode: `string` -> The shortCode from which we eant to get the visits. +* Query params: + * startDate: `string` -> If provided, only visits older that this date will be returned + * endDate: `string` -> If provided, only visits newer that this date will be returned * Headers: * X-Auth-Token: `string` -> The token provided in the authentication request diff --git a/module/Rest/src/Action/GetVisitsMiddleware.php b/module/Rest/src/Action/GetVisitsMiddleware.php index 148053ee..3e5bfbaf 100644 --- a/module/Rest/src/Action/GetVisitsMiddleware.php +++ b/module/Rest/src/Action/GetVisitsMiddleware.php @@ -5,6 +5,7 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -37,14 +38,15 @@ class GetVisitsMiddleware extends AbstractRestMiddleware public function dispatch(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode'); + $startDate = $this->getDateQueryParam($request, 'startDate'); + $endDate = $this->getDateQueryParam($request, 'endDate'); try { - $visits = $this->visitsTracker->info($shortCode); + $visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate)); return new JsonResponse([ 'visits' => [ 'data' => $visits, -// 'pagination' => [], ] ]); } catch (InvalidArgumentException $e) { @@ -59,4 +61,19 @@ class GetVisitsMiddleware extends AbstractRestMiddleware ], 500); } } + + /** + * @param Request $request + * @param $key + * @return \DateTime|null + */ + protected function getDateQueryParam(Request $request, $key) + { + $query = $request->getQueryParams(); + if (! isset($query[$key]) || empty($query[$key])) { + return null; + } + + return new \DateTime($query[$key]); + } } From 3ba51c5390b8ff6c2a637a77dd3dc182a0a98f30 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 10:16:36 +0200 Subject: [PATCH 29/86] Improved visits REST endpoint path --- data/docs/rest.md | 2 +- module/Rest/config/routes.config.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/docs/rest.md b/data/docs/rest.md index d006c6c5..09147518 100644 --- a/data/docs/rest.md +++ b/data/docs/rest.md @@ -222,7 +222,7 @@ Posible errors: **REQUEST** -* `GET` -> `/rest/visits/{shortCode}` +* `GET` -> `/rest/short-codes/{shortCode}/visits` * Route params: * shortCode: `string` -> The shortCode from which we eant to get the visits. * Query params: diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index e8abb6fe..24226840 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -30,7 +30,7 @@ return [ ], [ 'name' => 'rest-get-visits', - 'path' => '/rest/visits/{shortCode}', + 'path' => '/rest/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsMiddleware::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], From cb99130c1e8929f356441d1cc6328d6f9a8d1d73 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 15:08:46 +0200 Subject: [PATCH 30/86] Created translator and used inside one of the commands --- .env.dist | 1 + composer.json | 3 +- config/autoload/translator.global.php | 8 ++ module/CLI/config/translator.config.php | 14 ++++ module/CLI/lang/es.mo | Bin 0 -> 1238 bytes module/CLI/lang/es.po | 40 ++++++++++ .../src/Command/GenerateShortcodeCommand.php | 46 +++++++---- module/Common/config/services.config.php | 6 ++ module/Common/config/templates.config.php | 12 +++ .../Common/src/Factory/TranslatorFactory.php | 30 +++++++ .../Twig/Extension/TranslatorExtension.php | 75 ++++++++++++++++++ 11 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 config/autoload/translator.global.php create mode 100644 module/CLI/config/translator.config.php create mode 100644 module/CLI/lang/es.mo create mode 100644 module/CLI/lang/es.po create mode 100644 module/Common/config/templates.config.php create mode 100644 module/Common/src/Factory/TranslatorFactory.php create mode 100644 module/Common/src/Twig/Extension/TranslatorExtension.php diff --git a/.env.dist b/.env.dist index d6271d57..726635ba 100644 --- a/.env.dist +++ b/.env.dist @@ -3,6 +3,7 @@ APP_ENV= SHORTENED_URL_SCHEMA= SHORTENED_URL_HOSTNAME= SHORTCODE_CHARS= +DEFAULT_LOCALE= # Database DB_USER= diff --git a/composer.json b/composer.json index 0211ed05..b9be6c6d 100644 --- a/composer.json +++ b/composer.json @@ -21,10 +21,11 @@ "zendframework/zend-servicemanager": "^3.0", "zendframework/zend-paginator": "^2.6", "zendframework/zend-config": "^2.6", + "zendframework/zend-i18n": "^2.7", "mtymek/expressive-config-manager": "^0.4", + "acelaya/zsm-annotated-services": "^0.2.0", "doctrine/orm": "^2.5", "guzzlehttp/guzzle": "^6.2", - "acelaya/zsm-annotated-services": "^0.2.0", "symfony/console": "^3.0" }, "require-dev": { diff --git a/config/autoload/translator.global.php b/config/autoload/translator.global.php new file mode 100644 index 00000000..e3730927 --- /dev/null +++ b/config/autoload/translator.global.php @@ -0,0 +1,8 @@ + [ + 'locale' => getenv('DEFAULT_LOCALE') ?: 'en', + ], + +]; diff --git a/module/CLI/config/translator.config.php b/module/CLI/config/translator.config.php new file mode 100644 index 00000000..ae120db3 --- /dev/null +++ b/module/CLI/config/translator.config.php @@ -0,0 +1,14 @@ + [ + 'translation_file_patterns' => [ + [ + 'type' => 'gettext', + 'base_dir' => __DIR__ . '/../lang', + 'pattern' => '%s.mo', + ], + ], + ], + +]; diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo new file mode 100644 index 0000000000000000000000000000000000000000..08f58bd96006efffab0495d0cc27f145a5b7d370 GIT binary patch literal 1238 zcmaJ=O=}cE5FIssu%ZV=ym+V$2#U=x;4VE~Z_$phq_m@+(S4b6TF3WXtM0MM zh@NLM$;w=+X`lvDE?iB+XKbw0DyEGrm+B2QqeiX$xDzfJ77mH@Sd>T6E2G>ifZk=Fd`3EwAr>Xsa_e{`KdXoUgvKps?SsN z=G<6dLl-9DrOH`Mm9>%lLmupCliHTF4rdu&49|_;sx6Pmu`o&2hNG|%lQqe)sb)-x zR$H=L3-)#&p*xu};Tx`}O@e&Hw-a literal 0 HcmV?d00001 diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po new file mode 100644 index 00000000..896f2965 --- /dev/null +++ b/module/CLI/lang/es.po @@ -0,0 +1,40 @@ +msgid "" +msgstr "" +"Project-Id-Version: Shlink 1.0\n" +"POT-Creation-Date: 2016-07-21 15:05+0200\n" +"PO-Revision-Date: 2016-07-21 15:08+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.7.1\n" +"X-Poedit-Basepath: ..\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: translate;translatePlural\n" +"X-Poedit-SearchPath-0: src\n" +"X-Poedit-SearchPath-1: config\n" + +msgid "Generates a shortcode for provided URL and returns the short URL" +msgstr "Genera un código corto para la URL proporcionada y devuelve la URL acortada" + +msgid "The long URL to parse" +msgstr "La URL larga a procesar" + +msgid "A long URL was not provided. Which URL do you want to shorten?:" +msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?" + +msgid "A URL was not provided!" +msgstr "¡No se ha proporcionado una URL!" + +msgid "Processed URL:" +msgstr "URL procesada:" + +msgid "Generated URL:" +msgstr "URL generada:" + +#, php-format +msgid "Provided URL \"%s\" is invalid. Try with a different one." +msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente." diff --git a/module/CLI/src/Command/GenerateShortcodeCommand.php b/module/CLI/src/Command/GenerateShortcodeCommand.php index 91cbd228..0a0af9eb 100644 --- a/module/CLI/src/Command/GenerateShortcodeCommand.php +++ b/module/CLI/src/Command/GenerateShortcodeCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Zend\Diactoros\Uri; +use Zend\I18n\Translator\TranslatorInterface; class GenerateShortcodeCommand extends Command { @@ -23,26 +24,37 @@ class GenerateShortcodeCommand extends Command * @var array */ private $domainConfig; + /** + * @var TranslatorInterface + */ + private $translator; /** * GenerateShortcodeCommand constructor. * @param UrlShortenerInterface|UrlShortener $urlShortener + * @param TranslatorInterface $translator * @param array $domainConfig * - * @Inject({UrlShortener::class, "config.url_shortener.domain"}) + * @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"}) */ - public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig) - { - parent::__construct(null); + public function __construct( + UrlShortenerInterface $urlShortener, + TranslatorInterface $translator, + array $domainConfig + ) { $this->urlShortener = $urlShortener; + $this->translator = $translator; $this->domainConfig = $domainConfig; + parent::__construct(null); } public function configure() { $this->setName('shortcode:generate') - ->setDescription('Generates a shortcode for provided URL and returns the short URL') - ->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse'); + ->setDescription( + $this->translator->translate('Generates a shortcode for provided URL and returns the short URL') + ) + ->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse')); } public function interact(InputInterface $input, OutputInterface $output) @@ -54,9 +66,10 @@ class GenerateShortcodeCommand extends Command /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question( - 'A long URL was not provided. Which URL do you want to shorten?: ' - ); + $question = new Question(sprintf( + '%s ', + $this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:') + )); $longUrl = $helper->ask($input, $output, $question); if (! empty($longUrl)) { @@ -70,7 +83,7 @@ class GenerateShortcodeCommand extends Command try { if (! isset($longUrl)) { - $output->writeln('A URL was not provided!'); + $output->writeln(sprintf('%s', $this->translator->translate('A URL was not provided!'))); return; } @@ -80,13 +93,16 @@ class GenerateShortcodeCommand extends Command ->withHost($this->domainConfig['hostname']); $output->writeln([ - sprintf('Processed URL %s', $longUrl), - sprintf('Generated URL %s', $shortUrl), + sprintf('%s %s', $this->translator->translate('Processed URL:'), $longUrl), + sprintf('%s %s', $this->translator->translate('Generated URL:'), $shortUrl), ]); } catch (InvalidUrlException $e) { - $output->writeln( - sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) - ); + $output->writeln(sprintf( + '' . $this->translator->translate( + 'Provided URL "%s" is invalid. Try with a different one.' + ) . '', + $longUrl + )); } } } diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php index 838b7896..1423b543 100644 --- a/module/Common/config/services.config.php +++ b/module/Common/config/services.config.php @@ -4,7 +4,10 @@ use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManager; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; +use Shlinkio\Shlink\Common\Factory\TranslatorFactory; use Shlinkio\Shlink\Common\Service\IpLocationResolver; +use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension; +use Zend\I18n\Translator\Translator; use Zend\ServiceManager\Factory\InvokableFactory; return [ @@ -15,10 +18,13 @@ return [ GuzzleHttp\Client::class => InvokableFactory::class, Cache::class => CacheFactory::class, IpLocationResolver::class => AnnotatedFactory::class, + Translator::class => TranslatorFactory::class, + TranslatorExtension::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, 'httpClient' => GuzzleHttp\Client::class, + 'translator' => Translator::class, AnnotatedFactory::CACHE_SERVICE => Cache::class, ], ], diff --git a/module/Common/config/templates.config.php b/module/Common/config/templates.config.php new file mode 100644 index 00000000..903c1e8c --- /dev/null +++ b/module/Common/config/templates.config.php @@ -0,0 +1,12 @@ + [ + 'extensions' => [ + TranslatorExtension::class, + ], + ], + +]; diff --git a/module/Common/src/Factory/TranslatorFactory.php b/module/Common/src/Factory/TranslatorFactory.php new file mode 100644 index 00000000..e41ba2fc --- /dev/null +++ b/module/Common/src/Factory/TranslatorFactory.php @@ -0,0 +1,30 @@ +get('config'); + return Translator::factory(isset($config['translator']) ? $config['translator'] : []); + } +} diff --git a/module/Common/src/Twig/Extension/TranslatorExtension.php b/module/Common/src/Twig/Extension/TranslatorExtension.php new file mode 100644 index 00000000..48ee3f11 --- /dev/null +++ b/module/Common/src/Twig/Extension/TranslatorExtension.php @@ -0,0 +1,75 @@ +translator = $translator; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return __CLASS__; + } + + public function getFunctions() + { + return [ + new \Twig_SimpleFunction('translate', [$this, 'translate']), + new \Twig_SimpleFunction('translate_plural', [$this, 'translatePlural']), + ]; + } + + /** + * Translate a message. + * + * @param string $message + * @param string $textDomain + * @param string $locale + * @return string + */ + public function translate($message, $textDomain = 'default', $locale = null) + { + return $this->translator->translate($message, $textDomain, $locale); + } + + /** + * Translate a plural message. + * + * @param string $singular + * @param string $plural + * @param int $number + * @param string $textDomain + * @param string|null $locale + * @return string + */ + public function translatePlural( + $singular, + $plural, + $number, + $textDomain = 'default', + $locale = null + ) { + $this->translator->translatePlural($singular, $plural, $number, $textDomain, $locale); + } +} From 545fe7da70462404bfc6c2ff10c95c818dd693ff Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 15:35:52 +0200 Subject: [PATCH 31/86] Added translator and translations to GetVisitsCommand --- module/CLI/lang/es.mo | Bin 1238 -> 2199 bytes module/CLI/lang/es.po | 31 ++++++++++++++- module/CLI/src/Command/GetVisitsCommand.php | 42 +++++++++++++------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 08f58bd96006efffab0495d0cc27f145a5b7d370..2dcd57bcde8e004d9f701961ff46710a00bf1717 100644 GIT binary patch literal 2199 zcmbtU%WEV>9In;%HNI9s1o808g5qO)oEdQwI)rS@Ll-+8Li(d--p2Bz@ z<7Ww;mK?8#n^)0)GNx zOFRc7j{^zV1vY`f-Y39oz%PI}Dt-or*nb7y0{#gM@m_rx19%e{_CE!}jglkqBCOdj z*n1M=5sb&`)k{E7m3PBZuoZrw5szYo*uoECm3b3uw71ixv2>)3r=c|?m2$d(_1x&e zxtmRTFH=cY?xlt%roese$s2N+v7Ys6jQiy`B_`#2=ua5tj@(65I1VIvZpJPUAE?~3 zrIB*VyDMbb7gj4cXmmaypBcxbd5j}IlbTqkluFP4k-_BY34`;}`if&C5WU5kEk4o^ zc08Lpl2xE(oCn6vQdp)(!jH;VM3Hfw6EKLLH3>U+!iQ;@`101}7u;o1j+7qBT%}Fw z+X;=8&tNQ72M25!={B01Vu!=b0uPO6T1!(4R!l>FDw`>Lsd5;qk_T3}c__Uyn$9?U zS`E>Mue16i{X=OTm#e3TXk2zd%{mTMu9{%LUhFv}QV!r}i1Y(ad~`jH-ev1xHm2Py zSNcP0H(QO~c0XFTT>2A*RL9h5wXa64#i-M!_8W0$>D5-J6%Ix_9IP3A?zz`H)w!;8 zKI&VkT`s+`F*UkU4+=OF^;sUy&0}JBdvmvOqeuEK{FY`AI~5A*M_-N zdYO_eq{6T4^|zv>({sTbDiLjJGy?iErlo!58}CPT-vmE;hEwGc^ip%N*~VVA5WOiK z52epyYBn3ays$ElwooD$lOC2+x6)ar>T~5KO$#g3etG$nVAm8j;q}V1m@3z1^zZOw zY;5Yf%6Y_6Cx(}2UzN!@I+ixcdhjS}#pG;q?p8a-+tvqa&=7ZyZsO@PWioXC(AWeu zl6cXDmZ6~+#J}%TF6}@z>CO+gis?7y6G+)HG)X4V6KPj#f}yR+r^n!?WO|&cfq@^^ zo7wdPRl$(-si_u~th%#OeBqx<8YI9G>;aN%qQD$sD^O7bZ9C$y% zJFfPtRHhX*4nuuP?(TWlO9kQ}K+_{A5Ngx21@FsH^0KOsp;Cn)LEBcrQ?a2eCyAOK zYbYMXNQ#X)4{Fh=_JblJuPM}wPx4>I!ui>vp6w@5`&zaOw6KoCip{&PrGcoBd8k(K a#*x{_gN7!}m0YX^ed6Fm;RU^RwxZ}BpVR3L-{}@3`{`GJh_!IX7Wa+>67O(i%ovb?7(DTI$4)R qjmf}bb12Jg#>ooot0jCA6+(l26mk-a(i0UD6$*;-lT(Wmix>dt?Ib<` diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po index 896f2965..595211c2 100644 --- a/module/CLI/lang/es.po +++ b/module/CLI/lang/es.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shlink 1.0\n" -"POT-Creation-Date: 2016-07-21 15:05+0200\n" -"PO-Revision-Date: 2016-07-21 15:08+0200\n" +"POT-Creation-Date: 2016-07-21 15:28+0200\n" +"PO-Revision-Date: 2016-07-21 15:32+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: es_ES\n" @@ -38,3 +38,30 @@ msgstr "URL generada:" #, php-format msgid "Provided URL \"%s\" is invalid. Try with a different one." msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente." + +msgid "Returns the detailed visits information for provided short code" +msgstr "Devuelve la información detallada de visitas para el código corto proporcionado" + +msgid "The short code which visits we want to get" +msgstr "El código corto del cual queremos obtener las visitas" + +msgid "Allows to filter visits, returning only those older than start date" +msgstr "Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que startDate" + +msgid "Allows to filter visits, returning only those newer than end date" +msgstr "Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate" + +msgid "A short code was not provided. Which short code do you want to use?:" +msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?" + +msgid "Referer" +msgstr "Origen" + +msgid "Date" +msgstr "Fecha" + +msgid "Remote Address" +msgstr "Dirección remota" + +msgid "User agent" +msgstr "Agente de usuario" diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php index e7221e5e..c25ba27a 100644 --- a/module/CLI/src/Command/GetVisitsCommand.php +++ b/module/CLI/src/Command/GetVisitsCommand.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; +use Zend\I18n\Translator\TranslatorInterface; class GetVisitsCommand extends Command { @@ -20,35 +21,47 @@ class GetVisitsCommand extends Command * @var VisitsTrackerInterface */ private $visitsTracker; + /** + * @var TranslatorInterface + */ + private $translator; /** * GetVisitsCommand constructor. * @param VisitsTrackerInterface|VisitsTracker $visitsTracker + * @param TranslatorInterface $translator * - * @Inject({VisitsTracker::class}) + * @Inject({VisitsTracker::class, "translator"}) */ - public function __construct(VisitsTrackerInterface $visitsTracker) + public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator) { - parent::__construct(null); $this->visitsTracker = $visitsTracker; + $this->translator = $translator; + parent::__construct(null); } public function configure() { $this->setName('shortcode:visits') - ->setDescription('Returns the detailed visits information for provided short code') - ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get') + ->setDescription( + $this->translator->translate('Returns the detailed visits information for provided short code') + ) + ->addArgument( + 'shortCode', + InputArgument::REQUIRED, + $this->translator->translate('The short code which visits we want to get') + ) ->addOption( 'startDate', 's', InputOption::VALUE_OPTIONAL, - 'Allows to filter visits, returning only those older than start date' + $this->translator->translate('Allows to filter visits, returning only those older than start date') ) ->addOption( 'endDate', 'e', InputOption::VALUE_OPTIONAL, - 'Allows to filter visits, returning only those newer than end date' + $this->translator->translate('Allows to filter visits, returning only those newer than end date') ); } @@ -61,9 +74,10 @@ class GetVisitsCommand extends Command /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question( - 'A short code was not provided. Which short code do you want to use?: ' - ); + $question = new Question(sprintf( + '%s ', + $this->translator->translate('A short code was not provided. Which short code do you want to use?:') + )); $shortCode = $helper->ask($input, $output, $question); if (! empty($shortCode)) { @@ -80,10 +94,10 @@ class GetVisitsCommand extends Command $visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate)); $table = new Table($output); $table->setHeaders([ - 'Referer', - 'Date', - 'Remote Address', - 'User agent', + $this->translator->translate('Referer'), + $this->translator->translate('Date'), + $this->translator->translate('Remote Address'), + $this->translator->translate('User agent'), ]); foreach ($visits as $row) { From 8e51b51cae5d3f7dc8e66c1dad2609da4bf83d97 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 15:43:19 +0200 Subject: [PATCH 32/86] Added translator and translations to ListShortcodesCommand --- module/CLI/lang/es.mo | Bin 2199 -> 2749 bytes module/CLI/lang/es.po | 43 +++++++++++++++--- .../CLI/src/Command/ListShortcodesCommand.php | 36 ++++++++++----- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 2dcd57bcde8e004d9f701961ff46710a00bf1717..002ecddd3c899feefce8fafe3ebbccf18725531c 100644 GIT binary patch delta 1004 zcmX|;O-K}B7{_0Cx75B&%RbEL4Ky_f68t!}nQ-qz0 zAu@;{ymlz|CUsgm*1d}mc?vpojfj5F?C1l}`P{NA8kv0XX$p*z} zk^bWJk{!&gO0q&}Cf0$oMRiOrnEWR(8II+s!?8KKfv7WnGj?{GMF1WtsC4X}pqqFIIJ8+D; zRWbU$81|hx zqdhCDll;t=aw8X$3x#vJk>UDW;d5>{8{Z1a_W4vVHr~oeYRcG^?9`4)mDp~h?k8tj z?+%F;QsTg|BB`IMp+Xp2kwTdcc7(0Aew>_W)E##rNaKMj(<5}NCfx0%tyCTbMWFFa y)%bCM$N-VeafXu7wtFKt8k^(=GS9L|rrA}RTb^zCtCqu-EZx}jVz;_mTm1+4iNRX{ delta 456 zcmX}nJxc>Y5P;#iz1x_?uS5ufSZEW2h)4vr5l@X`5!7HS$RUCJ0I{)16Cv6zVy7sU zX(U*QV55y_;SUf6I}3X&-#LQ|bI;E0-OSEi?lfC}8wgH>l_iFWD`JqC@OZGAsQD2t z;02cO0~axGM3%9J0k*IYuQ9~?PVV_4v*a1$ zh}d|e lUV^e)vY{!?xrWV~>0-NOABshortUrlService = $shortUrlService; + $this->translator = $translator; + parent::__construct(null); } public function configure() { $this->setName('shortcode:list') - ->setDescription('List all short URLs') + ->setDescription($this->translator->translate('List all short URLs')) ->addOption( 'page', 'p', InputOption::VALUE_OPTIONAL, - sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE), + sprintf( + $this->translator->translate('The first page to list (%s items per page)'), + PaginableRepositoryAdapter::ITEMS_PER_PAGE + ), 1 ); } @@ -59,10 +69,10 @@ class ListShortcodesCommand extends Command $page++; $table = new Table($output); $table->setHeaders([ - 'Short code', - 'Original URL', - 'Date created', - 'Visits count', + $this->translator->translate('Short code'), + $this->translator->translate('Original URL'), + $this->translator->translate('Date created'), + $this->translator->translate('Visits count'), ]); foreach ($result as $row) { @@ -72,10 +82,14 @@ class ListShortcodesCommand extends Command if ($this->isLastPage($result)) { $continue = false; - $output->writeln('You have reached last page'); + $output->writeln( + sprintf('%s', $this->translator->translate('You have reached last page')) + ); } else { $continue = $helper->ask($input, $output, new ConfirmationQuestion( - sprintf('Continue with page %s? (y/N) ', $page), + sprintf('' . $this->translator->translate( + 'Continue with page' + ) . ' %s? (y/N) ', $page), false )); } From 6a05265a4846c14c706488f82cd5aebd8daca1a1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 15:50:27 +0200 Subject: [PATCH 33/86] Added translator and translations to ProcessVisitsCommand --- module/CLI/lang/es.mo | Bin 2749 -> 3165 bytes module/CLI/lang/es.po | 20 +++++++++- .../CLI/src/Command/ProcessVisitsCommand.php | 35 +++++++++++++----- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 002ecddd3c899feefce8fafe3ebbccf18725531c..8c24a7b497614ef7e8e972d2c9326a246ca0d746 100644 GIT binary patch delta 1050 zcmYk)Pe>I(9Ki8;K3_iVm1U{V)EtFM!~O_j8T~Od;X%-oqEnac?dDt9cH_-bWQ34` zoq||`qDy(|;LSrBbn;RVbSgVk6hYT6UW$UgzumDmFNYw@?rM-R&DhEn*_;SNs}X%swHDie7#A+k}So<*S0-L^Y7L5Dv=tFjR zR8}}fUv-Te8hx$m)SP4|H}JJ$}pIHA7ogsS{+gHm}ZUw2?a7QTE#`Xb zx+mP4Q>nNsOMbGMJ7d|FcKjsYHw~-(U=grtiyr~^XpNkcbB zbEJ>d&isd-!f&p8*QVy5F~|NRGj4B~vOVAUwu_cEYxV;vYp!t-NczlE@X0)ec1visitService = $visitService; $this->ipLocationResolver = $ipLocationResolver; + $this->translator = $translator; + parent::__construct(null); } public function configure() { $this->setName('visit:process') - ->setDescription('Processes visits where location is not set already'); + ->setDescription( + $this->translator->translate('Processes visits where location is not set yet') + ); } public function execute(InputInterface $input, OutputInterface $output) @@ -51,9 +63,11 @@ class ProcessVisitsCommand extends Command foreach ($visits as $visit) { $ipAddr = $visit->getRemoteAddr(); - $output->write(sprintf('Processing IP %s', $ipAddr)); + $output->write(sprintf('%s %s', $this->translator->translate('Processing IP'), $ipAddr)); if ($ipAddr === self::LOCALHOST) { - $output->writeln(' (Ignored localhost address)'); + $output->writeln( + sprintf(' (%s)', $this->translator->translate('Ignored localhost address')) + ); continue; } @@ -63,12 +77,15 @@ class ProcessVisitsCommand extends Command $location->exchangeArray($result); $visit->setVisitLocation($location); $this->visitService->saveVisit($visit); - $output->writeln(sprintf(' (Address located at "%s")', $location->getCityName())); + $output->writeln(sprintf( + ' (' . $this->translator->translate('Address located at "%s"') . ')', + $location->getCityName() + )); } catch (WrongIpException $e) { continue; } } - $output->writeln('Finished processing all IPs'); + $output->writeln($this->translator->translate('Finished processing all IPs')); } } From 73a35a8f449bc3d0974f86f50d0b0e15d030c31e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 16:01:16 +0200 Subject: [PATCH 34/86] Added translator and translations to ResolveUrlCommand --- module/CLI/lang/es.mo | Bin 3165 -> 3826 bytes module/CLI/lang/es.po | 25 ++++++++++- module/CLI/src/Command/ResolveUrlCommand.php | 41 +++++++++++++------ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 8c24a7b497614ef7e8e972d2c9326a246ca0d746..5d5830b3db3fb3b331f55692446c7766ffeddf52 100644 GIT binary patch delta 1205 zcmZ9~Uq}=|9Ki9p(=+w@W0rsVqfKTK2boqHNR*Wdf%HcS3PRYt@mx4}Y;W(h2&5pW z*A@#3A*l$Wx1)z%dJ%-^p?d2@A)=_ai15XizQ0XF8gA#aJ9{(p`~7AY>VGv%EmoIK zD_S$xPOhUdr8@9(oQ~F3qEsEW<8eHVeRvmp@Waacno^}s@Vp;Sp~Y$}B0n`xw;tc& z4*Y~0m6}q&x#?!0B%#zc?7=NKgcazc#v3RL%peoh3v9*LxBe2V?} z6?^d@lN)dn&*FU?#BW&7{;Dgf6hW&&l#*RSi7<J##2+Qb z9Y1G^OYvv1RKN8y`ochempWrbJ!11-n%>vBQR@eK*rttc+M8<%AC}&0J?Gnkn>OkH zU+PgOryWnbUct$_(flzd2vdnm4Fe{~`(925qef>}cGFkPsLM7^IFZPN^NHv2j>DlX zYl(Ms79W&-iN$Pqy}Z5ZYTh-z$q|EBFn-_`ZTWJnCF}SZCyF;`*jF2m_kr)Et@d0m zllL4+BpgcCl}2%!%Hpx!tR9{#rrnHXF+Z?++_&S_A9k%L^W=XzaE)hl-jn1B-byla uvt(n#+T>VUk16C$wm=fm`al>uZ32I8Hit5E?H=LH~~kb{4Xk z1^xq8R@fkqvL)GgEPQ|0O@4Ly+;i^l-t#+;x9DvoaTj)P3Qc)TKoB!uySRo7uT{ZOi1 zp)_J2{dtVy2@c^Ej$mb)NCX#=e~Llffo-h9L)5}AaS)$S^BbtLnE7RZ2VLWUYz zAD2)+JjX7)MrxBtx=0q5;xIO%<}G6rZlE67nbW^V&Ch2NEvy>LupJXDSn_E9LzDEZ zG<1t~)mkw-{uf%X7RzGnW6N>g=uxsUc0oGC+;->@glL&Gy=xjQro!Gb?vj1^bdY;u zABSBr&wf*vvXczCCoHq;*)VbMn7Q+gn_=I&E7oEjeU+|QYtourlShortener = $urlShortener; + $this->translator = $translator; + parent::__construct(null); } public function configure() { $this->setName('shortcode:parse') - ->setDescription('Returns the long URL behind a short code') - ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse'); + ->setDescription($this->translator->translate('Returns the long URL behind a short code')) + ->addArgument( + 'shortCode', + InputArgument::REQUIRED, + $this->translator->translate('The short code to parse') + ); } public function interact(InputInterface $input, OutputInterface $output) @@ -47,9 +58,10 @@ class ResolveUrlCommand extends Command /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question( - 'A short code was not provided. Which short code do you want to parse?: ' - ); + $question = new Question(sprintf( + '%s ', + $this->translator->translate('A short code was not provided. Which short code do you want to parse?:') + )); $shortCode = $helper->ask($input, $output, $question); if (! empty($shortCode)) { @@ -64,15 +76,18 @@ class ResolveUrlCommand extends Command try { $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); if (! isset($longUrl)) { - $output->writeln(sprintf('No URL found for short code "%s"', $shortCode)); + $output->writeln(sprintf( + '' . $this->translator->translate('No URL found for short code "%s"') . '', + $shortCode + )); return; } - $output->writeln(sprintf('Long URL %s', $longUrl)); + $output->writeln(sprintf('%s %s', $this->translator->translate('Long URL:'), $longUrl)); } catch (InvalidShortCodeException $e) { - $output->writeln( - sprintf('Provided short code "%s" has an invalid format.', $shortCode) - ); + $output->writeln(sprintf('' . $this->translator->translate( + 'Provided short code "%s" has an invalid format.' + ) . '', $shortCode)); } } } From 06868f782b74f41630ffa15c3c4d8bf4448c0cdd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 16:20:27 +0200 Subject: [PATCH 35/86] Created middleware for rest that reads the language from the Accept-Language header --- data/docs/rest.md | 6 ++ .../config/middleware-pipeline.config.php | 1 + module/Rest/config/services.config.php | 1 + module/Rest/config/translator.config.php | 14 ++++ .../Rest/src/Middleware/LocaleMiddleware.php | 82 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 module/Rest/config/translator.config.php create mode 100644 module/Rest/src/Middleware/LocaleMiddleware.php diff --git a/data/docs/rest.md b/data/docs/rest.md index 09147518..35f18e2a 100644 --- a/data/docs/rest.md +++ b/data/docs/rest.md @@ -15,6 +15,12 @@ Statuses: [TODO] +## Language + +In order to set the application language, you have to pass it by using the Accept-Language header. + +If not provided or provided language is not supported, english (en_US) will be used. + ## Endpoints #### Authenticate diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index 78c20c38..aa9889f1 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -8,6 +8,7 @@ return [ 'path' => '/rest', 'middleware' => [ Middleware\CheckAuthenticationMiddleware::class, + Middleware\LocaleMiddleware::class, Middleware\CrossDomainMiddleware::class, ], 'priority' => 5, diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index aff4cc96..093e6e41 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,6 +19,7 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, + Middleware\LocaleMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/config/translator.config.php b/module/Rest/config/translator.config.php new file mode 100644 index 00000000..ae120db3 --- /dev/null +++ b/module/Rest/config/translator.config.php @@ -0,0 +1,14 @@ + [ + 'translation_file_patterns' => [ + [ + 'type' => 'gettext', + 'base_dir' => __DIR__ . '/../lang', + 'pattern' => '%s.mo', + ], + ], + ], + +]; diff --git a/module/Rest/src/Middleware/LocaleMiddleware.php b/module/Rest/src/Middleware/LocaleMiddleware.php new file mode 100644 index 00000000..f3a4ddde --- /dev/null +++ b/module/Rest/src/Middleware/LocaleMiddleware.php @@ -0,0 +1,82 @@ +translator = $translator; + } + + /** + * Process an incoming request and/or response. + * + * Accepts a server-side request and a response instance, and does + * something with them. + * + * If the response is not complete and/or further processing would not + * interfere with the work done in the middleware, or if the middleware + * wants to delegate to another process, it can use the `$out` callable + * if present. + * + * If the middleware does not return a value, execution of the current + * request is considered complete, and the response instance provided will + * be considered the response to return. + * + * Alternately, the middleware may return a response instance. + * + * Often, middleware will `return $out();`, with the assumption that a + * later middleware will return a response. + * + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + if (! $request->hasHeader('Accept-Language')) { + return $out($request, $response); + } + + $locale = $request->getHeaderLine('Accept-Language'); + $this->translator->setLocale($this->normalizeLocale($locale)); + return $out($request, $response); + } + + /** + * @param string $locale + * @return string + */ + protected function normalizeLocale($locale) + { + $parts = explode('_', $locale); + if (count($parts) > 1) { + return $parts[0]; + } + + $parts = explode('-', $locale); + if (count($parts) > 1) { + return $parts[0]; + } + + return $locale; + } +} From e42469b0908c9650f02988a5bb7a33e46ddb3fa3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 16:41:16 +0200 Subject: [PATCH 36/86] Added translations for error messages returned by the REST API --- module/Rest/lang/es.mo | Bin 0 -> 1754 bytes module/Rest/lang/es.po | 60 ++++++++++++++++++ .../src/Action/AuthenticateMiddleware.php | 15 +++-- .../src/Action/CreateShortcodeMiddleware.php | 25 ++++++-- .../Rest/src/Action/GetVisitsMiddleware.php | 15 +++-- .../src/Action/ListShortcodesMiddleware.php | 13 +++- .../Rest/src/Action/ResolveUrlMiddleware.php | 20 ++++-- .../CheckAuthenticationMiddleware.php | 17 +++-- module/Rest/src/Service/RestTokenService.php | 2 +- module/Rest/src/Util/RestUtils.php | 2 +- 10 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 module/Rest/lang/es.mo create mode 100644 module/Rest/lang/es.po diff --git a/module/Rest/lang/es.mo b/module/Rest/lang/es.mo new file mode 100644 index 0000000000000000000000000000000000000000..2a8d4431a081c2eee3abe6973ebe7cb2bac0ce20 GIT binary patch literal 1754 zcmZ`(L5m|r6fSkuHJ}F%3xen~BDhHRBw3jmd&og&XJs(68)h=Ho}{MxB`LR4)mBw! zHuwt!4_^EMUIY&+vf#;E4*meaqu@W#f8bZ$Njl8#HoSc4>Q~?U-m7}=?^myVC-Agj zufuZK%dp>J;rSPK1NI+`VAr1!;v3*i;G4kTfP28dfHCluXN7nh*ayA_l)xVFE8suy z`5E{I-oL$8`~3n8vHk?^hV|!#xC{IP_#yDd3qo{&_kls**T7rAAAq0}&%v(4u)TUf zFR-=B=f!$SytG1W0V$oI4(L)k(#BI^&0J-iiDNyNxyqtA zNtqgt*+L;1&2h7@;-d{AO;InY*AyY-O!}w0H_^no=7$BRo-<-=A=RX5X)Q|rnJK|L zXXL)F16>%%YOfEWT@|XmK7)1$_yxG4<1G4=twTu@I-BN7KcH^hX$|jc9PWHNDlR?#+XS;LMaZ<-=N8LbbFF{U^K#&EN*gdE~Mx3?FR%)gia& zSlVULNYT{Y8mdbICrJA%FtrR1UAHV zy!;itW@x_r9t~{V(y}K_@{uNklk<$9aJy@cm`)kfKvrRO$#ckIG;!ts zIKzS%JAabIc{2{S#A80BGK0t~Ojyq)K3#B%bFCV@5zjdNkYAHLW)Zv)BV9hq)Fc$Z zqIE)hi$PVMdPZzl4*pGEGNxngmp^JRb3@1@2&(E>C5l;4jRMSpqpLx-IqQ(*6|N&T zu7-gLXRxW@;|xLOl|$WrestTokenService = $restTokenService; + $this->translator = $translator; } /** @@ -40,7 +47,7 @@ class AuthenticateMiddleware extends AbstractRestMiddleware if (! isset($authData['username'], $authData['password'])) { return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => 'You have to provide both "username" and "password"' + 'message' => $this->translator->translate('You have to provide both "username" and "password"'), ], 400); } @@ -50,7 +57,7 @@ class AuthenticateMiddleware extends AbstractRestMiddleware } catch (AuthenticationException $e) { return new JsonResponse([ 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => 'Invalid username and/or password', + 'message' => $this->translator->translate('Invalid username and/or password'), ], 401); } } diff --git a/module/Rest/src/Action/CreateShortcodeMiddleware.php b/module/Rest/src/Action/CreateShortcodeMiddleware.php index fdb0ade3..fe3074fa 100644 --- a/module/Rest/src/Action/CreateShortcodeMiddleware.php +++ b/module/Rest/src/Action/CreateShortcodeMiddleware.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Uri; +use Zend\I18n\Translator\TranslatorInterface; class CreateShortcodeMiddleware extends AbstractRestMiddleware { @@ -21,18 +22,27 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware * @var array */ private $domainConfig; + /** + * @var TranslatorInterface + */ + private $translator; /** * GenerateShortcodeMiddleware constructor. * * @param UrlShortenerInterface|UrlShortener $urlShortener + * @param TranslatorInterface $translator * @param array $domainConfig * - * @Inject({UrlShortener::class, "config.url_shortener.domain"}) + * @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"}) */ - public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig) - { + public function __construct( + UrlShortenerInterface $urlShortener, + TranslatorInterface $translator, + array $domainConfig + ) { $this->urlShortener = $urlShortener; + $this->translator = $translator; $this->domainConfig = $domainConfig; } @@ -48,7 +58,7 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware if (! isset($postData['longUrl'])) { return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => 'A URL was not provided', + 'message' => $this->translator->translate('A URL was not provided'), ], 400); } $longUrl = $postData['longUrl']; @@ -67,12 +77,15 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware } catch (InvalidUrlException $e) { return new JsonResponse([ 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl), + 'message' => sprintf( + $this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'), + $longUrl + ), ], 400); } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occured', + 'message' => $this->translator->translate('Unexpected error occurred'), ], 500); } } diff --git a/module/Rest/src/Action/GetVisitsMiddleware.php b/module/Rest/src/Action/GetVisitsMiddleware.php index 3e5bfbaf..75765389 100644 --- a/module/Rest/src/Action/GetVisitsMiddleware.php +++ b/module/Rest/src/Action/GetVisitsMiddleware.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; +use Zend\I18n\Translator\TranslatorInterface; class GetVisitsMiddleware extends AbstractRestMiddleware { @@ -17,16 +18,22 @@ class GetVisitsMiddleware extends AbstractRestMiddleware * @var VisitsTrackerInterface */ private $visitsTracker; + /** + * @var TranslatorInterface + */ + private $translator; /** * GetVisitsMiddleware constructor. * @param VisitsTrackerInterface|VisitsTracker $visitsTracker + * @param TranslatorInterface $translator * - * @Inject({VisitsTracker::class}) + * @Inject({VisitsTracker::class, "translator"}) */ - public function __construct(VisitsTrackerInterface $visitsTracker) + public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator) { $this->visitsTracker = $visitsTracker; + $this->translator = $translator; } /** @@ -52,12 +59,12 @@ class GetVisitsMiddleware extends AbstractRestMiddleware } catch (InvalidArgumentException $e) { return new JsonResponse([ 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided short code "%s" is invalid', $shortCode), + 'message' => sprintf($this->translator->translate('Provided short code "%s" is invalid'), $shortCode), ], 400); } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occured', + 'message' => $this->translator->translate('Unexpected error occurred'), ], 500); } } diff --git a/module/Rest/src/Action/ListShortcodesMiddleware.php b/module/Rest/src/Action/ListShortcodesMiddleware.php index 4361ab89..99b1c718 100644 --- a/module/Rest/src/Action/ListShortcodesMiddleware.php +++ b/module/Rest/src/Action/ListShortcodesMiddleware.php @@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; +use Zend\I18n\Translator\TranslatorInterface; class ListShortcodesMiddleware extends AbstractRestMiddleware { @@ -18,16 +19,22 @@ class ListShortcodesMiddleware extends AbstractRestMiddleware * @var ShortUrlServiceInterface */ private $shortUrlService; + /** + * @var TranslatorInterface + */ + private $translator; /** * ListShortcodesMiddleware constructor. * @param ShortUrlServiceInterface|ShortUrlService $shortUrlService + * @param TranslatorInterface $translator * - * @Inject({ShortUrlService::class}) + * @Inject({ShortUrlService::class, "translator"}) */ - public function __construct(ShortUrlServiceInterface $shortUrlService) + public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator) { $this->shortUrlService = $shortUrlService; + $this->translator = $translator; } /** @@ -45,7 +52,7 @@ class ListShortcodesMiddleware extends AbstractRestMiddleware } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occured', + 'message' => $this->translator->translate('Unexpected error occurred'), ], 500); } } diff --git a/module/Rest/src/Action/ResolveUrlMiddleware.php b/module/Rest/src/Action/ResolveUrlMiddleware.php index 92a1e5c2..db7e5827 100644 --- a/module/Rest/src/Action/ResolveUrlMiddleware.php +++ b/module/Rest/src/Action/ResolveUrlMiddleware.php @@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; +use Zend\I18n\Translator\TranslatorInterface; class ResolveUrlMiddleware extends AbstractRestMiddleware { @@ -16,16 +17,22 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware * @var UrlShortenerInterface */ private $urlShortener; + /** + * @var TranslatorInterface + */ + private $translator; /** * ResolveUrlMiddleware constructor. * @param UrlShortenerInterface|UrlShortener $urlShortener + * @param TranslatorInterface $translator * - * @Inject({UrlShortener::class}) + * @Inject({UrlShortener::class, "translator"}) */ - public function __construct(UrlShortenerInterface $urlShortener) + public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator) { $this->urlShortener = $urlShortener; + $this->translator = $translator; } /** @@ -43,7 +50,7 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware if (! isset($longUrl)) { return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => sprintf('No URL found for shortcode "%s"', $shortCode), + 'message' => sprintf($this->translator->translate('No URL found for shortcode "%s"'), $shortCode), ], 400); } @@ -53,12 +60,15 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware } catch (InvalidShortCodeException $e) { return new JsonResponse([ 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode), + 'message' => sprintf( + $this->translator->translate('Provided short code "%s" has an invalid format'), + $shortCode + ), ], 400); } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occured', + 'message' => $this->translator->translate('Unexpected error occurred'), ], 500); } } diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index 9120e2c6..f05c1b78 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\Expressive\Router\RouteResult; +use Zend\I18n\Translator\TranslatorInterface; use Zend\Stratigility\MiddlewareInterface; class CheckAuthenticationMiddleware implements MiddlewareInterface @@ -20,16 +21,22 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface * @var RestTokenServiceInterface */ private $restTokenService; + /** + * @var TranslatorInterface + */ + private $translator; /** * CheckAuthenticationMiddleware constructor. * @param RestTokenServiceInterface|RestTokenService $restTokenService + * @param TranslatorInterface $translator * - * @Inject({RestTokenService::class}) + * @Inject({RestTokenService::class, "translator"}) */ - public function __construct(RestTokenServiceInterface $restTokenService) + public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator) { $this->restTokenService = $restTokenService; + $this->translator = $translator; } /** @@ -93,8 +100,10 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface return new JsonResponse([ 'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR, 'message' => sprintf( - 'Missing or invalid auth token provided. Perform a new authentication request and send provided token ' - . 'on every new request on the "%s" header', + $this->translator->translate( + 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' + . 'token on every new request on the "%s" header' + ), self::AUTH_TOKEN_HEADER ), ], 401); diff --git a/module/Rest/src/Service/RestTokenService.php b/module/Rest/src/Service/RestTokenService.php index c452af2a..b9dd4a9d 100644 --- a/module/Rest/src/Service/RestTokenService.php +++ b/module/Rest/src/Service/RestTokenService.php @@ -21,8 +21,8 @@ class RestTokenService implements RestTokenServiceInterface /** * ShortUrlService constructor. * @param EntityManagerInterface $em - * * @param array $restConfig + * * @Inject({"em", "config.rest"}) */ public function __construct(EntityManagerInterface $em, array $restConfig) diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 82082dcd..8e50a1cf 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -21,7 +21,7 @@ class RestUtils return self::INVALID_SHORTCODE_ERROR; case $e instanceof Core\InvalidUrlException: return self::INVALID_URL_ERROR; - case $e instanceof Core\InvalidArgumentException: + case $e instanceof Common\InvalidArgumentException: return self::INVALID_ARGUMENT_ERROR; case $e instanceof Rest\AuthenticationException: return self::INVALID_CREDENTIALS_ERROR; From fb9c7f8eecc30f5e20d394e500a6ab73d15dda48 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 16:54:00 +0200 Subject: [PATCH 37/86] Used twig extension to load translations on twig templates --- module/Core/config/translator.config.php | 14 +++++++ module/Core/lang/es.mo | Bin 0 -> 1036 bytes module/Core/lang/es.po | 35 ++++++++++++++++++ .../Core/templates/core/error/404.html.twig | 8 ++-- .../Core/templates/core/error/error.html.twig | 8 ++-- 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 module/Core/config/translator.config.php create mode 100644 module/Core/lang/es.mo create mode 100644 module/Core/lang/es.po diff --git a/module/Core/config/translator.config.php b/module/Core/config/translator.config.php new file mode 100644 index 00000000..ae120db3 --- /dev/null +++ b/module/Core/config/translator.config.php @@ -0,0 +1,14 @@ + [ + 'translation_file_patterns' => [ + [ + 'type' => 'gettext', + 'base_dir' => __DIR__ . '/../lang', + 'pattern' => '%s.mo', + ], + ], + ], + +]; diff --git a/module/Core/lang/es.mo b/module/Core/lang/es.mo new file mode 100644 index 0000000000000000000000000000000000000000..d34bb83b2b79815b5e36833dd99a0c223c0b994a GIT binary patch literal 1036 zcmZuw!A=xG5N#DTS>r*@YKnsqBQx%7B9PhaL2*%Vkqs>9#h7NM*cqAa-gNhXB>sjU zzzq*3Cf+={U*W|c@Zib2uV)ZAP??w4JzaTKQ~l=G^wcMTxCYz?dcY0fBjDp0=mMXC ztH2*%7WfO?1nyoE;tqHYJbhV+Rq!hIZ?6b(9efP-`Cq}ez(2tR{tZTIRTorgd-gE} zK&lF))v+e`m~3g74s}VnN{TY&lw?tm%b1c(8ksmYc8(5mmyy!MADodiE>+@6>2j?? zv98BQ25hV+R?HuRVrRHu}ZK+8DE<2 zS^uyx+JvHS^m|TRu#yJbXanV9+RTbv?NJmq8~yb`uw=OEBX}kq$JA;@^FebVXhjsw z$B&y2o2{l#3^sT__W~E^MRi_JS{DqARJM>#o0u9ssfMK-axh>yI=7FB?Yr(~V{K)v zd)Aw8YDwc;6l4y^=*MATkH&?}RlAy}W!KpnEC-8cdA{5(o1lw1q`4Z#w78SI#+%?d zD>ii>ea&g^@X?FmLKxw!ei1yCmdDa%F@<5HUzA1`!Ll|Z81sa|$w70q-g zdeA--*wm#-cnLR+&SI)tJC%RQhX>lEwwGHR;^wQtaQkG%b;bW%$M5Pw=j2(MB @@ -10,8 +10,8 @@ {% endblock %} {% block content %} -

Oops!

+

{{ translate('Oops!') }}


-

This short URL doesn't seem to be valid.

-

Make sure you included all the characters, with no extra punctuation.

+

{{ translate('This short URL doesn\'t seem to be valid.') }}

+

{{ translate('Make sure you included all the characters, with no extra punctuation.') }}

{% endblock %} diff --git a/module/Core/templates/core/error/error.html.twig b/module/Core/templates/core/error/error.html.twig index c93b3b7e..7426a33a 100644 --- a/module/Core/templates/core/error/error.html.twig +++ b/module/Core/templates/core/error/error.html.twig @@ -10,11 +10,11 @@ {% endblock %} {% block content %} -

Oops!

+

{{ translate('Oops!') }}


-

We encountered a {{ status }} {{ reason }} error.

+

{{ translate('We encountered a %s %s error.') | format(status, reason) }}

{% if status == 404 %} -

This short URL doesn't seem to be valid.

-

Make sure you included all the characters, with no extra punctuation.

+

{{ translate('This short URL doesn\'t seem to be valid.') }}

+

{{ translate('Make sure you included all the characters, with no extra punctuation.') }}

{% endif %} {% endblock %} From cd5bbcd60a638425b3d173b60513684f06be6093 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 16:59:27 +0200 Subject: [PATCH 38/86] Reused middleware to check Accept-Language header on any HTTP related middleware --- .../Common/config/middleware-pipeline.config.php | 14 ++++++++++++++ module/Common/config/services.config.php | 2 ++ .../src/Middleware/LocaleMiddleware.php | 2 +- module/Rest/config/middleware-pipeline.config.php | 1 - module/Rest/config/services.config.php | 1 - 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 module/Common/config/middleware-pipeline.config.php rename module/{Rest => Common}/src/Middleware/LocaleMiddleware.php (98%) diff --git a/module/Common/config/middleware-pipeline.config.php b/module/Common/config/middleware-pipeline.config.php new file mode 100644 index 00000000..361621c6 --- /dev/null +++ b/module/Common/config/middleware-pipeline.config.php @@ -0,0 +1,14 @@ + [ + 'pre-routing' => [ + 'middleware' => [ + Middleware\LocaleMiddleware::class, + ], + 'priority' => 5, + ], + ], +]; diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php index 1423b543..42d9dbc5 100644 --- a/module/Common/config/services.config.php +++ b/module/Common/config/services.config.php @@ -5,6 +5,7 @@ use Doctrine\ORM\EntityManager; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; use Shlinkio\Shlink\Common\Factory\TranslatorFactory; +use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; use Shlinkio\Shlink\Common\Service\IpLocationResolver; use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension; use Zend\I18n\Translator\Translator; @@ -20,6 +21,7 @@ return [ IpLocationResolver::class => AnnotatedFactory::class, Translator::class => TranslatorFactory::class, TranslatorExtension::class => AnnotatedFactory::class, + LocaleMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Rest/src/Middleware/LocaleMiddleware.php b/module/Common/src/Middleware/LocaleMiddleware.php similarity index 98% rename from module/Rest/src/Middleware/LocaleMiddleware.php rename to module/Common/src/Middleware/LocaleMiddleware.php index f3a4ddde..20f796ff 100644 --- a/module/Rest/src/Middleware/LocaleMiddleware.php +++ b/module/Common/src/Middleware/LocaleMiddleware.php @@ -1,5 +1,5 @@ '/rest', 'middleware' => [ Middleware\CheckAuthenticationMiddleware::class, - Middleware\LocaleMiddleware::class, Middleware\CrossDomainMiddleware::class, ], 'priority' => 5, diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index 093e6e41..aff4cc96 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,7 +19,6 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, - Middleware\LocaleMiddleware::class => AnnotatedFactory::class, ], ], From 5c8353da02d150abc0196a7a9ae4ccf72f233c51 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 18:46:15 +0200 Subject: [PATCH 39/86] Set collation to utf8_bin in shortCode column of ShortUrl so that the UNIQUE key is case sensitive --- module/Core/src/Entity/ShortUrl.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index b1f524bc..f7b42895 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -23,7 +23,14 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable protected $originalUrl; /** * @var string - * @ORM\Column(name="short_code", type="string", nullable=false, length=10, unique=true) + * @ORM\Column( + * name="short_code", + * type="string", + * nullable=false, + * length=10, + * unique=true, + * options={"collation": "utf8_bin"} + * ) */ protected $shortCode; /** From 57d81115de612c31a0d5273855c1ca725c8f2a15 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Jul 2016 18:46:52 +0200 Subject: [PATCH 40/86] Added PHP 7.1 to the CI matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index eb2ead28..905480ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ branches: php: - 5.6 - 7 + - 7.1 - hhvm before_script: From 0ef1e416c63d665c0bc00425def2e7bf7c8010ad Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Jul 2016 09:54:13 +0200 Subject: [PATCH 41/86] Created middleware to catch rest errors and return JSON responses --- .../autoload/middleware-pipeline.global.php | 6 -- .../config/middleware-pipeline.config.php | 9 +++ module/Rest/config/services.config.php | 2 + .../Error/ResponseTypeMiddleware.php | 59 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index ca116a95..6e72ea1f 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -26,11 +26,5 @@ return [ ], 'priority' => 1, ], - - 'error' => [ - 'middleware' => [], - 'error' => true, - 'priority' => -10000, - ], ], ]; diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index 78c20c38..4d908635 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -12,5 +12,14 @@ return [ ], 'priority' => 5, ], + + 'rest-error' => [ + 'path' => '/rest', + 'middleware' => [ + Middleware\Error\ResponseTypeMiddleware::class, + ], + 'error' => true, + 'priority' => -10000, + ], ], ]; diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index aff4cc96..44ce97f0 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,6 +19,8 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, + + Middleware\Error\ResponseTypeMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php b/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php new file mode 100644 index 00000000..a7e2c5e8 --- /dev/null +++ b/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php @@ -0,0 +1,59 @@ +translator = $translator; + } + + /** + * Process an incoming error, along with associated request and response. + * + * Accepts an error, a server-side request, and a response instance, and + * does something with them; if further processing can be done, it can + * delegate to `$out`. + * + * @see MiddlewareInterface + * @param mixed $error + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke($error, Request $request, Response $response, callable $out = null) + { + $accept = $request->getHeader('Accept'); + if (! empty(array_intersect(['application/json', 'text/json', 'application/x-json'], $accept))) { + $status = $response->getStatusCode(); + $status = $status >= 400 ? $status : 500; + + return new JsonResponse([ + 'error' => RestUtils::UNKNOWN_ERROR, + 'message' => $this->translator->translate('Unknown error'), + ], $status); + } + + return $out($request, $response, $error); + } +} From 83f29080c6f179f0b1e3952b0d7948e65cfdcbd4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Jul 2016 11:05:17 +0200 Subject: [PATCH 42/86] Improved the way rest errors are catched --- .../Error/ResponseTypeMiddleware.php | 45 ++++++++++++++++--- module/Rest/src/Util/RestUtils.php | 3 +- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php b/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php index a7e2c5e8..c6319caa 100644 --- a/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php +++ b/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php @@ -45,15 +45,48 @@ class ResponseTypeMiddleware implements ErrorMiddlewareInterface { $accept = $request->getHeader('Accept'); if (! empty(array_intersect(['application/json', 'text/json', 'application/x-json'], $accept))) { - $status = $response->getStatusCode(); - $status = $status >= 400 ? $status : 500; + $status = $this->determineStatus($response); + $errorData = $this->determineErrorCode($request, $status); - return new JsonResponse([ - 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => $this->translator->translate('Unknown error'), - ], $status); + return new JsonResponse($errorData, $status); } return $out($request, $response, $error); } + + /** + * @param Response $response + * @return int + */ + protected function determineStatus(Response $response) + { + $status = $response->getStatusCode(); + return $status >= 400 ? $status : 500; + } + + /** + * @param Request $request + * @param int $status + * @return string + */ + protected function determineErrorCode(Request $request, $status) + { + $errorData = $request->getAttribute('errorData'); + if (isset($errorData)) { + return $errorData; + } + + switch ($status) { + case 404: + return [ + 'error' => RestUtils::NOT_FOUND_ERROR, + 'message' => $this->translator->translate('Requested route does not exist'), + ]; + default: + return [ + 'error' => RestUtils::UNKNOWN_ERROR, + 'message' => $this->translator->translate('Unknown error occured'), + ]; + } + } } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 8e50a1cf..b67491ed 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -11,7 +11,8 @@ class RestUtils const INVALID_URL_ERROR = 'INVALID_URL'; const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; - const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN_ERROR'; + const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; + const NOT_FOUND_ERROR = 'NOT_FOUND'; const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; public static function getRestErrorCodeFromException(Common\ExceptionInterface $e) From a81dba58bd468b0494c602bfa43e612aae50f29f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Jul 2016 19:09:54 +0200 Subject: [PATCH 43/86] Defined custom NotFoundMiddleware for rest routes --- module/Core/src/Action/RedirectMiddleware.php | 4 +- .../config/middleware-pipeline.config.php | 7 +-- module/Rest/config/routes.config.php | 2 +- module/Rest/config/services.config.php | 4 +- .../CheckAuthenticationMiddleware.php | 4 +- .../src/Middleware/NotFoundMiddleware.php | 62 +++++++++++++++++++ .../{Error => }/ResponseTypeMiddleware.php | 23 ++++--- 7 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 module/Rest/src/Middleware/NotFoundMiddleware.php rename module/Rest/src/Middleware/{Error => }/ResponseTypeMiddleware.php (77%) diff --git a/module/Core/src/Action/RedirectMiddleware.php b/module/Core/src/Action/RedirectMiddleware.php index 5b1907ca..1afd2d01 100644 --- a/module/Core/src/Action/RedirectMiddleware.php +++ b/module/Core/src/Action/RedirectMiddleware.php @@ -67,8 +67,8 @@ class RedirectMiddleware implements MiddlewareInterface try { $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); - // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which is 404 - // middleware + // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger + // a not-found error if (! isset($longUrl)) { return $out($request, $response); } diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index 4d908635..ba228bf1 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -13,13 +13,12 @@ return [ 'priority' => 5, ], - 'rest-error' => [ + 'rest-not-found' => [ 'path' => '/rest', 'middleware' => [ - Middleware\Error\ResponseTypeMiddleware::class, + Middleware\NotFoundMiddleware::class, ], - 'error' => true, - 'priority' => -10000, + 'priority' => -1, ], ], ]; diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 24226840..528e3011 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -23,7 +23,7 @@ return [ 'allowed_methods' => ['GET', 'OPTIONS'], ], [ - 'name' => 'rest-lActionist-shortened-url', + 'name' => 'rest-list-shortened-url', 'path' => '/rest/short-codes', 'middleware' => Action\ListShortcodesMiddleware::class, 'allowed_methods' => ['GET'], diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index 44ce97f0..05a1afbe 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,8 +19,8 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, - - Middleware\Error\ResponseTypeMiddleware::class => AnnotatedFactory::class, + Middleware\ResponseTypeMiddleware::class => AnnotatedFactory::class, + Middleware\NotFoundMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index f05c1b78..1ad53c4b 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -69,7 +69,9 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface // If current route is the authenticate route or an OPTIONS request, continue to the next middleware /** @var RouteResult $routeResult */ $routeResult = $request->getAttribute(RouteResult::class); - if ((isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') + if (! isset($routeResult) + || $routeResult->isFailure() + || $routeResult->getMatchedRouteName() === 'rest-authenticate' || $request->getMethod() === 'OPTIONS' ) { return $out($request, $response); diff --git a/module/Rest/src/Middleware/NotFoundMiddleware.php b/module/Rest/src/Middleware/NotFoundMiddleware.php new file mode 100644 index 00000000..720f39ca --- /dev/null +++ b/module/Rest/src/Middleware/NotFoundMiddleware.php @@ -0,0 +1,62 @@ +translator = $translator; + } + + /** + * Process an incoming request and/or response. + * + * Accepts a server-side request and a response instance, and does + * something with them. + * + * If the response is not complete and/or further processing would not + * interfere with the work done in the middleware, or if the middleware + * wants to delegate to another process, it can use the `$out` callable + * if present. + * + * If the middleware does not return a value, execution of the current + * request is considered complete, and the response instance provided will + * be considered the response to return. + * + * Alternately, the middleware may return a response instance. + * + * Often, middleware will `return $out();`, with the assumption that a + * later middleware will return a response. + * + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + return new JsonResponse([ + 'error' => RestUtils::NOT_FOUND_ERROR, + 'message' => $this->translator->translate('Requested route does not exist.'), + ], 404); + } +} diff --git a/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php b/module/Rest/src/Middleware/ResponseTypeMiddleware.php similarity index 77% rename from module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php rename to module/Rest/src/Middleware/ResponseTypeMiddleware.php index c6319caa..e32bef12 100644 --- a/module/Rest/src/Middleware/Error/ResponseTypeMiddleware.php +++ b/module/Rest/src/Middleware/ResponseTypeMiddleware.php @@ -1,15 +1,16 @@ getHeader('Accept'); if (! empty(array_intersect(['application/json', 'text/json', 'application/x-json'], $accept))) { - $status = $this->determineStatus($response); + $status = $this->determineStatus($request, $response); $errorData = $this->determineErrorCode($request, $status); return new JsonResponse($errorData, $status); } - return $out($request, $response, $error); + return $out($request, $response); } /** + * @param Request $request * @param Response $response * @return int */ - protected function determineStatus(Response $response) + protected function determineStatus(Request $request, Response $response) { + /** @var RouteResult $routeResult */ + $routeResult = $request->getAttribute(RouteResult::class); + if ($routeResult->isFailure()) { + return 404; + } + $status = $response->getStatusCode(); return $status >= 400 ? $status : 500; } From f3d2cf5e153024e452ea2cf4ae873e28f5583ca6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Jul 2016 19:10:43 +0200 Subject: [PATCH 44/86] Deleted ResponseTypeMiddleware which is not ussable anymore --- module/Rest/config/services.config.php | 1 - .../src/Middleware/ResponseTypeMiddleware.php | 99 ------------------- 2 files changed, 100 deletions(-) delete mode 100644 module/Rest/src/Middleware/ResponseTypeMiddleware.php diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index 05a1afbe..b2c77bc7 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,7 +19,6 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, - Middleware\ResponseTypeMiddleware::class => AnnotatedFactory::class, Middleware\NotFoundMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/src/Middleware/ResponseTypeMiddleware.php b/module/Rest/src/Middleware/ResponseTypeMiddleware.php deleted file mode 100644 index e32bef12..00000000 --- a/module/Rest/src/Middleware/ResponseTypeMiddleware.php +++ /dev/null @@ -1,99 +0,0 @@ -translator = $translator; - } - - /** - * Process an incoming error, along with associated request and response. - * - * Accepts an error, a server-side request, and a response instance, and - * does something with them; if further processing can be done, it can - * delegate to `$out`. - * - * @see MiddlewareInterface - * @param Request $request - * @param Response $response - * @param null|callable $out - * @return null|Response - */ - public function __invoke(Request $request, Response $response, callable $out = null) - { - $accept = $request->getHeader('Accept'); - if (! empty(array_intersect(['application/json', 'text/json', 'application/x-json'], $accept))) { - $status = $this->determineStatus($request, $response); - $errorData = $this->determineErrorCode($request, $status); - - return new JsonResponse($errorData, $status); - } - - return $out($request, $response); - } - - /** - * @param Request $request - * @param Response $response - * @return int - */ - protected function determineStatus(Request $request, Response $response) - { - /** @var RouteResult $routeResult */ - $routeResult = $request->getAttribute(RouteResult::class); - if ($routeResult->isFailure()) { - return 404; - } - - $status = $response->getStatusCode(); - return $status >= 400 ? $status : 500; - } - - /** - * @param Request $request - * @param int $status - * @return string - */ - protected function determineErrorCode(Request $request, $status) - { - $errorData = $request->getAttribute('errorData'); - if (isset($errorData)) { - return $errorData; - } - - switch ($status) { - case 404: - return [ - 'error' => RestUtils::NOT_FOUND_ERROR, - 'message' => $this->translator->translate('Requested route does not exist'), - ]; - default: - return [ - 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => $this->translator->translate('Unknown error occured'), - ]; - } - } -} From 75e744838c936b9e1e799b270916a22837cf3925 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 27 Jul 2016 20:17:23 +0200 Subject: [PATCH 45/86] Created content based error handler which allows managing errors in a different way depending on the Accepted content type from the client --- .travis.yml | 4 ++ config/autoload/database.global.php | 2 +- config/autoload/errorhandler.local.php.dist | 14 +++- config/autoload/services.global.php | 4 +- module/Common/config/error-handler.config.php | 22 ++++++ .../Expressive/ContentBasedErrorHandler.php | 64 ++++++++++++++++++ .../ContentBasedErrorHandlerFactory.php | 30 ++++++++ .../src/Expressive/ErrorHandlerInterface.php | 18 +++++ module/Rest/config/error-handler.config.php | 18 +++++ module/Rest/lang/es.mo | Bin 1754 -> 1897 bytes module/Rest/lang/es.po | 15 ++-- .../Rest/src/Expressive/JsonErrorHandler.php | 39 +++++++++++ 12 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 module/Common/config/error-handler.config.php create mode 100644 module/Common/src/Expressive/ContentBasedErrorHandler.php create mode 100644 module/Common/src/Expressive/ContentBasedErrorHandlerFactory.php create mode 100644 module/Common/src/Expressive/ErrorHandlerInterface.php create mode 100644 module/Rest/config/error-handler.config.php create mode 100644 module/Rest/src/Expressive/JsonErrorHandler.php diff --git a/.travis.yml b/.travis.yml index 905480ea..abc18f1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,10 @@ php: - 7.1 - hhvm +matrix: + allow_failures: + - hhvm + before_script: - composer self-update - composer install --no-interaction diff --git a/config/autoload/database.global.php b/config/autoload/database.global.php index 4e3b00e3..62733f22 100644 --- a/config/autoload/database.global.php +++ b/config/autoload/database.global.php @@ -5,7 +5,7 @@ return [ 'driver' => 'pdo_mysql', 'user' => getenv('DB_USER'), 'password' => getenv('DB_PASSWORD'), - 'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener', + 'dbname' => getenv('DB_NAME') ?: 'shlink', 'charset' => 'utf8', 'driverOptions' => [ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index 92a92497..d6de56d7 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,14 +1,14 @@ [ 'invokables' => [ 'Zend\Expressive\Whoops' => Whoops\Run::class, 'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class, ], - 'factories' => [ - 'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class, - ], ], 'whoops' => [ @@ -18,4 +18,12 @@ return [ 'ajax_only' => true, ], ], + + 'error_handler' => [ + 'plugins' => [ + 'factories' => [ + ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class, + ], + ], + ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index dc99033c..d7f56ed6 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,4 +1,6 @@ InvokableFactory::class, // View - 'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class, + 'Zend\Expressive\FinalHandler' => ContentBasedErrorHandlerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ diff --git a/module/Common/config/error-handler.config.php b/module/Common/config/error-handler.config.php new file mode 100644 index 00000000..a6165fa2 --- /dev/null +++ b/module/Common/config/error-handler.config.php @@ -0,0 +1,22 @@ + [ + 'plugins' => [ + 'invokables' => [ + 'text/plain' => FinalHandler::class, + ], + 'factories' => [ + ContentBasedErrorHandler::DEFAULT_CONTENT => TemplatedErrorHandlerFactory::class, + ], + 'aliases' => [ + 'application/xhtml+xml' => ContentBasedErrorHandler::DEFAULT_CONTENT, + ], + ], + ], + +]; diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php new file mode 100644 index 00000000..f0d4bc04 --- /dev/null +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -0,0 +1,64 @@ +resolveErrorHandlerFromAcceptHeader($request); + return $errorHandler($request, $response, $err); + } + + /** + * Tries to resolve + * + * @param Request $request + * @return callable + */ + protected function resolveErrorHandlerFromAcceptHeader(Request $request) + { + $accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT; + $accepts = explode(',', $accepts); + foreach ($accepts as $accept) { + if (! $this->has($accept)) { + continue; + } + + return $this->get($accept); + } + + throw new InvalidArgumentException(sprintf( + 'It wasn\'t possible to find an error handler for ' + )); + } +} diff --git a/module/Common/src/Expressive/ContentBasedErrorHandlerFactory.php b/module/Common/src/Expressive/ContentBasedErrorHandlerFactory.php new file mode 100644 index 00000000..f262d320 --- /dev/null +++ b/module/Common/src/Expressive/ContentBasedErrorHandlerFactory.php @@ -0,0 +1,30 @@ +get('config')['error_handler']; + $plugins = isset($config['plugins']) ? $config['plugins'] : []; + return new ContentBasedErrorHandler($container, $plugins); + } +} diff --git a/module/Common/src/Expressive/ErrorHandlerInterface.php b/module/Common/src/Expressive/ErrorHandlerInterface.php new file mode 100644 index 00000000..0c787a09 --- /dev/null +++ b/module/Common/src/Expressive/ErrorHandlerInterface.php @@ -0,0 +1,18 @@ + [ + 'plugins' => [ + 'invokables' => [ + 'application/json' => JsonErrorHandler::class, + ], + 'aliases' => [ + 'application/x-json' => 'application/json', + 'text/json' => 'application/json', + ], + ], + ], + +]; diff --git a/module/Rest/lang/es.mo b/module/Rest/lang/es.mo index 2a8d4431a081c2eee3abe6973ebe7cb2bac0ce20..1bfa9aefbc137b30b9e4597348190e0ec80aacf7 100644 GIT binary patch delta 445 zcmX}nJxjwt7zgkt>6=y*;~)yHIdKuhG_=%~B4X(zln%vJ@MvzRq&ZA3pdu6}72Jv+ zz>gpu#Sb7jJ2<#GI4DjIPX3eDKHQ(*CHKN}ud^R>{kMhWEum(R9HI~dc}Aj|*N7IO z0U2C}H*gP{@C8=j5451A6Rp7m7_DExWq1dR@Btdg7qUXsCrXVDnD7xB8gL70X`&Xa z!aBT!HhhA4_zR zUxrFBM~Z-XQZaGT4U{!F)UPuYW>;(F(%@TPZH?y}cG0%8O&+MteV==Q%a!zNtnP{< z?m50>4dL=r&bImgpWVr<6E9gE+1nWw5}%rOHq51ZTE5AdA1clQ>2^C^#T|}DMtult IaFuTV0Rh5L(f|Me delta 326 zcmaFKcZ;|Fo)F7a1|Z-9Vi_RL0b*Vt-UGxS@BxU$fcPU2D*!PEBLhPZkk$m!@<4hU zkTwU>*MPJhkmh4zU{D6qwm@1ENGAemBOqN3q#pv+tOL?=K>h?4u)2DN^*{#5vdci) z3?#wIz~Bd@j{|8(AT7qmz+eERBY-r>ft^6w7)UPz(m?YVn1PrBh=JO{0BAk~P%GF& z><}RaPACo30yI=1G{{GxJh50IFTZ579Ah%$&_QlMR>^<-lwc18oB% n0|PFf#Nra&kfOxA;+({i{30ub&GVVxGH&i=&1IY{!5#\n" "Language-Team: \n" "Language: es_ES\n" "MIME-Version: 1.0\n" @@ -52,9 +52,12 @@ msgid "" "Missing or invalid auth token provided. Perform a new authentication request " "and send provided token on every new request on the \"%s\" header" msgstr "" -"No se ha proporcionado token de autenticación o este es inválido. Realia una " -"nueva petición de autenticación y envía el token proporcionado en cada nueva " -"petición en la cabecera \"%s\"" +"No se ha proporcionado token de autenticación o este es inválido. Realiza " +"una nueva petición de autenticación y envía el token proporcionado en cada " +"nueva petición en la cabecera \"%s\"" + +msgid "Requested route does not exist." +msgstr "La ruta solicitada no existe." #~ msgid "RestToken not found for token \"%s\"" #~ msgstr "No se ha encontrado un RestToken para el token \"%s\"" diff --git a/module/Rest/src/Expressive/JsonErrorHandler.php b/module/Rest/src/Expressive/JsonErrorHandler.php new file mode 100644 index 00000000..0d02c45b --- /dev/null +++ b/module/Rest/src/Expressive/JsonErrorHandler.php @@ -0,0 +1,39 @@ +getStatusCode(); + $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase(); + $status = $status < 400 ? 500 : $status; + + return new JsonResponse([ + 'error' => $this->responsePhraseToCode($responsePhrase), + 'message' => $responsePhrase, + ], $status); + } + + /** + * @param string $responsePhrase + * @return string + */ + protected function responsePhraseToCode($responsePhrase) + { + return strtoupper(str_replace(' ', '_', $responsePhrase)); + } +} From 36259588dbb1a01cf5a0721c68677faed007383b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 27 Jul 2016 20:22:50 +0200 Subject: [PATCH 46/86] Fixed Action prefix on routable middlewares --- .travis.yml | 4 ---- module/Core/config/routes.config.php | 4 ++-- module/Core/config/services.config.php | 4 ++-- .../{RedirectMiddleware.php => RedirectAction.php} | 2 +- module/Rest/config/routes.config.php | 10 +++++----- module/Rest/config/services.config.php | 10 +++++----- ...stractRestMiddleware.php => AbstractRestAction.php} | 2 +- ...thenticateMiddleware.php => AuthenticateAction.php} | 4 ++-- ...ortcodeMiddleware.php => CreateShortcodeAction.php} | 2 +- .../{GetVisitsMiddleware.php => GetVisitsAction.php} | 4 ++-- ...ortcodesMiddleware.php => ListShortcodesAction.php} | 4 ++-- .../{ResolveUrlMiddleware.php => ResolveUrlAction.php} | 4 ++-- 12 files changed, 25 insertions(+), 29 deletions(-) rename module/Core/src/Action/{RedirectMiddleware.php => RedirectAction.php} (98%) rename module/Rest/src/Action/{AbstractRestMiddleware.php => AbstractRestAction.php} (96%) rename module/Rest/src/Action/{AuthenticateMiddleware.php => AuthenticateAction.php} (95%) rename module/Rest/src/Action/{CreateShortcodeMiddleware.php => CreateShortcodeAction.php} (97%) rename module/Rest/src/Action/{GetVisitsMiddleware.php => GetVisitsAction.php} (96%) rename module/Rest/src/Action/{ListShortcodesMiddleware.php => ListShortcodesAction.php} (94%) rename module/Rest/src/Action/{ResolveUrlMiddleware.php => ResolveUrlAction.php} (96%) diff --git a/.travis.yml b/.travis.yml index abc18f1c..905480ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,6 @@ php: - 7.1 - hhvm -matrix: - allow_failures: - - hhvm - before_script: - composer self-update - composer install --no-interaction diff --git a/module/Core/config/routes.config.php b/module/Core/config/routes.config.php index 8e9ac66d..4d9a85e5 100644 --- a/module/Core/config/routes.config.php +++ b/module/Core/config/routes.config.php @@ -1,5 +1,5 @@ 'long-url-redirect', 'path' => '/{shortCode}', - 'middleware' => RedirectMiddleware::class, + 'middleware' => RedirectAction::class, 'allowed_methods' => ['GET'], ], ], diff --git a/module/Core/config/services.config.php b/module/Core/config/services.config.php index 00de6a67..7d044875 100644 --- a/module/Core/config/services.config.php +++ b/module/Core/config/services.config.php @@ -1,6 +1,6 @@ AnnotatedFactory::class, // Middleware - RedirectMiddleware::class => AnnotatedFactory::class, + RedirectAction::class => AnnotatedFactory::class, ], ], diff --git a/module/Core/src/Action/RedirectMiddleware.php b/module/Core/src/Action/RedirectAction.php similarity index 98% rename from module/Core/src/Action/RedirectMiddleware.php rename to module/Core/src/Action/RedirectAction.php index 1afd2d01..8f7e7042 100644 --- a/module/Core/src/Action/RedirectMiddleware.php +++ b/module/Core/src/Action/RedirectAction.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Zend\Diactoros\Response\RedirectResponse; use Zend\Stratigility\MiddlewareInterface; -class RedirectMiddleware implements MiddlewareInterface +class RedirectAction implements MiddlewareInterface { /** * @var UrlShortenerInterface diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 528e3011..4cc0f510 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -7,31 +7,31 @@ return [ [ 'name' => 'rest-authenticate', 'path' => '/rest/authenticate', - 'middleware' => Action\AuthenticateMiddleware::class, + 'middleware' => Action\AuthenticateAction::class, 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-create-shortcode', 'path' => '/rest/short-codes', - 'middleware' => Action\CreateShortcodeMiddleware::class, + 'middleware' => Action\CreateShortcodeAction::class, 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-resolve-url', 'path' => '/rest/short-codes/{shortCode}', - 'middleware' => Action\ResolveUrlMiddleware::class, + 'middleware' => Action\ResolveUrlAction::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], [ 'name' => 'rest-list-shortened-url', 'path' => '/rest/short-codes', - 'middleware' => Action\ListShortcodesMiddleware::class, + 'middleware' => Action\ListShortcodesAction::class, 'allowed_methods' => ['GET'], ], [ 'name' => 'rest-get-visits', 'path' => '/rest/short-codes/{shortCode}/visits', - 'middleware' => Action\GetVisitsMiddleware::class, + 'middleware' => Action\GetVisitsAction::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], ], diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index b2c77bc7..926368c3 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -11,11 +11,11 @@ return [ 'factories' => [ Service\RestTokenService::class => AnnotatedFactory::class, - Action\AuthenticateMiddleware::class => AnnotatedFactory::class, - Action\CreateShortcodeMiddleware::class => AnnotatedFactory::class, - Action\ResolveUrlMiddleware::class => AnnotatedFactory::class, - Action\GetVisitsMiddleware::class => AnnotatedFactory::class, - Action\ListShortcodesMiddleware::class => AnnotatedFactory::class, + Action\AuthenticateAction::class => AnnotatedFactory::class, + Action\CreateShortcodeAction::class => AnnotatedFactory::class, + Action\ResolveUrlAction::class => AnnotatedFactory::class, + Action\GetVisitsAction::class => AnnotatedFactory::class, + Action\ListShortcodesAction::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, diff --git a/module/Rest/src/Action/AbstractRestMiddleware.php b/module/Rest/src/Action/AbstractRestAction.php similarity index 96% rename from module/Rest/src/Action/AbstractRestMiddleware.php rename to module/Rest/src/Action/AbstractRestAction.php index a273d248..587e1a93 100644 --- a/module/Rest/src/Action/AbstractRestMiddleware.php +++ b/module/Rest/src/Action/AbstractRestAction.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Stratigility\MiddlewareInterface; -abstract class AbstractRestMiddleware implements MiddlewareInterface +abstract class AbstractRestAction implements MiddlewareInterface { /** * Process an incoming request and/or response. diff --git a/module/Rest/src/Action/AuthenticateMiddleware.php b/module/Rest/src/Action/AuthenticateAction.php similarity index 95% rename from module/Rest/src/Action/AuthenticateMiddleware.php rename to module/Rest/src/Action/AuthenticateAction.php index 1a37ea90..7d564e4f 100644 --- a/module/Rest/src/Action/AuthenticateMiddleware.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\I18n\Translator\TranslatorInterface; -class AuthenticateMiddleware extends AbstractRestMiddleware +class AuthenticateAction extends AbstractRestAction { /** * @var RestTokenServiceInterface @@ -23,7 +23,7 @@ class AuthenticateMiddleware extends AbstractRestMiddleware private $translator; /** - * AuthenticateMiddleware constructor. + * AuthenticateAction constructor. * @param RestTokenServiceInterface|RestTokenService $restTokenService * @param TranslatorInterface $translator * diff --git a/module/Rest/src/Action/CreateShortcodeMiddleware.php b/module/Rest/src/Action/CreateShortcodeAction.php similarity index 97% rename from module/Rest/src/Action/CreateShortcodeMiddleware.php rename to module/Rest/src/Action/CreateShortcodeAction.php index fe3074fa..29aa1108 100644 --- a/module/Rest/src/Action/CreateShortcodeMiddleware.php +++ b/module/Rest/src/Action/CreateShortcodeAction.php @@ -12,7 +12,7 @@ use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Uri; use Zend\I18n\Translator\TranslatorInterface; -class CreateShortcodeMiddleware extends AbstractRestMiddleware +class CreateShortcodeAction extends AbstractRestAction { /** * @var UrlShortener|UrlShortenerInterface diff --git a/module/Rest/src/Action/GetVisitsMiddleware.php b/module/Rest/src/Action/GetVisitsAction.php similarity index 96% rename from module/Rest/src/Action/GetVisitsMiddleware.php rename to module/Rest/src/Action/GetVisitsAction.php index 75765389..bd78adbb 100644 --- a/module/Rest/src/Action/GetVisitsMiddleware.php +++ b/module/Rest/src/Action/GetVisitsAction.php @@ -12,7 +12,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\I18n\Translator\TranslatorInterface; -class GetVisitsMiddleware extends AbstractRestMiddleware +class GetVisitsAction extends AbstractRestAction { /** * @var VisitsTrackerInterface @@ -24,7 +24,7 @@ class GetVisitsMiddleware extends AbstractRestMiddleware private $translator; /** - * GetVisitsMiddleware constructor. + * GetVisitsAction constructor. * @param VisitsTrackerInterface|VisitsTracker $visitsTracker * @param TranslatorInterface $translator * diff --git a/module/Rest/src/Action/ListShortcodesMiddleware.php b/module/Rest/src/Action/ListShortcodesAction.php similarity index 94% rename from module/Rest/src/Action/ListShortcodesMiddleware.php rename to module/Rest/src/Action/ListShortcodesAction.php index 99b1c718..3d0d9613 100644 --- a/module/Rest/src/Action/ListShortcodesMiddleware.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\I18n\Translator\TranslatorInterface; -class ListShortcodesMiddleware extends AbstractRestMiddleware +class ListShortcodesAction extends AbstractRestAction { use PaginatorUtilsTrait; @@ -25,7 +25,7 @@ class ListShortcodesMiddleware extends AbstractRestMiddleware private $translator; /** - * ListShortcodesMiddleware constructor. + * ListShortcodesAction constructor. * @param ShortUrlServiceInterface|ShortUrlService $shortUrlService * @param TranslatorInterface $translator * diff --git a/module/Rest/src/Action/ResolveUrlMiddleware.php b/module/Rest/src/Action/ResolveUrlAction.php similarity index 96% rename from module/Rest/src/Action/ResolveUrlMiddleware.php rename to module/Rest/src/Action/ResolveUrlAction.php index db7e5827..c1783834 100644 --- a/module/Rest/src/Action/ResolveUrlMiddleware.php +++ b/module/Rest/src/Action/ResolveUrlAction.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\I18n\Translator\TranslatorInterface; -class ResolveUrlMiddleware extends AbstractRestMiddleware +class ResolveUrlAction extends AbstractRestAction { /** * @var UrlShortenerInterface @@ -23,7 +23,7 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware private $translator; /** - * ResolveUrlMiddleware constructor. + * ResolveUrlAction constructor. * @param UrlShortenerInterface|UrlShortener $urlShortener * @param TranslatorInterface $translator * From af9193f72118fd4676d7a27cedce897b24dc0668 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 28 Jul 2016 09:40:36 +0200 Subject: [PATCH 47/86] Removed duplicated error handling for 404 errors --- .../Expressive/ContentBasedErrorHandler.php | 2 +- .../src/Expressive/ErrorHandlerInterface.php | 2 +- module/Core/src/Action/RedirectAction.php | 2 +- .../Core/templates/core/error/error.html.twig | 4 +- .../config/middleware-pipeline.config.php | 8 --- module/Rest/config/services.config.php | 1 - .../Rest/src/Expressive/JsonErrorHandler.php | 16 +++-- .../src/Middleware/NotFoundMiddleware.php | 62 ------------------- 8 files changed, 18 insertions(+), 79 deletions(-) delete mode 100644 module/Rest/src/Middleware/NotFoundMiddleware.php diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index f0d4bc04..60e1dfdb 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -1,7 +1,7 @@ withStatus(404), 'Not found'); } // Track visit to this shortcode diff --git a/module/Core/templates/core/error/error.html.twig b/module/Core/templates/core/error/error.html.twig index 7426a33a..9821ec0e 100644 --- a/module/Core/templates/core/error/error.html.twig +++ b/module/Core/templates/core/error/error.html.twig @@ -12,7 +12,9 @@ {% block content %}

{{ translate('Oops!') }}


-

{{ translate('We encountered a %s %s error.') | format(status, reason) }}

+ {% if status != 404 %} +

{{ translate('We encountered a %s %s error.') | format(status, reason) }}

+ {% endif %} {% if status == 404 %}

{{ translate('This short URL doesn\'t seem to be valid.') }}

{{ translate('Make sure you included all the characters, with no extra punctuation.') }}

diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index ba228bf1..78c20c38 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -12,13 +12,5 @@ return [ ], 'priority' => 5, ], - - 'rest-not-found' => [ - 'path' => '/rest', - 'middleware' => [ - Middleware\NotFoundMiddleware::class, - ], - 'priority' => -1, - ], ], ]; diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index 926368c3..551c4c96 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -19,7 +19,6 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, - Middleware\NotFoundMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/src/Expressive/JsonErrorHandler.php b/module/Rest/src/Expressive/JsonErrorHandler.php index 0d02c45b..9294ec24 100644 --- a/module/Rest/src/Expressive/JsonErrorHandler.php +++ b/module/Rest/src/Expressive/JsonErrorHandler.php @@ -1,10 +1,11 @@ getStatusCode(); - $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase(); - $status = $status < 400 ? 500 : $status; + $hasRoute = $request->getAttribute(RouteResult::class) !== null; + $isNotFound = ! $hasRoute && ! isset($err); + if ($isNotFound) { + $responsePhrase = 'Not found'; + $status = 404; + } else { + $status = $response->getStatusCode(); + $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase(); + $status = $status < 400 ? 500 : $status; + } return new JsonResponse([ 'error' => $this->responsePhraseToCode($responsePhrase), diff --git a/module/Rest/src/Middleware/NotFoundMiddleware.php b/module/Rest/src/Middleware/NotFoundMiddleware.php deleted file mode 100644 index 720f39ca..00000000 --- a/module/Rest/src/Middleware/NotFoundMiddleware.php +++ /dev/null @@ -1,62 +0,0 @@ -translator = $translator; - } - - /** - * Process an incoming request and/or response. - * - * Accepts a server-side request and a response instance, and does - * something with them. - * - * If the response is not complete and/or further processing would not - * interfere with the work done in the middleware, or if the middleware - * wants to delegate to another process, it can use the `$out` callable - * if present. - * - * If the middleware does not return a value, execution of the current - * request is considered complete, and the response instance provided will - * be considered the response to return. - * - * Alternately, the middleware may return a response instance. - * - * Often, middleware will `return $out();`, with the assumption that a - * later middleware will return a response. - * - * @param Request $request - * @param Response $response - * @param null|callable $out - * @return null|Response - */ - public function __invoke(Request $request, Response $response, callable $out = null) - { - return new JsonResponse([ - 'error' => RestUtils::NOT_FOUND_ERROR, - 'message' => $this->translator->translate('Requested route does not exist.'), - ], 404); - } -} From f4532c3015b974f064b7be9368f89888fbe7df0d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 28 Jul 2016 09:56:44 +0200 Subject: [PATCH 48/86] Replaced exclusive ifs by if-else --- module/Core/templates/core/error/error.html.twig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/module/Core/templates/core/error/error.html.twig b/module/Core/templates/core/error/error.html.twig index 9821ec0e..5cb66c57 100644 --- a/module/Core/templates/core/error/error.html.twig +++ b/module/Core/templates/core/error/error.html.twig @@ -14,8 +14,7 @@
{% if status != 404 %}

{{ translate('We encountered a %s %s error.') | format(status, reason) }}

- {% endif %} - {% if status == 404 %} + {% else %}

{{ translate('This short URL doesn\'t seem to be valid.') }}

{{ translate('Make sure you included all the characters, with no extra punctuation.') }}

{% endif %} From ab6aa99a6dcc40dbc83f2b05d30d08d1cfe2d763 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 28 Jul 2016 20:49:27 +0200 Subject: [PATCH 49/86] Created more tests on CLI module --- .../Command/ListShortcodesCommandTest.php | 119 ++++++++++++++++++ module/CLI/test/ConfigProviderTest.php | 30 +++++ 2 files changed, 149 insertions(+) create mode 100644 module/CLI/test/Command/ListShortcodesCommandTest.php create mode 100644 module/CLI/test/ConfigProviderTest.php diff --git a/module/CLI/test/Command/ListShortcodesCommandTest.php b/module/CLI/test/Command/ListShortcodesCommandTest.php new file mode 100644 index 00000000..7ec0e5af --- /dev/null +++ b/module/CLI/test/Command/ListShortcodesCommandTest.php @@ -0,0 +1,119 @@ +shortUrlService = $this->prophesize(ShortUrlServiceInterface::class); + $app = new Application(); + $command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([])); + $app->add($command); + + $this->questionHelper = $command->getHelper('question'); + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function noInputCallsListJustOnce() + { + $this->questionHelper->setInputStream($this->getInputStream('\n')); + $this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute(['command' => 'shortcode:list']); + } + + /** + * @test + */ + public function loadingMorePagesCallsListMoreTimes() + { + // The paginator will return more than one page for the first 3 times + $data = []; + for ($i = 0; $i < 30; $i++) { + $data[] = new ShortUrl(); + } + $data = array_chunk($data, 11); + + $questionHelper = $this->questionHelper; + $that = $this; + $this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) { + $questionHelper->setInputStream($that->getInputStream('y')); + return new Paginator(new ArrayAdapter(array_shift($data))); + })->shouldBeCalledTimes(3); + + $this->commandTester->execute(['command' => 'shortcode:list']); + } + + /** + * @test + */ + public function havingMorePagesButAnsweringNoCallsListJustOnce() + { + // The paginator will return more than one page + $data = []; + for ($i = 0; $i < 30; $i++) { + $data[] = new ShortUrl(); + } + + $this->questionHelper->setInputStream($this->getInputStream('n')); + $this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data))) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute(['command' => 'shortcode:list']); + } + + /** + * @test + */ + public function passingPageWillMakeListStartOnThatPage() + { + $page = 5; + $this->questionHelper->setInputStream($this->getInputStream('\n')); + $this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:list', + '--page' => $page, + ]); + } + + protected function getInputStream($inputData) + { + $stream = fopen('php://memory', 'r+', false); + fputs($stream, $inputData); + rewind($stream); + + return $stream; + } +} diff --git a/module/CLI/test/ConfigProviderTest.php b/module/CLI/test/ConfigProviderTest.php new file mode 100644 index 00000000..8d368561 --- /dev/null +++ b/module/CLI/test/ConfigProviderTest.php @@ -0,0 +1,30 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function confiIsProperlyReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('cli', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('translator', $config); + } +} From c569cef23912189bf1513f0ad55f83b77c20477b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 29 Jul 2016 11:25:53 +0200 Subject: [PATCH 50/86] Fixed ContentBased error handler not using the default content if accepted contents are not valid --- .../src/Expressive/ContentBasedErrorHandler.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index 60e1dfdb..7dbe2509 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -47,6 +47,7 @@ class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHan */ protected function resolveErrorHandlerFromAcceptHeader(Request $request) { + // Try to find an error handler for one of the accepted content types $accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT; $accepts = explode(',', $accepts); foreach ($accepts as $accept) { @@ -57,8 +58,17 @@ class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHan return $this->get($accept); } + // If it wasn't possible to find an error handler for accepted content type, use default one if registered + if ($this->has(self::DEFAULT_CONTENT)) { + return $this->get(self::DEFAULT_CONTENT); + } + + // It wasn't possible to find an error handler throw new InvalidArgumentException(sprintf( - 'It wasn\'t possible to find an error handler for ' + 'It wasn\'t possible to find an error handler for ["%s"] content types. ' + . 'Make sure you have registered at least the default "%s" content type', + implode('", "', $accepts), + self::DEFAULT_CONTENT )); } } From 2f5119d0b3298ef2a1bb14fce99320512929a218 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 10:08:21 +0200 Subject: [PATCH 51/86] Split ContentBasedErrorHandler responsibilities into two separated classes --- config/autoload/services.global.php | 10 ++++-- .../Expressive/ContentBasedErrorHandler.php | 34 ++++++++++--------- .../src/Expressive/ErrorHandlerManager.php | 21 ++++++++++++ ...ory.php => ErrorHandlerManagerFactory.php} | 4 +-- .../ErrorHandlerManagerInterface.php | 9 +++++ 5 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 module/Common/src/Expressive/ErrorHandlerManager.php rename module/Common/src/Expressive/{ContentBasedErrorHandlerFactory.php => ErrorHandlerManagerFactory.php} (88%) create mode 100644 module/Common/src/Expressive/ErrorHandlerManagerInterface.php diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index d7f56ed6..841a389a 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,6 +1,8 @@ InvokableFactory::class, // View - 'Zend\Expressive\FinalHandler' => ContentBasedErrorHandlerFactory::class, + ContentBasedErrorHandler::class => AnnotatedFactory::class, + ErrorHandlerManager::class => ErrorHandlerManagerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ Router\RouterInterface::class => Router\FastRouteRouter::class, + 'Zend\Expressive\FinalHandler' => ContentBasedErrorHandler::class, ], ], diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index 60e1dfdb..df6cde91 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -1,27 +1,29 @@ errorHandlerManager = $errorHandlerManager; } /** @@ -50,11 +52,11 @@ class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHan $accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT; $accepts = explode(',', $accepts); foreach ($accepts as $accept) { - if (! $this->has($accept)) { + if (! $this->errorHandlerManager->has($accept)) { continue; } - return $this->get($accept); + return $this->errorHandlerManager->get($accept); } throw new InvalidArgumentException(sprintf( diff --git a/module/Common/src/Expressive/ErrorHandlerManager.php b/module/Common/src/Expressive/ErrorHandlerManager.php new file mode 100644 index 00000000..5cce0095 --- /dev/null +++ b/module/Common/src/Expressive/ErrorHandlerManager.php @@ -0,0 +1,21 @@ +get('config')['error_handler']; $plugins = isset($config['plugins']) ? $config['plugins'] : []; - return new ContentBasedErrorHandler($container, $plugins); + return new ErrorHandlerManager($container, $plugins); } } diff --git a/module/Common/src/Expressive/ErrorHandlerManagerInterface.php b/module/Common/src/Expressive/ErrorHandlerManagerInterface.php new file mode 100644 index 00000000..371cbbf4 --- /dev/null +++ b/module/Common/src/Expressive/ErrorHandlerManagerInterface.php @@ -0,0 +1,9 @@ + Date: Sat, 30 Jul 2016 10:47:29 +0200 Subject: [PATCH 52/86] Fixed ContentBasedErrorHandler fatching error handlers from the composed plugin manager --- module/Common/src/Expressive/ContentBasedErrorHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index dbd3d900..6f094f69 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -61,8 +61,8 @@ class ContentBasedErrorHandler implements ErrorHandlerInterface } // If it wasn't possible to find an error handler for accepted content type, use default one if registered - if ($this->has(self::DEFAULT_CONTENT)) { - return $this->get(self::DEFAULT_CONTENT); + if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) { + return $this->errorHandlerManager->get(self::DEFAULT_CONTENT); } // It wasn't possible to find an error handler From e345c2bbfe3410ad1660aa2ed30a9f348219a9bd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 13:51:52 +0200 Subject: [PATCH 53/86] Moved error handler classes from Expressive namespace to ErrorHandler namespace --- config/autoload/errorhandler.local.php.dist | 2 +- config/autoload/services.global.php | 7 +------ module/Common/config/error-handler.config.php | 2 +- module/Common/config/services.config.php | 4 ++++ .../ContentBasedErrorHandler.php | 2 +- .../{Expressive => ErrorHandler}/ErrorHandlerInterface.php | 2 +- .../{Expressive => ErrorHandler}/ErrorHandlerManager.php | 2 +- .../ErrorHandlerManagerFactory.php | 2 +- .../ErrorHandlerManagerInterface.php | 2 +- module/Rest/config/error-handler.config.php | 2 +- .../src/{Expressive => ErrorHandler}/JsonErrorHandler.php | 4 ++-- 11 files changed, 15 insertions(+), 16 deletions(-) rename module/Common/src/{Expressive => ErrorHandler}/ContentBasedErrorHandler.php (98%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerInterface.php (89%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManager.php (92%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManagerFactory.php (95%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManagerInterface.php (72%) rename module/Rest/src/{Expressive => ErrorHandler}/JsonErrorHandler.php (92%) diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index d6de56d7..c65493f2 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,6 +1,6 @@ InvokableFactory::class, // View - ContentBasedErrorHandler::class => AnnotatedFactory::class, - ErrorHandlerManager::class => ErrorHandlerManagerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ diff --git a/module/Common/config/error-handler.config.php b/module/Common/config/error-handler.config.php index a6165fa2..d19b9ac6 100644 --- a/module/Common/config/error-handler.config.php +++ b/module/Common/config/error-handler.config.php @@ -1,5 +1,5 @@ TranslatorFactory::class, TranslatorExtension::class => AnnotatedFactory::class, LocaleMiddleware::class => AnnotatedFactory::class, + + ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class, + ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/ErrorHandler/ContentBasedErrorHandler.php similarity index 98% rename from module/Common/src/Expressive/ContentBasedErrorHandler.php rename to module/Common/src/ErrorHandler/ContentBasedErrorHandler.php index 6f094f69..be19e848 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/ErrorHandler/ContentBasedErrorHandler.php @@ -1,5 +1,5 @@ Date: Sat, 30 Jul 2016 13:54:00 +0200 Subject: [PATCH 54/86] Removed whiteline --- config/autoload/errorhandler.local.php.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index c65493f2..afbc52ed 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,5 +1,4 @@ Date: Sat, 30 Jul 2016 14:12:56 +0200 Subject: [PATCH 55/86] Created GenerateShortcodeCommandTest --- .../src/Command/GenerateShortcodeCommand.php | 6 +- .../Command/GenerateShortcodeCommandTest.php | 70 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 module/CLI/test/Command/GenerateShortcodeCommandTest.php diff --git a/module/CLI/src/Command/GenerateShortcodeCommand.php b/module/CLI/src/Command/GenerateShortcodeCommand.php index 0a0af9eb..f02c110b 100644 --- a/module/CLI/src/Command/GenerateShortcodeCommand.php +++ b/module/CLI/src/Command/GenerateShortcodeCommand.php @@ -52,7 +52,7 @@ class GenerateShortcodeCommand extends Command { $this->setName('shortcode:generate') ->setDescription( - $this->translator->translate('Generates a shortcode for provided URL and returns the short URL') + $this->translator->translate('Generates a short code for provided URL and returns the short URL') ) ->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse')); } @@ -87,8 +87,8 @@ class GenerateShortcodeCommand extends Command return; } - $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); - $shortUrl = (new Uri())->withPath($shortcode) + $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortCode) ->withScheme($this->domainConfig['schema']) ->withHost($this->domainConfig['hostname']); diff --git a/module/CLI/test/Command/GenerateShortcodeCommandTest.php b/module/CLI/test/Command/GenerateShortcodeCommandTest.php new file mode 100644 index 00000000..45cb8130 --- /dev/null +++ b/module/CLI/test/Command/GenerateShortcodeCommandTest.php @@ -0,0 +1,70 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [ + 'schema' => 'http', + 'hostname' => 'foo.com' + ]); + $app = new Application(); + $app->add($command); + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function properShortCodeIsCreatedIfLongUrlIsCorrect() + { + $this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123') + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:generate', + 'longUrl' => 'http://domain.com/foo/bar' + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0); + } + + /** + * @test + */ + public function exceptionWhileParsingLongUrlOutputsError() + { + $this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException()) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:generate', + 'longUrl' => 'http://domain.com/invalid' + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue( + strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0 + ); + } +} From 3923bf06046d7c9ce79d0857e0053300ad0d0aef Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 14:30:30 +0200 Subject: [PATCH 56/86] Created GetVisitsCommandTest --- .../CLI/test/Command/GetVisitsCommandTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 module/CLI/test/Command/GetVisitsCommandTest.php diff --git a/module/CLI/test/Command/GetVisitsCommandTest.php b/module/CLI/test/Command/GetVisitsCommandTest.php new file mode 100644 index 00000000..4294823b --- /dev/null +++ b/module/CLI/test/Command/GetVisitsCommandTest.php @@ -0,0 +1,91 @@ +visitsTracker = $this->prophesize(VisitsTrackerInterface::class); + $command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function noDateFlagsTriesToListWithoutDateRange() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([]) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + ]); + } + + /** + * @test + */ + public function providingDateFlagsTheListGetsFiltered() + { + $shortCode = 'abc123'; + $startDate = '2016-01-01'; + $endDate = '2016-02-01'; + $this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate))) + ->willReturn([]) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + '--startDate' => $startDate, + '--endDate' => $endDate, + ]); + } + + /** + * @test + */ + public function outputIsProperlyGenerated() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::any())->willReturn([ + (new Visit())->setReferer('foo') + ->setRemoteAddr('1.2.3.4') + ->setUserAgent('bar'), + ])->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'foo') > 0); + $this->assertTrue(strpos($output, '1.2.3.4') > 0); + $this->assertTrue(strpos($output, 'bar') > 0); + } +} From 50f15494572a1c185cb719479046bcfe67b38326 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 14:42:09 +0200 Subject: [PATCH 57/86] Created ProcessVisitsCommand --- .../test/Command/ProcessVisitsCommandTest.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 module/CLI/test/Command/ProcessVisitsCommandTest.php diff --git a/module/CLI/test/Command/ProcessVisitsCommandTest.php b/module/CLI/test/Command/ProcessVisitsCommandTest.php new file mode 100644 index 00000000..23463123 --- /dev/null +++ b/module/CLI/test/Command/ProcessVisitsCommandTest.php @@ -0,0 +1,96 @@ +visitService = $this->prophesize(VisitService::class); + $this->ipResolver = $this->prophesize(IpLocationResolver::class); + $command = new ProcessVisitsCommand( + $this->visitService->reveal(), + $this->ipResolver->reveal(), + Translator::factory([]) + ); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function allReturnedVisitsIpsAreProcessed() + { + $visits = [ + (new Visit())->setRemoteAddr('1.2.3.4'), + (new Visit())->setRemoteAddr('4.3.2.1'), + (new Visit())->setRemoteAddr('12.34.56.78'), + ]; + $this->visitService->getUnlocatedVisits()->willReturn($visits) + ->shouldBeCalledTimes(1); + + $this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits)); + $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]) + ->shouldBeCalledTimes(count($visits)); + + $this->commandTester->execute([ + 'command' => 'visit:process', + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'Processing IP 1.2.3.4') === 0); + $this->assertTrue(strpos($output, 'Processing IP 4.3.2.1') > 0); + $this->assertTrue(strpos($output, 'Processing IP 12.34.56.78') > 0); + } + + /** + * @test + */ + public function localhostAddressIsIgnored() + { + $visits = [ + (new Visit())->setRemoteAddr('1.2.3.4'), + (new Visit())->setRemoteAddr('4.3.2.1'), + (new Visit())->setRemoteAddr('12.34.56.78'), + (new Visit())->setRemoteAddr('127.0.0.1'), + (new Visit())->setRemoteAddr('127.0.0.1'), + ]; + $this->visitService->getUnlocatedVisits()->willReturn($visits) + ->shouldBeCalledTimes(1); + + $this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 2); + $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]) + ->shouldBeCalledTimes(count($visits) - 2); + + $this->commandTester->execute([ + 'command' => 'visit:process', + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'Ignored localhost address') > 0); + } +} From 4c6cc9cd11f65ec2dd4b63297b3dcc7fc4b5570f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 16:48:02 +0200 Subject: [PATCH 58/86] Created ResolveUrlCommandTest --- module/CLI/src/Factory/ApplicationFactory.php | 2 +- .../test/Command/ResolveUrlCommandTest.php | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 module/CLI/test/Command/ResolveUrlCommandTest.php diff --git a/module/CLI/src/Factory/ApplicationFactory.php b/module/CLI/src/Factory/ApplicationFactory.php index 51d5fdb3..a8e24bf3 100644 --- a/module/CLI/src/Factory/ApplicationFactory.php +++ b/module/CLI/src/Factory/ApplicationFactory.php @@ -25,7 +25,7 @@ class ApplicationFactory implements FactoryInterface public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { $config = $container->get('config')['cli']; - $app = new CliApp(); + $app = new CliApp('Shlink', '1.0.0'); $commands = isset($config['commands']) ? $config['commands'] : []; foreach ($commands as $command) { diff --git a/module/CLI/test/Command/ResolveUrlCommandTest.php b/module/CLI/test/Command/ResolveUrlCommandTest.php new file mode 100644 index 00000000..a2f47c92 --- /dev/null +++ b/module/CLI/test/Command/ResolveUrlCommandTest.php @@ -0,0 +1,85 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function correctShortCodeResolvesUrl() + { + $shortCode = 'abc123'; + $expectedUrl = 'http://domain.com/foo/bar'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output); + } + + /** + * @test + */ + public function incorrectShortCodeOutputsErrorMessage() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output); + } + + /** + * @test + */ + public function wrongShortCodeFormatOutputsErrorMessage() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException()) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output); + } +} From 00db8a7ea5e46e91af90c8d8152b9f933fce3d31 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:07:35 +0200 Subject: [PATCH 59/86] Created Common\ErrorHandler tests --- .../ContentBasedErrorHandlerTest.php | 75 +++++++++++++++++++ .../ErrorHandlerManagerFactoryTest.php | 35 +++++++++ .../ErrorHandler/ErrorHandlerManagerTest.php | 45 +++++++++++ 3 files changed, 155 insertions(+) create mode 100644 module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php create mode 100644 module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php create mode 100644 module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php diff --git a/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php b/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php new file mode 100644 index 00000000..6b480e54 --- /dev/null +++ b/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php @@ -0,0 +1,75 @@ +errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [ + 'factories' => [ + 'text/html' => [$this, 'factory'], + 'application/json' => [$this, 'factory'], + ], + ])); + } + + public function factory($container, $name) + { + return function () use ($name) { + return $name; + }; + } + + /** + * @test + */ + public function correctAcceptHeaderValueInvokesErrorHandler() + { + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,application/json'); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('application/json', $result); + } + + /** + * @test + */ + public function defaultContentTypeIsUsedWhenNoAcceptHeaderisPresent() + { + $request = ServerRequestFactory::fromGlobals(); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('text/html', $result); + } + + /** + * @test + */ + public function defaultContentTypeIsUsedWhenAcceptedContentIsNotSupported() + { + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml'); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('text/html', $result); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException + */ + public function ifNoErrorHandlerIsFoundAnExceptionIsThrown() + { + $this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [])); + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml'); + $result = $this->errorHandler->__invoke($request, new Response()); + } +} diff --git a/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php b/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php new file mode 100644 index 00000000..be6d4e6d --- /dev/null +++ b/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php @@ -0,0 +1,35 @@ +factory = new ErrorHandlerManagerFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + 'config' => [ + 'error_handler' => [ + 'plugins' => [], + ], + ], + ]]), ''); + $this->assertInstanceOf(ErrorHandlerManager::class, $instance); + } +} diff --git a/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php b/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php new file mode 100644 index 00000000..4b14f113 --- /dev/null +++ b/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php @@ -0,0 +1,45 @@ +pluginManager = new ErrorHandlerManager(new ServiceManager(), [ + 'services' => [ + 'foo' => function () { + }, + ], + 'invokables' => [ + 'invalid' => \stdClass::class, + ] + ]); + } + + /** + * @test + */ + public function callablesAreReturned() + { + $instance = $this->pluginManager->get('foo'); + $this->assertInstanceOf(\Closure::class, $instance); + } + + /** + * @test + * @expectedException \Zend\ServiceManager\Exception\InvalidServiceException + */ + public function nonCallablesThrowException() + { + $this->pluginManager->get('invalid'); + } +} From 2ce6c1f44b8062556311de2b8f12629cfb260a8b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:17:21 +0200 Subject: [PATCH 60/86] Added more tests to Common module --- .../test/Factory/TranslatorFactoryTest.php | 31 ++++++++ .../test/Middleware/LocaleMiddlewareTest.php | 71 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 module/Common/test/Factory/TranslatorFactoryTest.php create mode 100644 module/Common/test/Middleware/LocaleMiddlewareTest.php diff --git a/module/Common/test/Factory/TranslatorFactoryTest.php b/module/Common/test/Factory/TranslatorFactoryTest.php new file mode 100644 index 00000000..e70f820b --- /dev/null +++ b/module/Common/test/Factory/TranslatorFactoryTest.php @@ -0,0 +1,31 @@ +factory = new TranslatorFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + 'config' => [], + ]]), ''); + $this->assertInstanceOf(Translator::class, $instance); + } +} diff --git a/module/Common/test/Middleware/LocaleMiddlewareTest.php b/module/Common/test/Middleware/LocaleMiddlewareTest.php new file mode 100644 index 00000000..72e6bf77 --- /dev/null +++ b/module/Common/test/Middleware/LocaleMiddlewareTest.php @@ -0,0 +1,71 @@ +translator = Translator::factory(['locale' => 'ru']); + $this->middleware = new LocaleMiddleware($this->translator); + } + + /** + * @test + */ + public function whenNoHeaderIsPresentLocaleIsNotChanged() + { + $this->assertEquals('ru', $this->translator->getLocale()); + $this->middleware->__invoke(ServerRequestFactory::fromGlobals(), new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('ru', $this->translator->getLocale()); + } + + /** + * @test + */ + public function whenTheHeaderIsPresentLocaleIsChanged() + { + $this->assertEquals('ru', $this->translator->getLocale()); + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('es', $this->translator->getLocale()); + } + + /** + * @test + */ + public function localeGetsNormalized() + { + $this->assertEquals('ru', $this->translator->getLocale()); + + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es_ES'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('es', $this->translator->getLocale()); + + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'en-US'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('en', $this->translator->getLocale()); + } +} From fcdcfde04f7a71822f3bc5cee0df532fec29ed71 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:45:48 +0200 Subject: [PATCH 61/86] Added missing tests for Common module --- module/Common/test/ConfigProviderTest.php | 31 ++++++++ .../PaginableRepositoryAdapterTest.php | 43 ++++++++++++ .../test/Service/IpLocationResolverTest.php | 56 +++++++++++++++ .../Extension/TranslatorExtensionTest.php | 70 +++++++++++++++++++ module/Common/test/Util/DateRangeTest.php | 32 +++++++++ 5 files changed, 232 insertions(+) create mode 100644 module/Common/test/ConfigProviderTest.php create mode 100644 module/Common/test/Paginator/PaginableRepositoryAdapterTest.php create mode 100644 module/Common/test/Service/IpLocationResolverTest.php create mode 100644 module/Common/test/Twig/Extension/TranslatorExtensionTest.php create mode 100644 module/Common/test/Util/DateRangeTest.php diff --git a/module/Common/test/ConfigProviderTest.php b/module/Common/test/ConfigProviderTest.php new file mode 100644 index 00000000..54c8c65f --- /dev/null +++ b/module/Common/test/ConfigProviderTest.php @@ -0,0 +1,31 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function configIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('error_handler', $config); + $this->assertArrayHasKey('middleware_pipeline', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('twig', $config); + } +} diff --git a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php new file mode 100644 index 00000000..1135682f --- /dev/null +++ b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php @@ -0,0 +1,43 @@ +repo = $this->prophesize(PaginableRepositoryInterface::class); + $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', 'order'); + } + + /** + * @test + */ + public function getItemsFallbacksToFindList() + { + $this->repo->findList(10, 5, 'search', 'order')->shouldBeCalledTimes(1); + $this->adapter->getItems(5, 10); + } + + /** + * @test + */ + public function countFallbacksToCountList() + { + $this->repo->countList('search')->shouldBeCalledTimes(1); + $this->adapter->count(); + } +} diff --git a/module/Common/test/Service/IpLocationResolverTest.php b/module/Common/test/Service/IpLocationResolverTest.php new file mode 100644 index 00000000..e6130569 --- /dev/null +++ b/module/Common/test/Service/IpLocationResolverTest.php @@ -0,0 +1,56 @@ +client = $this->prophesize(Client::class); + $this->ipResolver = new IpLocationResolver($this->client->reveal()); + } + + /** + * @test + */ + public function correctIpReturnsDecodedInfo() + { + $expected = [ + 'foo' => 'bar', + 'baz' => 'foo', + ]; + $response = new Response(); + $response->getBody()->write(json_encode($expected)); + $response->getBody()->rewind(); + + $this->client->get('http://freegeoip.net/json/1.2.3.4')->willReturn($response) + ->shouldBeCalledTimes(1); + $this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4')); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\WrongIpException + */ + public function guzzleExceptionThrowsShlinkException() + { + $this->client->get('http://freegeoip.net/json/1.2.3.4')->willThrow(new TransferException()) + ->shouldBeCalledTimes(1); + $this->ipResolver->resolveIpLocation('1.2.3.4'); + } +} diff --git a/module/Common/test/Twig/Extension/TranslatorExtensionTest.php b/module/Common/test/Twig/Extension/TranslatorExtensionTest.php new file mode 100644 index 00000000..06b5a584 --- /dev/null +++ b/module/Common/test/Twig/Extension/TranslatorExtensionTest.php @@ -0,0 +1,70 @@ +translator = $this->prophesize(Translator::class); + $this->extension = new TranslatorExtension($this->translator->reveal()); + } + + /** + * @test + */ + public function extensionNameIsClassName() + { + $this->assertEquals(TranslatorExtension::class, $this->extension->getName()); + } + + /** + * @test + */ + public function properFunctionsAreReturned() + { + $funcs = $this->extension->getFunctions(); + $this->assertCount(2, $funcs); + foreach ($funcs as $func) { + $this->assertInstanceOf(\Twig_SimpleFunction::class, $func); + } + } + + /** + * @test + */ + public function translateFallbacksToTranslator() + { + $this->translator->translate('foo', 'default', null)->shouldBeCalledTimes(1); + $this->extension->translate('foo'); + + $this->translator->translate('bar', 'baz', 'en')->shouldBeCalledTimes(1); + $this->extension->translate('bar', 'baz', 'en'); + } + + /** + * @test + */ + public function translatePluralFallbacksToTranslator() + { + $this->translator->translatePlural('foo', 'bar', 'baz', 'default', null)->shouldBeCalledTimes(1); + $this->extension->translatePlural('foo', 'bar', 'baz'); + + $this->translator->translatePlural('foo', 'bar', 'baz', 'another', 'en')->shouldBeCalledTimes(1); + $this->extension->translatePlural('foo', 'bar', 'baz', 'another', 'en'); + } +} diff --git a/module/Common/test/Util/DateRangeTest.php b/module/Common/test/Util/DateRangeTest.php new file mode 100644 index 00000000..4ed1586e --- /dev/null +++ b/module/Common/test/Util/DateRangeTest.php @@ -0,0 +1,32 @@ +assertNull($range->getStartDate()); + $this->assertNull($range->getEndDate()); + $this->assertTrue($range->isEmpty()); + } + + /** + * @test + */ + public function providedDatesAreSet() + { + $startDate = new \DateTime(); + $endDate = new \DateTime(); + $range = new DateRange($startDate, $endDate); + $this->assertSame($startDate, $range->getStartDate()); + $this->assertSame($endDate, $range->getEndDate()); + $this->assertFalse($range->isEmpty()); + } +} From ebeaa3c64a721ae2e71172b38660fddf4187cfa8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 20:02:48 +0200 Subject: [PATCH 62/86] Created RedirectActionTest --- module/Core/src/Action/RedirectAction.php | 17 ++- .../Core/test/Action/RedirectActionTest.php | 104 ++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 module/Core/test/Action/RedirectActionTest.php diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index 122f7f29..031aa0fe 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -70,10 +70,10 @@ class RedirectAction implements MiddlewareInterface // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger // a not-found error if (! isset($longUrl)) { - return $out($request, $response->withStatus(404), 'Not found'); + return $this->notFoundResponse($request, $response, $out); } - // Track visit to this shortcode + // Track visit to this short code $this->visitTracker->track($shortCode); // Return a redirect response to the long URL. @@ -81,7 +81,18 @@ class RedirectAction implements MiddlewareInterface return new RedirectResponse($longUrl); } catch (\Exception $e) { // In case of error, dispatch 404 error - return $out($request, $response); + return $this->notFoundResponse($request, $response, $out); } } + + /** + * @param Request $request + * @param Response $response + * @param callable $out + * @return Response + */ + protected function notFoundResponse(Request $request, Response $response, callable $out) + { + return $out($request, $response->withStatus(404), 'Not Found'); + } } diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php new file mode 100644 index 00000000..434189fc --- /dev/null +++ b/module/Core/test/Action/RedirectActionTest.php @@ -0,0 +1,104 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $visitTracker = $this->prophesize(VisitsTracker::class); + $visitTracker->track(Argument::any()); + $this->action = new RedirectAction($this->urlShortener->reveal(), $visitTracker->reveal()); + } + + /** + * @test + */ + public function redirectionIsPerformedToLongUrl() + { + $shortCode = 'abc123'; + $expectedUrl = 'http://domain.com/foo/bar'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + + $this->assertInstanceOf(Response\RedirectResponse::class, $response); + $this->assertEquals(302, $response->getStatusCode()); + $this->assertTrue($response->hasHeader('Location')); + $this->assertEquals($expectedUrl, $response->getHeaderLine('Location')); + } + + /** + * @test + */ + public function nextErrorMiddlewareIsInvokedIfLongUrlIsNotFound() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $originalResponse = new Response(); + $test = $this; + $this->action->__invoke($request, $originalResponse, function ( + ServerRequestInterface $req, + ResponseInterface $resp, + $error + ) use ( + $test, + $request + ) { + $test->assertSame($request, $req); + $test->assertEquals(404, $resp->getStatusCode()); + $test->assertEquals('Not Found', $error); + }); + } + + /** + * @test + */ + public function nextErrorMiddlewareIsInvokedIfAnExceptionIsThrown() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $originalResponse = new Response(); + $test = $this; + $this->action->__invoke($request, $originalResponse, function ( + ServerRequestInterface $req, + ResponseInterface $resp, + $error + ) use ( + $test, + $request + ) { + $test->assertSame($request, $req); + $test->assertEquals(404, $resp->getStatusCode()); + $test->assertEquals('Not Found', $error); + }); + } +} From ce4877d4ac93931d30d8d8fc951a0e31464d10f0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 22:55:28 +0200 Subject: [PATCH 63/86] Improved VisitsTrackerTest --- .../Core/test/Service/VisitsTrackerTest.php | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/module/Core/test/Service/VisitsTrackerTest.php b/module/Core/test/Service/VisitsTrackerTest.php index 2a441e04..4d8a68fe 100644 --- a/module/Core/test/Service/VisitsTrackerTest.php +++ b/module/Core/test/Service/VisitsTrackerTest.php @@ -5,11 +5,29 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Service\VisitsTracker; class VisitsTrackerTest extends TestCase { + /** + * @var VisitsTracker + */ + protected $visitsTracker; + /** + * @var ObjectProphecy + */ + protected $em; + + public function setUp() + { + $this->em = $this->prophesize(EntityManager::class); + $this->visitsTracker = new VisitsTracker($this->em->reveal()); + } + /** * @test */ @@ -19,12 +37,32 @@ class VisitsTrackerTest extends TestCase $repo = $this->prophesize(EntityRepository::class); $repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl()); - $em = $this->prophesize(EntityManager::class); - $em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); - $em->persist(Argument::any())->shouldBeCalledTimes(1); - $em->flush()->shouldBeCalledTimes(1); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + $this->em->persist(Argument::any())->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); - $visitsTracker = new VisitsTracker($em->reveal()); - $visitsTracker->track($shortCode); + $this->visitsTracker->track($shortCode); + } + + /** + * @test + */ + public function infoReturnsVisistForCertainShortCode() + { + $shortCode = '123ABC'; + $shortUrl = (new ShortUrl())->setOriginalUrl('http://domain.com/foo/bar'); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $list = [ + new Visit(), + new Visit(), + ]; + $repo2 = $this->prophesize(VisitRepository::class); + $repo2->findVisitsByShortUrl($shortUrl, null)->willReturn($list); + $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledTimes(1); + + $this->assertEquals($list, $this->visitsTracker->info($shortCode)); } } From 9c6420fe2672ef2ad6afeb02b0f1fffe26cb127c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:01:07 +0200 Subject: [PATCH 64/86] Created VisitServiceTest --- module/Core/test/Service/VisitServiceTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 module/Core/test/Service/VisitServiceTest.php diff --git a/module/Core/test/Service/VisitServiceTest.php b/module/Core/test/Service/VisitServiceTest.php new file mode 100644 index 00000000..2ecf0a45 --- /dev/null +++ b/module/Core/test/Service/VisitServiceTest.php @@ -0,0 +1,49 @@ +em = $this->prophesize(EntityManager::class); + $this->visitService = new VisitService($this->em->reveal()); + } + + /** + * @test + */ + public function saveVisitsPersistsProvidedVisit() + { + $visit = new Visit(); + $this->em->persist($visit)->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + $this->visitService->saveVisit($visit); + } + + /** + * @test + */ + public function getUnlocatedVisitsFallbacksToRepository() + { + $repo = $this->prophesize(VisitRepository::class); + $repo->findUnlocatedVisits()->shouldBeCalledTimes(1); + $this->em->getRepository(Visit::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + $this->visitService->getUnlocatedVisits(); + } +} From 8c446f0f3b06c2632caac17a62d76637fe4dd758 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:07:44 +0200 Subject: [PATCH 65/86] Created Core\ConfigProviderTest --- module/Core/test/ConfigProviderTest.php | 32 +++++++++++++++++++++++++ phpunit.xml.dist | 4 ++++ 2 files changed, 36 insertions(+) create mode 100644 module/Core/test/ConfigProviderTest.php diff --git a/module/Core/test/ConfigProviderTest.php b/module/Core/test/ConfigProviderTest.php new file mode 100644 index 00000000..fdd41cad --- /dev/null +++ b/module/Core/test/ConfigProviderTest.php @@ -0,0 +1,32 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function properConfigIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('routes', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('templates', $config); + $this->assertArrayHasKey('translator', $config); + $this->assertArrayHasKey('zend-expressive', $config); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 06e1e939..ddd4f42b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,10 @@ ./module/Core/src ./module/Rest/src ./module/CLI/src + + + ./module/Core/src/Repository + From 41939b790da5ba8793dd89cc3ead6d83e13637a7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:17:13 +0200 Subject: [PATCH 66/86] Added more Rest module tests --- module/Rest/test/ConfigProviderTest.php | 33 ++++++++ .../ErrorHandler/JsonErrorHandlerTest.php | 79 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 module/Rest/test/ConfigProviderTest.php create mode 100644 module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php new file mode 100644 index 00000000..377c845f --- /dev/null +++ b/module/Rest/test/ConfigProviderTest.php @@ -0,0 +1,33 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function properConfigIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('error_handler', $config); + $this->assertArrayHasKey('middleware_pipeline', $config); + $this->assertArrayHasKey('rest', $config); + $this->assertArrayHasKey('routes', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('translator', $config); + } +} diff --git a/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php b/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php new file mode 100644 index 00000000..ea1eee80 --- /dev/null +++ b/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php @@ -0,0 +1,79 @@ +errorHandler = new JsonErrorHandler(); + } + + /** + * @test + */ + public function noMatchedRouteReturnsNotFoundResponse() + { + $response = $this->errorHandler->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(404, $response->getStatusCode()); + } + + /** + * @test + */ + public function matchedRouteWithErrorReturnsMethodNotAllowedResponse() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals(), + (new Response())->withStatus(405), + 405 + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(405, $response->getStatusCode()); + } + + /** + * @test + */ + public function responseWithErrorKeepsStatus() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('foo', 'bar', []) + ), + (new Response())->withStatus(401), + 401 + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function responseWithoutErrorReturnsStatus500() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('foo', 'bar', []) + ), + (new Response())->withStatus(200), + 'Some error' + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(500, $response->getStatusCode()); + } +} From f904f79c18ca58320d0e784360c1238154e5d9a6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:26:49 +0200 Subject: [PATCH 67/86] Created CheckAuthenticationMiddlewareTest --- .../CheckAuthenticationMiddlewareTest.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php new file mode 100644 index 00000000..f2d396af --- /dev/null +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -0,0 +1,80 @@ +tokenService = $this->prophesize(RestTokenService::class); + $this->middleware = new CheckAuthenticationMiddleware($this->tokenService->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function someWhitelistedSituationsFallbackToNextMiddleware() + { + $request = ServerRequestFactory::fromGlobals(); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteFailure(['GET']) + ); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('rest-authenticate', 'foo', []) + ); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withMethod('OPTIONS'); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + } +} From ef6f4fba668c2429b5d471a161f583f8c102cb42 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:01:08 +0200 Subject: [PATCH 68/86] Improved CheckAuthenticationMiddlewareTest --- .../CheckAuthenticationMiddlewareTest.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php index f2d396af..650d4d2f 100644 --- a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -3,6 +3,7 @@ namespace ShlinkioTest\Shlink\Rest\Middleware; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Core\Entity\RestToken; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Service\RestTokenService; use Zend\Diactoros\Response; @@ -77,4 +78,57 @@ class CheckAuthenticationMiddlewareTest extends TestCase }); $this->assertTrue($isCalled); } + + /** + * @test + */ + public function noHeaderReturnsError() + { + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + ); + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function provideAnExpiredTokenReturnsError() + { + $authToken = 'ABC-abc'; + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); + $this->tokenService->getByToken($authToken)->willReturn( + (new RestToken())->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D'))) + )->shouldBeCalledTimes(1); + + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function provideCorrectTokenUpdatesExpirationAndFallbacksToNextMiddleware() + { + $authToken = 'ABC-abc'; + $restToken = (new RestToken())->setExpirationDate((new \DateTime())->add(new \DateInterval('P1D'))); + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); + $this->tokenService->getByToken($authToken)->willReturn($restToken)->shouldBeCalledTimes(1); + $this->tokenService->updateExpiration($restToken)->shouldBeCalledTimes(1); + + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + } } From f701e65f7520ae0e1437dd841b8345c0d33125fc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:14:06 +0200 Subject: [PATCH 69/86] Created RestTokenServiceTest --- .../test/Service/RestTokenServiceTest.php | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 module/Rest/test/Service/RestTokenServiceTest.php diff --git a/module/Rest/test/Service/RestTokenServiceTest.php b/module/Rest/test/Service/RestTokenServiceTest.php new file mode 100644 index 00000000..d4487ff1 --- /dev/null +++ b/module/Rest/test/Service/RestTokenServiceTest.php @@ -0,0 +1,93 @@ +em = $this->prophesize(EntityManager::class); + $this->service = new RestTokenService($this->em->reveal(), [ + 'username' => 'foo', + 'password' => 'bar', + ]); + } + + /** + * @test + */ + public function tokenIsCreatedIfCredentialsAreCorrect() + { + $this->em->persist(Argument::type(RestToken::class))->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + + $token = $this->service->createToken('foo', 'bar'); + $this->assertInstanceOf(RestToken::class, $token); + $this->assertFalse($token->isExpired()); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Rest\Exception\AuthenticationException + */ + public function exceptionIsThrownWhileCreatingTokenWithWrongCredentials() + { + $this->service->createToken('foo', 'wrong'); + } + + /** + * @test + */ + public function restTokenIsReturnedFromTokenString() + { + $authToken = 'ABC-abc'; + $theToken = new RestToken(); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['token' => $authToken])->willReturn($theToken)->shouldBeCalledTimes(1); + $this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $this->assertSame($theToken, $this->service->getByToken($authToken)); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException + */ + public function exceptionIsThrownWhenRequestingWrongToken() + { + $authToken = 'ABC-abc'; + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['token' => $authToken])->willReturn(null)->shouldBeCalledTimes(1); + $this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $this->service->getByToken($authToken); + } + + /** + * @test + */ + public function updateExpirationFlushesEntityManager() + { + $token = $this->prophesize(RestToken::class); + $token->updateExpiration()->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + + $this->service->updateExpiration($token->reveal()); + } +} From 6f7e4f7e7f27712d52a473af46c29cb5ba139482 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:18:36 +0200 Subject: [PATCH 70/86] Created RestUtilsTest --- module/Rest/test/Util/RestUtilsTest.php | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 module/Rest/test/Util/RestUtilsTest.php diff --git a/module/Rest/test/Util/RestUtilsTest.php b/module/Rest/test/Util/RestUtilsTest.php new file mode 100644 index 00000000..d53b6905 --- /dev/null +++ b/module/Rest/test/Util/RestUtilsTest.php @@ -0,0 +1,40 @@ +assertEquals( + RestUtils::INVALID_SHORTCODE_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidShortCodeException()) + ); + $this->assertEquals( + RestUtils::INVALID_URL_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidUrlException()) + ); + $this->assertEquals( + RestUtils::INVALID_ARGUMENT_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidArgumentException()) + ); + $this->assertEquals( + RestUtils::INVALID_CREDENTIALS_ERROR, + RestUtils::getRestErrorCodeFromException(new AuthenticationException()) + ); + $this->assertEquals( + RestUtils::UNKNOWN_ERROR, + RestUtils::getRestErrorCodeFromException(new WrongIpException()) + ); + } +} From 878518ced705cf2fc1cafb73f49670667b34e97a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:33:55 +0200 Subject: [PATCH 71/86] Created AuthenticateActionTest --- .../test/Action/AuthenticateActionTest.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 module/Rest/test/Action/AuthenticateActionTest.php diff --git a/module/Rest/test/Action/AuthenticateActionTest.php b/module/Rest/test/Action/AuthenticateActionTest.php new file mode 100644 index 00000000..d61da421 --- /dev/null +++ b/module/Rest/test/Action/AuthenticateActionTest.php @@ -0,0 +1,75 @@ +tokenService = $this->prophesize(RestTokenService::class); + $this->action = new AuthenticateAction($this->tokenService->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function notProvidingAuthDataReturnsError() + { + $resp = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertEquals(400, $resp->getStatusCode()); + } + + /** + * @test + */ + public function properCredentialsReturnTokenInResponse() + { + $this->tokenService->createToken('foo', 'bar')->willReturn( + (new RestToken())->setToken('abc-ABC') + )->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'username' => 'foo', + 'password' => 'bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + + $response->getBody()->rewind(); + $this->assertEquals(['token' => 'abc-ABC'], json_decode($response->getBody()->getContents(), true)); + } + + /** + * @test + */ + public function authenticationExceptionsReturnErrorResponse() + { + $this->tokenService->createToken('foo', 'bar')->willThrow(new AuthenticationException()) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'username' => 'foo', + 'password' => 'bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } +} From 04e0a192add9ae5504fca0ec9be2be685a169716 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 15:58:18 +0200 Subject: [PATCH 72/86] Created CreateShortcodeActionTest --- .../test/Action/CreateShortcodeActionTest.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 module/Rest/test/Action/CreateShortcodeActionTest.php diff --git a/module/Rest/test/Action/CreateShortcodeActionTest.php b/module/Rest/test/Action/CreateShortcodeActionTest.php new file mode 100644 index 00000000..dcc56bf6 --- /dev/null +++ b/module/Rest/test/Action/CreateShortcodeActionTest.php @@ -0,0 +1,92 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $this->action = new CreateShortcodeAction($this->urlShortener->reveal(), Translator::factory([]), [ + 'schema' => 'http', + 'hostname' => 'foo.com', + ]); + } + + /** + * @test + */ + public function missingLongUrlParamReturnsError() + { + $response = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertEquals(400, $response->getStatusCode()); + } + + /** + * @test + */ + public function properShortcodeConversionReturnsData() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willReturn('abc123') + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0); + } + + /** + * @test + */ + public function anInvalidUrlReturnsError() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(InvalidUrlException::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0); + } + + /** + * @test + */ + public function aGenericExceptionWillReturnError() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); + } +} From c6b7515285166700a6c49c076eae3f4371923fd5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:10:16 +0200 Subject: [PATCH 73/86] Created GetVisitsActionTest --- .../Rest/test/Action/GetVisitsActionTest.php | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 module/Rest/test/Action/GetVisitsActionTest.php diff --git a/module/Rest/test/Action/GetVisitsActionTest.php b/module/Rest/test/Action/GetVisitsActionTest.php new file mode 100644 index 00000000..901c549e --- /dev/null +++ b/module/Rest/test/Action/GetVisitsActionTest.php @@ -0,0 +1,99 @@ +visitsTracker = $this->prophesize(VisitsTracker::class); + $this->action = new GetVisitsAction($this->visitsTracker->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function providingCorrectShortCodeReturnsVisits() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([]) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @test + */ + public function providingInvalidShortCodeReturnsError() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow( + InvalidArgumentException::class + )->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(400, $response->getStatusCode()); + } + + /** + * @test + */ + public function unexpectedExceptionWillReturnError() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow( + \Exception::class + )->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(500, $response->getStatusCode()); + } + + /** + * @test + */ + public function datesAreReadFromQuery() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, new DateRange(null, new \DateTime('2016-01-01 00:00:00'))) + ->willReturn([]) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode) + ->withQueryParams(['endDate' => '2016-01-01 00:00:00']), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } +} From 08f6d2de78389d0fed61c6165bee8a071259ee3f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:16:26 +0200 Subject: [PATCH 74/86] Created ListShortcodesActionTest --- .../test/Action/ListShortcodesActionTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 module/Rest/test/Action/ListShortcodesActionTest.php diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php new file mode 100644 index 00000000..b5ec0c9d --- /dev/null +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -0,0 +1,66 @@ +service = $this->prophesize(ShortUrlService::class); + $this->action = new ListShortcodesAction($this->service->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function properListReturnsSuccessResponse() + { + $page = 3; + $this->service->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withQueryParams([ + 'page' => $page, + ]), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @test + */ + public function anExceptionsReturnsErrorResponse() + { + $page = 3; + $this->service->listShortUrls($page)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withQueryParams([ + 'page' => $page, + ]), + new Response() + ); + $this->assertEquals(500, $response->getStatusCode()); + } +} From 3d5e5d5df95a1ed3c0cb02d8700f8966f122f10d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:24:00 +0200 Subject: [PATCH 75/86] Created ResolveUrlActionTest --- .../Rest/test/Action/ResolveUrlActionTest.php | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 module/Rest/test/Action/ResolveUrlActionTest.php diff --git a/module/Rest/test/Action/ResolveUrlActionTest.php b/module/Rest/test/Action/ResolveUrlActionTest.php new file mode 100644 index 00000000..0bd3bded --- /dev/null +++ b/module/Rest/test/Action/ResolveUrlActionTest.php @@ -0,0 +1,90 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $this->action = new ResolveUrlAction($this->urlShortener->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function incorrectShortCodeReturnsError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0); + } + + /** + * @test + */ + public function correctShortCodeReturnsSuccess() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar') + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0); + } + + /** + * @test + */ + public function invalidShortCodeExceptionReturnsError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0); + } + + /** + * @test + */ + public function unexpectedExceptionWillReturnError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); + } +} From a957f66ed080f4511ba49cd88c854efa8e9fcca7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:30:05 +0200 Subject: [PATCH 76/86] Renamed services first level config key to dependencies --- config/autoload/errorhandler.local.php.dist | 2 +- config/autoload/services.global.php | 2 +- config/container.php | 2 +- module/CLI/config/services.config.php | 2 +- module/CLI/test/ConfigProviderTest.php | 2 +- module/Common/config/services.config.php | 2 +- module/Common/test/ConfigProviderTest.php | 2 +- module/Core/config/services.config.php | 2 +- module/Core/test/ConfigProviderTest.php | 2 +- module/Rest/config/services.config.php | 2 +- module/Rest/test/ConfigProviderTest.php | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index afbc52ed..7e361e0a 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -3,7 +3,7 @@ use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler; use Zend\Expressive\Container\WhoopsErrorHandlerFactory; return [ - 'services' => [ + 'dependencies' => [ 'invokables' => [ 'Zend\Expressive\Whoops' => Whoops\Run::class, 'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class, diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 44bbbbdd..c0c5c46a 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -10,7 +10,7 @@ use Zend\ServiceManager\Factory\InvokableFactory; return [ - 'services' => [ + 'dependencies' => [ 'factories' => [ Expressive\Application::class => Container\ApplicationFactory::class, diff --git a/config/container.php b/config/container.php index 5678edcb..cbc84db5 100644 --- a/config/container.php +++ b/config/container.php @@ -16,6 +16,6 @@ if (class_exists(Dotenv::class)) { // Build container $config = require __DIR__ . '/config.php'; -$container = new ServiceManager($config['services']); +$container = new ServiceManager($config['dependencies']); $container->setService('config', $config); return $container; diff --git a/module/CLI/config/services.config.php b/module/CLI/config/services.config.php index 5c5931a4..86398cf4 100644 --- a/module/CLI/config/services.config.php +++ b/module/CLI/config/services.config.php @@ -5,7 +5,7 @@ use Symfony\Component\Console; return [ - 'services' => [ + 'dependencies' => [ 'factories' => [ Console\Application::class => CLI\Factory\ApplicationFactory::class, diff --git a/module/CLI/test/ConfigProviderTest.php b/module/CLI/test/ConfigProviderTest.php index 8d368561..6231752a 100644 --- a/module/CLI/test/ConfigProviderTest.php +++ b/module/CLI/test/ConfigProviderTest.php @@ -24,7 +24,7 @@ class ConfigProviderTest extends TestCase $config = $this->configProvider->__invoke(); $this->assertArrayHasKey('cli', $config); - $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('translator', $config); } } diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php index be062b86..bcd30faa 100644 --- a/module/Common/config/services.config.php +++ b/module/Common/config/services.config.php @@ -14,7 +14,7 @@ use Zend\ServiceManager\Factory\InvokableFactory; return [ - 'services' => [ + 'dependencies' => [ 'factories' => [ EntityManager::class => EntityManagerFactory::class, GuzzleHttp\Client::class => InvokableFactory::class, diff --git a/module/Common/test/ConfigProviderTest.php b/module/Common/test/ConfigProviderTest.php index 54c8c65f..dd7723df 100644 --- a/module/Common/test/ConfigProviderTest.php +++ b/module/Common/test/ConfigProviderTest.php @@ -25,7 +25,7 @@ class ConfigProviderTest extends TestCase $this->assertArrayHasKey('error_handler', $config); $this->assertArrayHasKey('middleware_pipeline', $config); - $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('twig', $config); } } diff --git a/module/Core/config/services.config.php b/module/Core/config/services.config.php index 7d044875..9bcacbfa 100644 --- a/module/Core/config/services.config.php +++ b/module/Core/config/services.config.php @@ -5,7 +5,7 @@ use Shlinkio\Shlink\Core\Service; return [ - 'services' => [ + 'dependencies' => [ 'factories' => [ // Services Service\UrlShortener::class => AnnotatedFactory::class, diff --git a/module/Core/test/ConfigProviderTest.php b/module/Core/test/ConfigProviderTest.php index fdd41cad..3af8a48a 100644 --- a/module/Core/test/ConfigProviderTest.php +++ b/module/Core/test/ConfigProviderTest.php @@ -24,7 +24,7 @@ class ConfigProviderTest extends TestCase $config = $this->configProvider->__invoke(); $this->assertArrayHasKey('routes', $config); - $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('templates', $config); $this->assertArrayHasKey('translator', $config); $this->assertArrayHasKey('zend-expressive', $config); diff --git a/module/Rest/config/services.config.php b/module/Rest/config/services.config.php index 551c4c96..e04c8ba0 100644 --- a/module/Rest/config/services.config.php +++ b/module/Rest/config/services.config.php @@ -7,7 +7,7 @@ use Zend\ServiceManager\Factory\InvokableFactory; return [ - 'services' => [ + 'dependencies' => [ 'factories' => [ Service\RestTokenService::class => AnnotatedFactory::class, diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php index 377c845f..6801a82b 100644 --- a/module/Rest/test/ConfigProviderTest.php +++ b/module/Rest/test/ConfigProviderTest.php @@ -27,7 +27,7 @@ class ConfigProviderTest extends TestCase $this->assertArrayHasKey('middleware_pipeline', $config); $this->assertArrayHasKey('rest', $config); $this->assertArrayHasKey('routes', $config); - $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('translator', $config); } } From 7b1e855e7fb2237a809d4829ba38e0aa5e01ad89 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:32:27 +0200 Subject: [PATCH 77/86] Renamed services config files to dependencies --- config/autoload/{services.global.php => dependencies.global.php} | 0 .../CLI/config/{services.config.php => dependencies.config.php} | 0 .../config/{services.config.php => dependencies.config.php} | 0 .../Core/config/{services.config.php => dependencies.config.php} | 0 .../Rest/config/{services.config.php => dependencies.config.php} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename config/autoload/{services.global.php => dependencies.global.php} (100%) rename module/CLI/config/{services.config.php => dependencies.config.php} (100%) rename module/Common/config/{services.config.php => dependencies.config.php} (100%) rename module/Core/config/{services.config.php => dependencies.config.php} (100%) rename module/Rest/config/{services.config.php => dependencies.config.php} (100%) diff --git a/config/autoload/services.global.php b/config/autoload/dependencies.global.php similarity index 100% rename from config/autoload/services.global.php rename to config/autoload/dependencies.global.php diff --git a/module/CLI/config/services.config.php b/module/CLI/config/dependencies.config.php similarity index 100% rename from module/CLI/config/services.config.php rename to module/CLI/config/dependencies.config.php diff --git a/module/Common/config/services.config.php b/module/Common/config/dependencies.config.php similarity index 100% rename from module/Common/config/services.config.php rename to module/Common/config/dependencies.config.php diff --git a/module/Core/config/services.config.php b/module/Core/config/dependencies.config.php similarity index 100% rename from module/Core/config/services.config.php rename to module/Core/config/dependencies.config.php diff --git a/module/Rest/config/services.config.php b/module/Rest/config/dependencies.config.php similarity index 100% rename from module/Rest/config/services.config.php rename to module/Rest/config/dependencies.config.php From d73d3049b777c51c1308fa32aff3e994d76be70a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:42:19 +0200 Subject: [PATCH 78/86] Removed dependency on expressive-helpers package --- composer.json | 5 ++--- config/autoload/dependencies.global.php | 9 --------- config/autoload/middleware-pipeline.global.php | 9 --------- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index b9be6c6d..56389b9f 100644 --- a/composer.json +++ b/composer.json @@ -2,11 +2,11 @@ "name": "shlinkio/shlink", "type": "project", "homepage": "http://shlink.io", - "description": "A PHP-based URL shortener application with analytics and management", + "description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces", "license": "MIT", "authors": [ { - "name": "Alejandro Celaya ALastrué", + "name": "Alejandro Celaya Alastrué", "homepage": "http://www.alejandrocelaya.com", "email": "alejandro@alejandrocelaya.com" } @@ -14,7 +14,6 @@ "require": { "php": "^5.6 || ^7.0", "zendframework/zend-expressive": "^1.0", - "zendframework/zend-expressive-helpers": "^2.0", "zendframework/zend-expressive-fastroute": "^1.1", "zendframework/zend-expressive-twigrenderer": "^1.0", "zendframework/zend-stdlib": "^2.7", diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index c0c5c46a..209334b2 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -2,7 +2,6 @@ use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler; use Zend\Expressive; use Zend\Expressive\Container; -use Zend\Expressive\Helper; use Zend\Expressive\Router; use Zend\Expressive\Template; use Zend\Expressive\Twig; @@ -13,15 +12,7 @@ return [ 'dependencies' => [ 'factories' => [ Expressive\Application::class => Container\ApplicationFactory::class, - - // Url helpers - Helper\UrlHelper::class => Helper\UrlHelperFactory::class, - Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, - Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, - Helper\ServerUrlHelper::class => InvokableFactory::class, Router\FastRouteRouter::class => InvokableFactory::class, - - // View Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 6e72ea1f..0e214307 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -1,17 +1,9 @@ [ - 'always' => [ - 'middleware' => [ - Helper\ServerUrlMiddleware::class, - ], - 'priority' => 10000, - ], - 'routing' => [ 'middleware' => [ ApplicationFactory::ROUTING_MIDDLEWARE, @@ -21,7 +13,6 @@ return [ 'post-routing' => [ 'middleware' => [ - Helper\UrlHelperMiddleware::class, ApplicationFactory::DISPATCH_MIDDLEWARE, ], 'priority' => 1, From 30988b10d1bd6356fe60f3c243b092df45b313f2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 14:36:39 +0200 Subject: [PATCH 79/86] Added Laravel's env helper --- composer.json | 5 ++- config/autoload/database.global.php | 6 ++-- config/autoload/translator.global.php | 2 +- config/autoload/url-shortener.global.php | 8 +++-- module/Common/functions/functions.php | 36 ++++++++++++++++++++++ module/Common/src/Factory/CacheFactory.php | 2 +- module/Rest/config/rest.config.php | 4 +-- 7 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 module/Common/functions/functions.php diff --git a/composer.json b/composer.json index 56389b9f..829bb949 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,10 @@ "Shlinkio\\Shlink\\Rest\\": "module/Rest/src", "Shlinkio\\Shlink\\Core\\": "module/Core/src", "Shlinkio\\Shlink\\Common\\": "module/Common/src" - } + }, + "files": [ + "module/Common/functions/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/config/autoload/database.global.php b/config/autoload/database.global.php index 62733f22..4d05ea36 100644 --- a/config/autoload/database.global.php +++ b/config/autoload/database.global.php @@ -3,9 +3,9 @@ return [ 'database' => [ 'driver' => 'pdo_mysql', - 'user' => getenv('DB_USER'), - 'password' => getenv('DB_PASSWORD'), - 'dbname' => getenv('DB_NAME') ?: 'shlink', + 'user' => env('DB_USER'), + 'password' => env('DB_PASSWORD'), + 'dbname' => env('DB_NAME', 'shlink'), 'charset' => 'utf8', 'driverOptions' => [ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' diff --git a/config/autoload/translator.global.php b/config/autoload/translator.global.php index e3730927..d0561cbe 100644 --- a/config/autoload/translator.global.php +++ b/config/autoload/translator.global.php @@ -2,7 +2,7 @@ return [ 'translator' => [ - 'locale' => getenv('DEFAULT_LOCALE') ?: 'en', + 'locale' => env('DEFAULT_LOCALE', 'en'), ], ]; diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 718ed3f0..5b0d95c9 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -1,12 +1,14 @@ [ 'domain' => [ - 'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http', - 'hostname' => getenv('SHORTENED_URL_HOSTNAME'), + 'schema' => env('SHORTENED_URL_SCHEMA', 'http'), + 'hostname' => env('SHORTENED_URL_HOSTNAME'), ], - 'shortcode_chars' => getenv('SHORTCODE_CHARS'), + 'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS), ], ]; diff --git a/module/Common/functions/functions.php b/module/Common/functions/functions.php new file mode 100644 index 00000000..f0f86f77 --- /dev/null +++ b/module/Common/functions/functions.php @@ -0,0 +1,36 @@ + [ - 'username' => getenv('REST_USER'), - 'password' => getenv('REST_PASSWORD'), + 'username' => env('REST_USER'), + 'password' => env('REST_PASSWORD'), ], ]; From 7b98527f2e5bbdc20e062cfef866e8b1b009084d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 20:16:13 +0200 Subject: [PATCH 80/86] Improved CacheFactory so that adapter can be set in config --- .../config/middleware-pipeline.config.php | 1 + module/Common/src/Factory/CacheFactory.php | 17 +++++++++- .../Common/test/Factory/CacheFactoryTest.php | 34 +++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/module/Common/config/middleware-pipeline.config.php b/module/Common/config/middleware-pipeline.config.php index 361621c6..aab5af85 100644 --- a/module/Common/config/middleware-pipeline.config.php +++ b/module/Common/config/middleware-pipeline.config.php @@ -11,4 +11,5 @@ return [ 'priority' => 5, ], ], + ]; diff --git a/module/Common/src/Factory/CacheFactory.php b/module/Common/src/Factory/CacheFactory.php index 6e17a82c..c866b980 100644 --- a/module/Common/src/Factory/CacheFactory.php +++ b/module/Common/src/Factory/CacheFactory.php @@ -11,6 +11,11 @@ use Zend\ServiceManager\Factory\FactoryInterface; class CacheFactory implements FactoryInterface { + const VALID_CACHE_ADAPTERS = [ + ApcuCache::class, + ArrayCache::class, + ]; + /** * Create an object * @@ -25,6 +30,16 @@ class CacheFactory implements FactoryInterface */ public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { - return env('APP_ENV', 'dev') === 'pro' ? new ApcuCache() : new ArrayCache(); + // Try to get the adapter from config + $config = $container->get('config'); + if (isset($config['cache']) + && isset($config['cache']['adapter']) + && in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS) + ) { + return new $config['cache']['adapter'](); + } + + // If the adapter has not been set in config, create one based on environment + return env('APP_ENV', 'pro') === 'pro' ? new ApcuCache() : new ArrayCache(); } } diff --git a/module/Common/test/Factory/CacheFactoryTest.php b/module/Common/test/Factory/CacheFactoryTest.php index 6f23aa0b..2e938dfa 100644 --- a/module/Common/test/Factory/CacheFactoryTest.php +++ b/module/Common/test/Factory/CacheFactoryTest.php @@ -3,6 +3,7 @@ namespace ShlinkioTest\Shlink\Common\Factory; use Doctrine\Common\Cache\ApcuCache; use Doctrine\Common\Cache\ArrayCache; +use Doctrine\Common\Cache\FilesystemCache; use PHPUnit_Framework_TestCase as TestCase; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Zend\ServiceManager\ServiceManager; @@ -30,7 +31,7 @@ class CacheFactoryTest extends TestCase public function productionReturnsApcAdapter() { putenv('APP_ENV=pro'); - $instance = $this->factory->__invoke(new ServiceManager(), ''); + $instance = $this->factory->__invoke($this->createSM(), ''); $this->assertInstanceOf(ApcuCache::class, $instance); } @@ -40,7 +41,36 @@ class CacheFactoryTest extends TestCase public function developmentReturnsArrayAdapter() { putenv('APP_ENV=dev'); - $instance = $this->factory->__invoke(new ServiceManager(), ''); + $instance = $this->factory->__invoke($this->createSM(), ''); $this->assertInstanceOf(ArrayCache::class, $instance); } + + /** + * @test + */ + public function adapterDefinedInConfigIgnoresEnvironment() + { + putenv('APP_ENV=pro'); + $instance = $this->factory->__invoke($this->createSM(ArrayCache::class), ''); + $this->assertInstanceOf(ArrayCache::class, $instance); + } + + /** + * @test + */ + public function invalidAdapterDefinedInConfigFallbacksToEnvironment() + { + putenv('APP_ENV=pro'); + $instance = $this->factory->__invoke($this->createSM(FilesystemCache::class), ''); + $this->assertInstanceOf(ApcuCache::class, $instance); + } + + private function createSM($cacheAdapter = null) + { + return new ServiceManager(['services' => [ + 'config' => isset($cacheAdapter) ? [ + 'cache' => ['adapter' => $cacheAdapter], + ] : [], + ]]); + } } From ce2b28a0b45a56de6a9999cfdd63149e88e8d521 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 20:44:45 +0200 Subject: [PATCH 81/86] Improved cli entry point so that the language is set to the translator based on the CLI_LANGUAGE env var --- .env.dist | 3 +++ bin/cli | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.env.dist b/.env.dist index 726635ba..9b175618 100644 --- a/.env.dist +++ b/.env.dist @@ -3,7 +3,10 @@ APP_ENV= SHORTENED_URL_SCHEMA= SHORTENED_URL_HOSTNAME= SHORTCODE_CHARS= + +# Language DEFAULT_LOCALE= +CLI_LOCALE= # Database DB_USER= diff --git a/bin/cli b/bin/cli index e400bef8..abbeed47 100755 --- a/bin/cli +++ b/bin/cli @@ -2,9 +2,16 @@ get('translator'); +$translator->setLocale(env('CLI_LOCALE', 'en')); + +/** @var Application $app */ $app = $container->get(CliApp::class); $app->run(); From 9b9b1415fe92a038a019b239109ea7438376e63c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 21:11:42 +0200 Subject: [PATCH 82/86] Created GenerateCharsetCommand --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 19 ++++--- .../Command/Config/GenerateCharsetCommand.php | 44 +++++++++++++++ .../Config/GenerateCharsetCommandTest.php | 56 +++++++++++++++++++ 4 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 module/CLI/src/Command/Config/GenerateCharsetCommand.php create mode 100644 module/CLI/test/Command/Config/GenerateCharsetCommandTest.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 6a6e4122..a9b13a72 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -10,6 +10,7 @@ return [ Command\ListShortcodesCommand::class, Command\GetVisitsCommand::class, Command\ProcessVisitsCommand::class, + Command\Config\GenerateCharsetCommand::class, ] ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 86398cf4..d99f68d9 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -1,19 +1,22 @@ [ 'factories' => [ - Console\Application::class => CLI\Factory\ApplicationFactory::class, + Application::class => ApplicationFactory::class, - CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, - CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, - CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, - CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class, - CLI\Command\ProcessVisitsCommand::class => AnnotatedFactory::class, + Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, + Command\ResolveUrlCommand::class => AnnotatedFactory::class, + Command\ListShortcodesCommand::class => AnnotatedFactory::class, + Command\GetVisitsCommand::class => AnnotatedFactory::class, + Command\ProcessVisitsCommand::class => AnnotatedFactory::class, + Command\ProcessVisitsCommand::class => AnnotatedFactory::class, + Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/Config/GenerateCharsetCommand.php b/module/CLI/src/Command/Config/GenerateCharsetCommand.php new file mode 100644 index 00000000..22369934 --- /dev/null +++ b/module/CLI/src/Command/Config/GenerateCharsetCommand.php @@ -0,0 +1,44 @@ +translator = $translator; + parent::__construct(null); + } + + public function configure() + { + $this->setName('config:generate-charset') + ->setDescription(sprintf($this->translator->translate( + 'Generates a character set sample just by shuffling the default one, "%s". ' + . 'Then it can be set in the SHORTCODE_CHARS environment variable' + ), UrlShortener::DEFAULT_CHARS)); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $charSet = str_shuffle(UrlShortener::DEFAULT_CHARS); + $output->writeln($this->translator->translate('Character set:') . sprintf(' %s', $charSet)); + } +} diff --git a/module/CLI/test/Command/Config/GenerateCharsetCommandTest.php b/module/CLI/test/Command/Config/GenerateCharsetCommandTest.php new file mode 100644 index 00000000..03a81b53 --- /dev/null +++ b/module/CLI/test/Command/Config/GenerateCharsetCommandTest.php @@ -0,0 +1,56 @@ +add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function charactersAreGeneratedFromDefault() + { + $prefix = 'Character set: '; + $prefixLength = strlen($prefix); + + $this->commandTester->execute([ + 'command' => 'config:generate-charset', + ]); + $output = $this->commandTester->getDisplay(); + + // Both default character set and the new one should have the same length + $this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output)); + + // Both default character set and the new one should have the same characters + $charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS)); + $orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS); + $orderedCharset = $this->orderStringLetters($charset); + $this->assertEquals($orderedDefault, $orderedCharset); + } + + protected function orderStringLetters($string) + { + $letters = str_split($string); + sort($letters); + return implode('', $letters); + } +} From 307dfc64b479af8b9b0be346bebcb492bf00e596 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 21:13:17 +0200 Subject: [PATCH 83/86] Created installation steps doc --- data/docs/installation.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 data/docs/installation.md diff --git a/data/docs/installation.md b/data/docs/installation.md new file mode 100644 index 00000000..67e241fc --- /dev/null +++ b/data/docs/installation.md @@ -0,0 +1,19 @@ +### Installation steps + +- Define ENV vars in apache or nginx: + - SHORTENED_URL_SCHEMA: http|https + - SHORTENED_URL_HOSTNAME: Short domain + - SHORTCODE_CHARS: The char set used to generate short codes (defaults to **123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ**, but a new one can be generated with the `config:generate-charset` command) + - DB_USER: MySQL database user + - DB_PASSWORD: MySQL database password + - REST_USER: Username for REST authentication + - REST_PASSWORD: Password for REST authentication + - DB_NAME: MySQL database name (defaults to **shlink**) + - DEFAULT_LOCALE: Language in which web requests (browser and REST) will be returned if no `Accept-Language` header is sent (defaults to **en**) + - CLI_LOCALE: Language in which console command messages will be displayed (defaults to **en**) +- Create database (`vendor/bin/doctrine orm:schema-tool:create`) +- Add write permissions to `data` directory +- Create doctrine proxies (`vendor/bin/doctrine orm:generate-proxies`) +- Create symlink to bin/cli as `shlink` in /usr/local/bin (linux only. Optional) + +Supported languages: es and en From 40a3ba6203593973d6a0e0f092c41bdd86ebd03a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 21:15:30 +0200 Subject: [PATCH 84/86] Improved rest documentation --- data/docs/rest.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/docs/rest.md b/data/docs/rest.md index 35f18e2a..bbc9a565 100644 --- a/data/docs/rest.md +++ b/data/docs/rest.md @@ -13,11 +13,13 @@ Statuses: ## Authentication -[TODO] +Once you have called to the authentication endpoint for the first time (see below) yopu will get an authentication token. + +You will have to send that token in the `X-Auth-Token` header on any later request or you will get an authentication error. ## Language -In order to set the application language, you have to pass it by using the Accept-Language header. +In order to set the application language, you have to pass it by using the `Accept-Language` header. If not provided or provided language is not supported, english (en_US) will be used. From 21dc9ac5a0c452281e446705f212b1112d3a0e05 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 21:25:15 +0200 Subject: [PATCH 85/86] Updated changelog for version 1 --- .travis.yml | 1 - CHANGELOG.md | 27 +++++++++++++++++++++++++++ module/CLI/lang/es.mo | Bin 3826 -> 4071 bytes module/CLI/lang/es.po | 20 ++++++++++++++++---- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 905480ea..52312334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ php: - 5.6 - 7 - 7.1 - - hhvm before_script: - composer self-update diff --git a/CHANGELOG.md b/CHANGELOG.md index c08baeb7..5c00b4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ ## CHANGELOG +### 1.0.0 + +**Enhancements:** + +* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33) +* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15) +* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23) +* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21) +* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22) +* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20) +* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11) +* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18) + +**Tasks** + +* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36) +* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4) +* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6) +* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30) +* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12) +* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25) +* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19) + +**Bugs** + +* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24) + ### 0.2.0 **Enhancements:** diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 5d5830b3db3fb3b331f55692446c7766ffeddf52..b51c40cb39c8ec9d16ff4e818f2aa38a26f83482 100644 GIT binary patch delta 1300 zcmZY8OH30%7{Kwtf+C2B_!eYDQKJ^yCcf$mLP1S1Rp6otF+&|-$+Fw*Zi&zIs8=-} zJbEK1n5Mtp>=_zaihTh#ao>$F2iRi^Snd} z#(zfHxlU@QQRmFYWvd)j*%LAypU!G`^#E?6!PdtA3<8Z0;r#RMa}`doYOW*` zH*1ZX9o6ymu)tBtJDY=pM zJ-b(Tc7>hYdSudi+KF_*czVprSGD8CgNIM$herDHx&DiNr+SBmwe@D4!1t!D7wH)j zIA+YX>hwfqpzh9+@}XoZtuyItCcP(>Nu?Uj=95Eq#t9wYOXf^uvpSPXA4sMa#$~GG z22B_xM*`!8u8I60t9xBLWxPv)uluZPt{8pP%=h#x)C%#V?t(vkykXFI6Q#<|`qspn zlR>HI6C}Y@lVro}-NTJhdE);EI&>sa5{=c<_Hw~xR$p6JR|gmUK+_I7VT<5Bh_dM;Ig|x^T_%PY>HECS2IFFSdjfuoR`j6Q^ delta 1002 zcmYk)O=uHA6u|Mo@7WMlD)D2Ye zAU?)8|Cd!N!*qPZ9^Anw-SIGL{}jgY3hDxua2MXhk3{P}?xlT$)n@P^&f#Y~g<}jJ zz$HA5cX1xqu#f*sx>JNj%RFkxE}*WkfV#6L>Vyxl55J=xRgBdzhaA8(CXq)96uRSO zoWa|u^Ss6ken5>xg3dadmMoQR=;9f?hDm&hJo1}j0%Odok;$Mg;5=%76?Ob0)D1jC zgKv=lg?)pl$GVfOS4-~zQPcesaSK{t{v$}ScYNMGvX-2(CM!KPy5#!dhQY&vu?FD-bF5smlr+1 zlJgwbEO@>t`CcvWxQ;n@cFx#E$M|lg>KDtVvgn%dQ+*SxbTr%JlOtnElT4*k~Ma5R`UE@-YeRUZI+GW)~ary Q=C\n" "Language-Team: \n" "Language: es_ES\n" "MIME-Version: 1.0\n" @@ -17,7 +17,19 @@ msgstr "" "X-Poedit-SearchPath-0: src\n" "X-Poedit-SearchPath-1: config\n" -msgid "Generates a shortcode for provided URL and returns the short URL" +#, php-format +msgid "" +"Generates a character set sample just by shuffling the default one, \"%s\". " +"Then it can be set in the SHORTCODE_CHARS environment variable" +msgstr "" +"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s" +"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS" + +msgid "Character set:" +msgstr "Grupo de caracteres:" + +#, fuzzy +msgid "Generates a short code for provided URL and returns the short URL" msgstr "" "Genera un código corto para la URL proporcionada y devuelve la URL acortada" From 5e493b435a7eefffd0f2fa14c4f92cef93d6b339 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Aug 2016 21:26:47 +0200 Subject: [PATCH 86/86] Added badges to readme file --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 767a2605..5a605704 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Shlink +[![Build Status](https://travis-ci.org/shlinkio/shlink.svg?branch=master)](https://travis-ci.org/shlinkio/shlink) +[![Code Coverage](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master) +[![Latest Stable Version](https://poser.pugx.org/shlinkio/shlink/v/stable.png)](https://packagist.org/packages/shlinkio/shlink) +[![License](https://poser.pugx.org/shlinkio/shlink/license.png)](https://packagist.org/packages/shlinkio/shlink) + A PHP-based URL shortener application with analytics and management