From ee1aa42900fc36bb4f962f54a9eef43ec093a75b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 7 Jun 2020 09:00:41 +0200 Subject: [PATCH 01/55] Improved titles on error templates --- module/Core/templates/error/404.phtml | 2 +- module/Core/templates/invalid-short-code.phtml | 2 +- module/Core/templates/layout/default.phtml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/Core/templates/error/404.phtml b/module/Core/templates/error/404.phtml index bd1b0b6f..20ac4ff8 100644 --- a/module/Core/templates/error/404.phtml +++ b/module/Core/templates/error/404.phtml @@ -1,7 +1,7 @@ layout('ShlinkCore::layout/default') ?> start('title') ?> - URL Not Found + Not Found end() ?> start('stylesheets') ?> diff --git a/module/Core/templates/invalid-short-code.phtml b/module/Core/templates/invalid-short-code.phtml index a435ba0b..47be4a16 100644 --- a/module/Core/templates/invalid-short-code.phtml +++ b/module/Core/templates/invalid-short-code.phtml @@ -1,7 +1,7 @@ layout('ShlinkCore::layout/default') ?> start('title') ?> - Invalid URL + Invalid Short URL end() ?> start('stylesheets') ?> diff --git a/module/Core/templates/layout/default.phtml b/module/Core/templates/layout/default.phtml index b231d48b..fbb78b26 100644 --- a/module/Core/templates/layout/default.phtml +++ b/module/Core/templates/layout/default.phtml @@ -1,7 +1,7 @@ - <?= $this->section('title', '') ?> | URL shortener + <?= $this->section('title', '') ?> | Shlink From 68919c19b812d9aa31884cfb020926d65259fec2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 7 Jun 2020 10:33:58 +0200 Subject: [PATCH 02/55] Added deprecation in BodyParserMiddleware --- module/Rest/src/Middleware/BodyParserMiddleware.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index 955041d9..c7e99121 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -19,12 +19,6 @@ use function trim; class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface { - /** - * Process an incoming server request and return a response, optionally delegating - * to the next middleware component to create the response. - * - * - */ public function process(Request $request, RequestHandlerInterface $handler): Response { $method = $request->getMethod(); @@ -51,8 +45,6 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac return $handler->handle($this->parseFromUrlEncoded($request)); } - /** - */ private function getRequestContentType(Request $request): string { $contentType = $request->getHeaderLine('Content-type'); @@ -60,8 +52,6 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac return trim(array_shift($contentTypes)); } - /** - */ private function parseFromJson(Request $request): Request { $rawBody = (string) $request->getBody(); @@ -74,6 +64,7 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac } /** + * @deprecated To be removed on Shlink v3.0.0, supporting only JSON requests. */ private function parseFromUrlEncoded(Request $request): Request { From 2867a9b7b0c10c51d1043768be5c67fa4ef35ab6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 7 Jun 2020 11:23:32 +0200 Subject: [PATCH 03/55] Added commands to run infection checks on database tests --- CHANGELOG.md | 2 +- composer.json | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6192481f..1b58f000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Changed -* *Nothing* +* [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests. #### Deprecated diff --git a/composer.json b/composer.json index ef9e0e92..777463b2 100644 --- a/composer.json +++ b/composer.json @@ -115,27 +115,31 @@ "@test:db" ], "test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox", - "test:unit:ci": "@test:unit --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/junit.xml", + "test:unit:ci": "@test:unit --coverage-clover=build/clover.xml --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", "test:db": [ - "@test:db:sqlite", + "@test:db:sqlite:ci", "@test:db:mysql", "@test:db:maria", "@test:db:postgres", "@test:db:ms" ], - "test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-db.cov --testdox -c phpunit-db.xml", + "test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml", + "test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml", "test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite", "test:db:maria": "DB_DRIVER=maria composer test:db:sqlite", "test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite", "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:api": "bin/test/run-api-tests.sh", - "test:api:ci": "@test:api --coverage-php build/coverage-api.cov", "test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage", "infect": "infection --threads=4 --min-msi=80 --log-verbosity=default --only-covered", - "infect:ci": "@infect --coverage=build --skip-initial-tests", - "infect:show": "@infect --show-mutations", + "infect:ci:base": "@infect --skip-initial-tests", + "infect:ci": [ + "@infect:ci:base --coverage=build/coverage-unit", + "@infect:ci:base --coverage=build/coverage-db --test-framework-options=--configuration=phpunit-db.xml" + ], "infect:test": [ "@test:unit:ci", + "@test:db:sqlite:ci", "@infect:ci" ], "clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php" @@ -158,7 +162,6 @@ "test:unit:pretty": "Runs unit test suites and generates an HTML code coverage report", "infect": "Checks unit tests quality applying mutation testing", "infect:ci": "Checks unit tests quality applying mutation testing with existing reports and logs", - "infect:show": "Checks unit tests quality applying mutation testing and shows applied mutators", "infect:test": "Checks unit tests quality applying mutation testing", "clean:dev": "Deletes artifacts which are gitignored and could affect dev env" }, From 248209ab41a0a5b26126238d2538d022b9e695ae Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 8 Jun 2020 23:30:19 +0200 Subject: [PATCH 04/55] Updated changelog --- CHANGELOG.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b58f000..6993aeb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +#### Added + +* *Nothing* + +#### Changed + +* [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests. + +#### Deprecated + +* *Nothing* + +#### Removed + +* *Nothing* + +#### Fixed + +* *Nothing* + + ## 2.2.2 - 2020-06-08 #### Added @@ -12,7 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Changed -* [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests. +* *Nothing* #### Deprecated From f476cfc30f110ff84b48fc7158822ef1ae571efc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Jun 2020 17:51:20 +0200 Subject: [PATCH 05/55] Simplified travis configuration, by removing all env vars checks --- .travis.yml | 85 ++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b1c35e6..bf412f7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,21 +6,6 @@ branches: only: - /.*/ -jobs: - fast_finish: true - include: - - name: "Docker publish" - php: '7.4' - if: NOT type = pull_request - env: - - DOCKER_PUBLISH="true" - - name: "CI" - php: '7.4' - env: - - DOCKER_PUBLISH="false" - allow_failures: - - name: "Docker publish" - services: - docker @@ -28,48 +13,56 @@ cache: directories: - $HOME/.composer/cache/files +jobs: + fast_finish: true + include: + - name: "Docker publish" + if: NOT type = pull_request + # Overwrite all common steps that have to be different + before_install: echo "Before install" + install: sudo ./data/infra/ci/install-docker.sh + before_script: echo "Before script" + script: bash ./docker/build + after_success: echo "After success" + - name: "CI" + php: '7.4' + # Deploy release only on smallest supported PHP version + before_deploy: + - rm -f ocular.phar + - if [[ ! -z ${TRAVIS_TAG} && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi + deploy: + - provider: releases + api_key: + secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I= + file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip" + skip_cleanup: true + on: + tags: true + +# Common steps for all jobs before_install: - echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - phpenv config-rm xdebug.ini || return 0 - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then sudo ./data/infra/ci/install-ms-odbc.sh ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms shlink_db shlink_db_postgres shlink_db_maria ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then yes | pecl install pdo_sqlsrv swoole-4.4.18 ; fi + - sudo ./data/infra/ci/install-ms-odbc.sh + - docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms shlink_db shlink_db_postgres shlink_db_maria + - yes | pecl install pdo_sqlsrv swoole-4.4.18 install: - - if [[ "${DOCKER_PUBLISH}" == 'true' ]]; then sudo ./data/infra/ci/install-docker.sh ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then composer self-update ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then composer install --no-interaction --prefer-dist ; fi + - composer self-update + - composer install --no-interaction --prefer-dist before_script: - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then docker-compose exec shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" ; fi + - docker-compose exec shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" - mkdir build - export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep Dockerfile) script: - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then bin/test/run-api-tests.sh --coverage-php build/coverage-api.cov && composer ci ; fi - - if [[ ! -z "${DOCKERFILE_CHANGED}" && "${TRAVIS_PHP_VERSION}" == "7.4" && "${DOCKER_PUBLISH}" == "false" ]]; then docker build -t shlink-docker-image:temp . ; fi - - if [[ "${DOCKER_PUBLISH}" == 'true' ]]; then bash ./docker/build ; fi + - bin/test/run-api-tests.sh --coverage-php build/coverage-api.cov && composer ci + - if [[ ! -z "${DOCKERFILE_CHANGED}" && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then docker build -t shlink-docker-image:temp . ; fi after_success: - rm -f build/clover.xml - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then wget https://phar.phpunit.de/phpcov-7.0.2.phar ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then phpdbg -qrr phpcov-7.0.2.phar merge build --clover build/clover.xml ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then wget https://scrutinizer-ci.com/ocular.phar ; fi - - if [[ "${DOCKER_PUBLISH}" == 'false' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/clover.xml ; fi - -# Before deploying, build dist file for current travis tag -before_deploy: - - rm -f ocular.phar - - if [[ ! -z ${TRAVIS_TAG} && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi - -deploy: - - provider: releases - api_key: - secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I= - file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip" - skip_cleanup: true - on: - all_branches: true - condition: ${DOCKER_PUBLISH} == 'false' - tags: true - php: '7.4' + - wget https://phar.phpunit.de/phpcov-7.0.2.phar + - phpdbg -qrr phpcov-7.0.2.phar merge build --clover build/clover.xml + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover build/clover.xml From e9c64b46b7e2c537b542d04a415595c916dc9353 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Jun 2020 17:54:41 +0200 Subject: [PATCH 06/55] Removed condition from travis that is now implicit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bf412f7d..622338d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ jobs: # Deploy release only on smallest supported PHP version before_deploy: - rm -f ocular.phar - - if [[ ! -z ${TRAVIS_TAG} && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi + - ./build.sh ${TRAVIS_TAG#?} deploy: - provider: releases api_key: From 68db52679b3b78a0cbcca7d0e8c2038caeb87039 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 17 Jun 2020 19:01:56 +0200 Subject: [PATCH 07/55] Added support to serve redirects with status 301 and Cache-Control --- config/autoload/url-shortener.global.php | 3 ++ config/cli-config.php | 3 +- module/Core/config/dependencies.config.php | 1 + module/Core/functions/functions.php | 3 ++ module/Core/src/Action/RedirectAction.php | 30 +++++++++++++--- .../Core/src/Options/UrlShortenerOptions.php | 34 +++++++++++++++++-- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 5ad66bea..ba1f473a 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; return [ @@ -15,6 +16,8 @@ return [ 'anonymize_remote_addr' => true, 'visits_webhooks' => [], 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, + 'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, + 'redirect_cache_lifetime' => 30, ], ]; diff --git a/config/cli-config.php b/config/cli-config.php index c0e80687..71c7a75e 100644 --- a/config/cli-config.php +++ b/config/cli-config.php @@ -4,11 +4,10 @@ declare(strict_types=1); use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\Console\ConsoleRunner; -use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; return (function () { - /** @var ContainerInterface|ServiceManager $container */ + /** @var ContainerInterface $container */ $container = include __DIR__ . '/container.php'; $em = $container->get(EntityManager::class); diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index debf021f..46bf1735 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -76,6 +76,7 @@ return [ Service\ShortUrl\ShortUrlResolver::class, Service\VisitsTracker::class, Options\AppOptions::class, + Options\UrlShortenerOptions::class, 'Logger_Shlink', ], Action\PixelAction::class => [ diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 3016b18c..1fac0bd5 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -6,12 +6,15 @@ namespace Shlinkio\Shlink\Core; use Cake\Chronos\Chronos; use DateTimeInterface; +use Fig\Http\Message\StatusCodeInterface; use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; const DEFAULT_SHORT_CODES_LENGTH = 5; const MIN_SHORT_CODES_LENGTH = 4; +const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND; +const DEFAULT_REDIRECT_CACHE_LIFETIME = 30; const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory'; function generateRandomShortCode(int $length): string diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index e6e1ec4c..72a56096 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -4,18 +4,40 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; +use Fig\Http\Message\StatusCodeInterface; use Laminas\Diactoros\Response\RedirectResponse; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; +use Shlinkio\Shlink\Core\Options; +use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; +use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; +use function sprintf; -class RedirectAction extends AbstractTrackingAction +class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface { + private Options\UrlShortenerOptions $urlShortenerOptions; + + public function __construct( + ShortUrlResolverInterface $urlResolver, + VisitsTrackerInterface $visitTracker, + Options\AppOptions $appOptions, + Options\UrlShortenerOptions $urlShortenerOptions, + ?LoggerInterface $logger = null + ) { + parent::__construct($urlResolver, $visitTracker, $appOptions, $logger); + $this->urlShortenerOptions = $urlShortenerOptions; + } + protected function createSuccessResp(string $longUrl): Response { - // Return a redirect response to the long URL. - // Use a temporary redirect to make sure browsers always hit the server for analytics purposes - return new RedirectResponse($longUrl); + $statusCode = $this->urlShortenerOptions->redirectStatusCode(); + $headers = $statusCode === self::STATUS_FOUND ? [] : [ + 'Cache-Control' => sprintf('private,max-age=%s', $this->urlShortenerOptions->redirectCacheLifetime()), + ]; + + return new RedirectResponse($longUrl, $statusCode, $headers); } protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index 19267ea1..69a06f1f 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -6,20 +6,50 @@ namespace Shlinkio\Shlink\Core\Options; use Laminas\Stdlib\AbstractOptions; +use function Functional\contains; + +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; + class UrlShortenerOptions extends AbstractOptions { protected $__strictMode__ = false; // phpcs:ignore private bool $validateUrl = true; + private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE; + private int $redirectCacheLifetime = 30; public function isUrlValidationEnabled(): bool { return $this->validateUrl; } - protected function setValidateUrl(bool $validateUrl): self + protected function setValidateUrl(bool $validateUrl): void { $this->validateUrl = $validateUrl; - return $this; + } + + public function redirectStatusCode(): int + { + return $this->redirectStatusCode; + } + + protected function setRedirectStatusCode(int $redirectStatusCode): void + { + $this->redirectStatusCode = $this->normalizeRedirectStatusCode($redirectStatusCode); + } + + private function normalizeRedirectStatusCode(int $statusCode): int + { + return contains([301, 302], $statusCode) ? $statusCode : 302; + } + + public function redirectCacheLifetime(): int + { + return $this->redirectCacheLifetime; + } + + protected function setRedirectCacheLifetime(int $redirectCacheLifetime): void + { + $this->redirectCacheLifetime = $redirectCacheLifetime; } } From cb70dc538925a1d5fb2082a6e5230487dca060fe Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 09:20:01 +0200 Subject: [PATCH 08/55] Removed stuff from local config file which already comes on third party config --- config/autoload/swoole.local.php.dist | 9 --------- 1 file changed, 9 deletions(-) diff --git a/config/autoload/swoole.local.php.dist b/config/autoload/swoole.local.php.dist index 0c485690..f30b3610 100644 --- a/config/autoload/swoole.local.php.dist +++ b/config/autoload/swoole.local.php.dist @@ -2,9 +2,6 @@ declare(strict_types=1); -use Laminas\ServiceManager\Factory\InvokableFactory; -use Mezzio\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher; - return [ 'mezzio-swoole' => [ @@ -13,10 +10,4 @@ return [ ], ], - 'dependencies' => [ - 'factories' => [ - InotifyFileWatcher::class => InvokableFactory::class, - ], - ], - ]; From 83cc11030db9fbb62e7fb28abc8c43fdeaa5442d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 09:30:23 +0200 Subject: [PATCH 09/55] Updated changelog --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6993aeb7..b2358670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Added -* *Nothing* +* [#746](https://github.com/shlinkio/shlink/issues/746) Allowed to configure the kind of redirect you want to use for your short URLs. You can either set: + + * `302` redirects: Default behavior. Visitors always hit the server. + * `301` redirects: Better for SEO. Visitors hit the server the first time and then cache the redirect. + + When selecting 301 redirects, you can also configure the time redirects are cached, to mitigate deviations in stats. + +* [#709](https://github.com/shlinkio/shlink/issues/709) Added multi-architecture builds for the docker image. #### Changed @@ -31,7 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Added -* [#709](https://github.com/shlinkio/shlink/issues/709) Added multi-architecture builds for the docker image. +* *Nothing* #### Changed From 0bea843e7f25551f21202c60632218691b87ab7f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 09:50:56 +0200 Subject: [PATCH 10/55] Added test covering how redirects config works --- module/Core/src/Action/RedirectAction.php | 1 + .../Core/src/Options/UrlShortenerOptions.php | 9 ++-- .../Core/test/Action/RedirectActionTest.php | 44 ++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index 72a56096..80fef898 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; + use function sprintf; class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index 69a06f1f..92bb7d07 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -8,6 +8,7 @@ use Laminas\Stdlib\AbstractOptions; use function Functional\contains; +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; class UrlShortenerOptions extends AbstractOptions @@ -16,7 +17,7 @@ class UrlShortenerOptions extends AbstractOptions private bool $validateUrl = true; private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE; - private int $redirectCacheLifetime = 30; + private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME; public function isUrlValidationEnabled(): bool { @@ -40,7 +41,7 @@ class UrlShortenerOptions extends AbstractOptions private function normalizeRedirectStatusCode(int $statusCode): int { - return contains([301, 302], $statusCode) ? $statusCode : 302; + return contains([301, 302], $statusCode) ? $statusCode : DEFAULT_REDIRECT_STATUS_CODE; } public function redirectCacheLifetime(): int @@ -50,6 +51,8 @@ class UrlShortenerOptions extends AbstractOptions protected function setRedirectCacheLifetime(int $redirectCacheLifetime): void { - $this->redirectCacheLifetime = $redirectCacheLifetime; + $this->redirectCacheLifetime = $redirectCacheLifetime > 0 + ? $redirectCacheLifetime + : DEFAULT_REDIRECT_CACHE_LIFETIME; } } diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index f4de05c5..1a6bb617 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -27,16 +27,19 @@ class RedirectActionTest extends TestCase private RedirectAction $action; private ObjectProphecy $urlResolver; private ObjectProphecy $visitTracker; + private Options\UrlShortenerOptions $shortenerOpts; public function setUp(): void { $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->visitTracker = $this->prophesize(VisitsTrackerInterface::class); + $this->shortenerOpts = new Options\UrlShortenerOptions(); $this->action = new RedirectAction( $this->urlResolver->reveal(), $this->visitTracker->reveal(), new Options\AppOptions(['disableTrackParam' => 'foobar']), + $this->shortenerOpts, ); } @@ -48,8 +51,9 @@ class RedirectActionTest extends TestCase { $shortCode = 'abc123'; $shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing'); - $shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, '')) - ->willReturn($shortUrl); + $shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl( + new ShortUrlIdentifier($shortCode, ''), + )->willReturn($shortUrl); $track = $this->visitTracker->track(Argument::cetera())->will(function (): void { }); @@ -110,4 +114,40 @@ class RedirectActionTest extends TestCase $track->shouldNotHaveBeenCalled(); } + + /** + * @test + * @dataProvider provideRedirectConfigs + */ + public function expectedStatusCodeAndCacheIsReturnedBasedOnConfig( + int $configuredStatus, + int $configuredLifetime, + int $expectedStatus, + ?string $expectedCacheControl + ): void { + $this->shortenerOpts->redirectStatusCode = $configuredStatus; + $this->shortenerOpts->redirectCacheLifetime = $configuredLifetime; + + $shortUrl = new ShortUrl('http://domain.com/foo/bar'); + $shortCode = $shortUrl->getShortCode(); + $this->urlResolver->resolveEnabledShortUrl(Argument::cetera())->willReturn($shortUrl); + + $request = (new ServerRequest())->withAttribute('shortCode', $shortCode); + $response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal()); + + $this->assertInstanceOf(Response\RedirectResponse::class, $response); + $this->assertEquals($expectedStatus, $response->getStatusCode()); + $this->assertEquals($response->hasHeader('Cache-Control'), $expectedCacheControl !== null); + $this->assertEquals($response->getHeaderLine('Cache-Control'), $expectedCacheControl ?? ''); + } + + public function provideRedirectConfigs(): iterable + { + yield 'status 302' => [302, 20, 302, null]; + yield 'status over 302' => [400, 20, 302, null]; + yield 'status below 301' => [201, 20, 302, null]; + yield 'status 301 with valid expiration' => [301, 20, 301, 'private,max-age=20']; + yield 'status 301 with zero expiration' => [301, 0, 301, 'private,max-age=30']; + yield 'status 301 with negative expiration' => [301, -20, 301, 'private,max-age=30']; + } } From f2f07be11f31bda6ad3da436d0a3eb013ffadd97 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 11:07:15 +0200 Subject: [PATCH 11/55] Updated to latest installer, supporting redirects customizations --- composer.json | 2 +- config/autoload/installer.global.php | 2 ++ config/autoload/url-shortener.global.php | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 777463b2..997f8558 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "shlinkio/shlink-common": "^3.1.0", "shlinkio/shlink-config": "^1.0", "shlinkio/shlink-event-dispatcher": "^1.4", - "shlinkio/shlink-installer": "^5.0.0", + "shlinkio/shlink-installer": "^5.1.0", "shlinkio/shlink-ip-geolocation": "^1.4", "symfony/console": "^5.1", "symfony/filesystem": "^5.1", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index db1914db..ba0b8332 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -37,6 +37,8 @@ return [ Option\Mercure\MercureJwtSecretConfigOption::class, Option\UrlShortener\GeoLiteLicenseKeyConfigOption::class, Option\UrlShortener\IpAnonymizationConfigOption::class, + Option\UrlShortener\RedirectStatusCodeConfigOption::class, + Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class, ], 'installation_commands' => [ diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index ba1f473a..f27210af 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; @@ -17,7 +18,7 @@ return [ 'visits_webhooks' => [], 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, 'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, - 'redirect_cache_lifetime' => 30, + 'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME, ], ]; From 5c163490c7961a8e3a22229c6cfaf50972378761 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 11:21:37 +0200 Subject: [PATCH 12/55] Allowed new redirect config options to be pased as env vars to the docker image --- docker/README.md | 8 +++++++- docker/config/shlink_in_docker.local.php | 7 ++++++- module/Core/functions/functions.php | 1 + module/Core/src/Config/SimplifiedConfigParser.php | 2 ++ module/Core/src/Options/DeleteShortUrlsOptions.php | 4 +++- module/Core/test/Config/SimplifiedConfigParserTest.php | 4 ++++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docker/README.md b/docker/README.md index 199dc6e2..561bdba0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -174,6 +174,8 @@ This is the complete list of supported env vars: * `MERCURE_INTERNAL_HUB_URL`: An internal URL for a mercure hub. Will be used only when publishing updates to mercure, and does not need to be public. If this is not provided but `MERCURE_PUBLIC_HUB_URL` was, the former one will be used to publish updates. * `MERCURE_JWT_SECRET`: The secret key that was provided to the mercure hub server, in order to be able to generate valid JWTs for publishing/subscribing to that server. * `ANONYMIZE_REMOTE_ADDR`: Tells if IP addresses from visitors should be obfuscated before storing them in the database. Default value is `true`. **Careful!** Setting this to `false` will make your Shlink instance no longer be in compliance with the GDPR and other similar data protection regulations. +* `REDIRECT_STATUS_CODE`: Either **301** or **302**. Used to determine if redirects from short to long URLs should be used with a 301 or 302 status. Defaults to 302. +* `REDIRECT_CACHE_LIFETIME`: Allows to set the amount of seconds that redirects should be cached when redirect status is 301. Default values is 30. An example using all env vars could look like this: @@ -206,6 +208,8 @@ docker run \ -e "MERCURE_INTERNAL_HUB_URL=http://my-mercure-hub.prod.svc.cluster.local" \ -e MERCURE_JWT_SECRET=super_secret_key \ -e ANONYMIZE_REMOTE_ADDR=false \ + -e REDIRECT_STATUS_CODE=301 \ + -e REDIRECT_CACHE_LIFETIME=90 \ shlinkio/shlink:stable ``` @@ -251,7 +255,9 @@ The whole configuration should have this format, but it can be split into multip "mercure_public_hub_url": "https://example.com", "mercure_internal_hub_url": "http://my-mercure-hub.prod.svc.cluster.local", "mercure_jwt_secret": "super_secret_key", - "anonymize_remote_addr": false + "anonymize_remote_addr": false, + "redirect_status_code": 301, + "redirect_cache_lifetime": 90 } ``` diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index b870ccd7..8d3c55fa 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -11,6 +11,9 @@ use function explode; use function Functional\contains; use function Shlinkio\Shlink\Common\env; +use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD; +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH; @@ -104,7 +107,7 @@ return [ 'delete_short_urls' => [ 'check_visits_threshold' => true, - 'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', 15), + 'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD), ], 'entity_manager' => [ @@ -120,6 +123,8 @@ return [ 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true), 'visits_webhooks' => $helper->getVisitsWebhooks(), 'default_short_codes_length' => $helper->getDefaultShortCodesLength(), + 'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE), + 'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME), ], 'not_found_redirects' => $helper->getNotFoundRedirectsConfig(), diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 1fac0bd5..deea5f5f 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -11,6 +11,7 @@ use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; +const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15; const DEFAULT_SHORT_CODES_LENGTH = 5; const MIN_SHORT_CODES_LENGTH = 4; const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND; diff --git a/module/Core/src/Config/SimplifiedConfigParser.php b/module/Core/src/Config/SimplifiedConfigParser.php index 81f05d14..38fbdea3 100644 --- a/module/Core/src/Config/SimplifiedConfigParser.php +++ b/module/Core/src/Config/SimplifiedConfigParser.php @@ -38,6 +38,8 @@ class SimplifiedConfigParser 'mercure_internal_hub_url' => ['mercure', 'internal_hub_url'], 'mercure_jwt_secret' => ['mercure', 'jwt_secret'], 'anonymize_remote_addr' => ['url_shortener', 'anonymize_remote_addr'], + 'redirect_status_code' => ['url_shortener', 'redirect_status_code'], + 'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'], ]; private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ 'delete_short_url_threshold' => [ diff --git a/module/Core/src/Options/DeleteShortUrlsOptions.php b/module/Core/src/Options/DeleteShortUrlsOptions.php index ee7d9dee..9fa7fcf0 100644 --- a/module/Core/src/Options/DeleteShortUrlsOptions.php +++ b/module/Core/src/Options/DeleteShortUrlsOptions.php @@ -6,9 +6,11 @@ namespace Shlinkio\Shlink\Core\Options; use Laminas\Stdlib\AbstractOptions; +use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD; + class DeleteShortUrlsOptions extends AbstractOptions { - private int $visitsThreshold = 15; + private int $visitsThreshold = DEFAULT_DELETE_SHORT_URL_THRESHOLD; private bool $checkVisitsThreshold = true; public function getVisitsThreshold(): int diff --git a/module/Core/test/Config/SimplifiedConfigParserTest.php b/module/Core/test/Config/SimplifiedConfigParserTest.php index 3700b042..9d4bca69 100644 --- a/module/Core/test/Config/SimplifiedConfigParserTest.php +++ b/module/Core/test/Config/SimplifiedConfigParserTest.php @@ -65,6 +65,8 @@ class SimplifiedConfigParserTest extends TestCase 'mercure_internal_hub_url' => 'internal_url', 'mercure_jwt_secret' => 'super_secret_value', 'anonymize_remote_addr' => false, + 'redirect_status_code' => 301, + 'redirect_cache_lifetime' => 90, ]; $expected = [ 'app_options' => [ @@ -94,6 +96,8 @@ class SimplifiedConfigParserTest extends TestCase ], 'default_short_codes_length' => 8, 'anonymize_remote_addr' => false, + 'redirect_status_code' => 301, + 'redirect_cache_lifetime' => 90, ], 'delete_short_urls' => [ From bffc044bc7214dc6bfece0027b804316355fc060 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 20 Jun 2020 11:34:09 +0200 Subject: [PATCH 13/55] Fixed typo --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 561bdba0..9526dd25 100644 --- a/docker/README.md +++ b/docker/README.md @@ -174,7 +174,7 @@ This is the complete list of supported env vars: * `MERCURE_INTERNAL_HUB_URL`: An internal URL for a mercure hub. Will be used only when publishing updates to mercure, and does not need to be public. If this is not provided but `MERCURE_PUBLIC_HUB_URL` was, the former one will be used to publish updates. * `MERCURE_JWT_SECRET`: The secret key that was provided to the mercure hub server, in order to be able to generate valid JWTs for publishing/subscribing to that server. * `ANONYMIZE_REMOTE_ADDR`: Tells if IP addresses from visitors should be obfuscated before storing them in the database. Default value is `true`. **Careful!** Setting this to `false` will make your Shlink instance no longer be in compliance with the GDPR and other similar data protection regulations. -* `REDIRECT_STATUS_CODE`: Either **301** or **302**. Used to determine if redirects from short to long URLs should be used with a 301 or 302 status. Defaults to 302. +* `REDIRECT_STATUS_CODE`: Either **301** or **302**. Used to determine if redirects from short to long URLs should be done with a 301 or 302 status. Defaults to 302. * `REDIRECT_CACHE_LIFETIME`: Allows to set the amount of seconds that redirects should be cached when redirect status is 301. Default values is 30. An example using all env vars could look like this: From 56d690d9a6d56e1f8b7bb9d8da0cc09268744b3e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Jun 2020 12:21:39 +0200 Subject: [PATCH 14/55] Removed references to master branch --- .travis.yml | 2 +- README.md | 8 ++++---- docker/README.md | 2 +- docker/build | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 622338d5..07e500ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,7 @@ install: before_script: - docker-compose exec shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" - mkdir build - - export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep Dockerfile) + - export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/main} --name-only | grep Dockerfile) script: - bin/test/run-api-tests.sh --coverage-php build/coverage-api.cov && composer ci diff --git a/README.md b/README.md index 4a7e25f6..f8a1990a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -![Shlink](https://raw.githubusercontent.com/shlinkio/shlink.io/master/public/images/shlink-hero.png) +![Shlink](https://raw.githubusercontent.com/shlinkio/shlink.io/main/public/images/shlink-hero.png) [![Build Status](https://img.shields.io/travis/shlinkio/shlink.svg?style=flat-square)](https://travis-ci.org/shlinkio/shlink) -[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master) -[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/) +[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/) [![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink) [![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/) -[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/master/LICENSE) +[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE) [![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate) A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain. diff --git a/docker/README.md b/docker/README.md index 9526dd25..89c9565b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -291,6 +291,6 @@ Versioning on this docker image works as follows: * `X.X.X`: when providing a specific version number, the image version will match the shlink version it contains. For example, installing `shlinkio/shlink:1.15.0`, you will get an image containing shlink v1.15.0. * `stable`: always holds the latest stable tag. For example, if latest shlink version is 2.0.0, installing `shlinkio/shlink:stable`, you will get an image containing shlink v2.0.0 -* `latest`: always holds the latest contents in master, and it's considered unstable and not suitable for production. +* `latest`: always holds the latest contents, and it's considered unstable and not suitable for production. > **Important**: The docker image was introduced with shlink v1.15.0, so there are no official images previous to that versions. diff --git a/docker/build b/docker/build index 8ac27d4d..a2855fdf 100755 --- a/docker/build +++ b/docker/build @@ -27,7 +27,7 @@ if [[ ! -z $TRAVIS_TAG ]]; then --platform ${PLATFORMS} \ ${TAGS} . -# If build branch is develop, build latest (on master, when there's no tag, do not build anything) +# If build branch is develop, build latest (on main branch, when there's no tag, do not build anything) elif [[ "$TRAVIS_BRANCH" == 'develop' ]]; then docker buildx build --push \ --platform ${PLATFORMS} \ From b4e58cc1bbfd74b8b3d1c63d4986a3efc44df8f9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 15 Jun 2020 18:35:53 +0200 Subject: [PATCH 15/55] Updated doctrine config for v3 --- composer.json | 2 +- migrations.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 997f8558..86e35f3f 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "cocur/slugify": "^4.0", "doctrine/cache": "^1.9", "doctrine/dbal": "^2.10", - "doctrine/migrations": "^2.2", + "doctrine/migrations": "^3.0.1", "doctrine/orm": "^2.7", "endroid/qr-code": "^3.6", "geoip2/geoip2": "^2.9", diff --git a/migrations.php b/migrations.php index 364f52ad..0341745e 100644 --- a/migrations.php +++ b/migrations.php @@ -4,8 +4,11 @@ declare(strict_types=1); return [ 'name' => 'ShlinkMigrations', - 'migrations_namespace' => 'ShlinkMigrations', - 'table_name' => 'migrations', - 'migrations_directory' => 'data/migrations', + 'migrations_paths' => [ + 'ShlinkMigrations' => 'data/migrations', + ], + 'table_storage' => [ + 'table_name' => 'migrations', + ], 'custom_template' => 'data/migrations_template.txt', ]; From eed353fedf89f92100ef992fae1fc73a80f4cce8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Jun 2020 12:29:56 +0200 Subject: [PATCH 16/55] Updated migration template --- data/migrations_template.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/migrations_template.txt b/data/migrations_template.txt index c7b938f2..482236e6 100644 --- a/data/migrations_template.txt +++ b/data/migrations_template.txt @@ -7,7 +7,7 @@ namespace ; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version extends AbstractMigration +final class extends AbstractMigration { public function up(Schema $schema): void { From 6b3fd2ac83b1dabd1e3af0c1238c09855db9d0d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Jun 2020 13:00:32 +0200 Subject: [PATCH 17/55] Commented out name config option for migrations, since it makes it fail --- migrations.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/migrations.php b/migrations.php index 0341745e..8d11ab91 100644 --- a/migrations.php +++ b/migrations.php @@ -3,7 +3,8 @@ declare(strict_types=1); return [ - 'name' => 'ShlinkMigrations', + +// 'name' => 'ShlinkMigrations', 'migrations_paths' => [ 'ShlinkMigrations' => 'data/migrations', ], @@ -11,4 +12,5 @@ return [ 'table_name' => 'migrations', ], 'custom_template' => 'data/migrations_template.txt', + ]; From f44540f95e60e7cc1277e85cc781d16a229061fe Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Jun 2020 13:01:10 +0200 Subject: [PATCH 18/55] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2358670..4f01eecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Changed * [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests. +* [#790](https://github.com/shlinkio/shlink/issues/790) Updated to doctrine/migrations v3. #### Deprecated From e107aa9ed867e2184e4b6887f87a245265c907ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 23 Jun 2020 19:23:33 +0200 Subject: [PATCH 19/55] Removed commented migrations option --- migrations.php | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations.php b/migrations.php index 8d11ab91..306c1c08 100644 --- a/migrations.php +++ b/migrations.php @@ -4,7 +4,6 @@ declare(strict_types=1); return [ -// 'name' => 'ShlinkMigrations', 'migrations_paths' => [ 'ShlinkMigrations' => 'data/migrations', ], From c7c9ab71ff507bb2a2b42f1f793dc752b2272906 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 26 Jun 2020 21:22:54 +0200 Subject: [PATCH 20/55] Created first draft of the contributing file --- CONTRIBUTING.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9dcb330a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing + +This file will guide you through the process of getting to project up and running, in order to provide coding contributions. + +You will also see how to ensure the code fulfills the expected code checks, and how to end up creating a pull request. + +## System dependencies + +The project provides all its dependencies as docker containers through a docker-compose configuration. + +Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/). + +## Setting up the project + +The first thing you need to do is fork the repository, and clone it in your local machine. + +Then you will have to follow these steps: + +* Copy all files with `.local.php.dist` extension from `config/autoload` by removing the dist extension. + + For example the `common.local.php.dist` file should be copied as `common.local.php`. + +* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension. +* Start-up the project by running `docker-compose up`. + + The first time this command is run, it will create several containers that are used during development, and may tike some time. + + It will also create some empty databases and install the project dependencies with composer. + +* Run `./indocker bin/cli db:create` to create an empty database. +* Run `./indocker bin/cli db:migrate` to get database migrations up to date. +* Run `./indocker bin/cli api-key:generate` to get your first API key generated. + +Once you finish this, you will have the project exposed in ports `8080` through nginx+php-fpm and `8000` through swoole. + +> Note: The `indocker` shell script is a helper used to run commands inside the main docker container. + +## Running code checks + +* Run `./indocker composer cs` to check coding styles are fulfilled. +* Run `./indocker composer cs:fix` to fix coding styles (some may not be fixeable from the CLI) +* Run `./indocker composer stan` to check the code with phpstan. This tool is the closest you have to "compile" PHP and verify everything would work as expected. +* Run `./indocker composer test:unit` to run the unit tests. +* Run `./indocker composer test:db` to run integration tests with the database. + + This command runs the same test suite against all supported database engines. If you just want to run one of them, you can add one of `:sqlite`, `:mysql`, `:maria`, `:postgres`, `:mssql` to the command to run just one of them. + + For example, `test:db:postgres`. + +* Run `./indocker composer test:api` to run API E2E tests. For these, the MySQL database engine is used. + +> Note: Due to some limitations in the tooling used by shlink, the testing databases need to exist first, both for db and api tests (except sqlite). +> +> However, they just need to be created empty, with no tables. Also, once created, they are automatically reset before each execution. +> +> The testing database is always called `shlink_testing`. You can create it using the database client of your choice. [DBeaver](https://dbeaver.io/) is a good multi-platform desktop database client which supports all the engines supported by shlink. + +## Pull request process From 035743ef6aaeae3cf085ab56ca05ea9438df4138 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 10:34:26 +0200 Subject: [PATCH 21/55] Added minor imporovements to CONTRIBUTING file --- CONTRIBUTING.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dcb330a..4b335516 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # Contributing -This file will guide you through the process of getting to project up and running, in order to provide coding contributions. +This file will guide you through the process of getting to project up and running, in case you want to provide coding contributions. -You will also see how to ensure the code fulfills the expected code checks, and how to end up creating a pull request. +You will also see how to ensure the code fulfills the expected code checks, and how to creating a pull request. ## System dependencies @@ -23,11 +23,11 @@ Then you will have to follow these steps: * Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension. * Start-up the project by running `docker-compose up`. - The first time this command is run, it will create several containers that are used during development, and may tike some time. + The first time this command is run, it will create several containers that are used during development, so it may take some time. It will also create some empty databases and install the project dependencies with composer. -* Run `./indocker bin/cli db:create` to create an empty database. +* Run `./indocker bin/cli db:create` to create the initial database. * Run `./indocker bin/cli db:migrate` to get database migrations up to date. * Run `./indocker bin/cli api-key:generate` to get your first API key generated. @@ -38,21 +38,24 @@ Once you finish this, you will have the project exposed in ports `8080` through ## Running code checks * Run `./indocker composer cs` to check coding styles are fulfilled. -* Run `./indocker composer cs:fix` to fix coding styles (some may not be fixeable from the CLI) -* Run `./indocker composer stan` to check the code with phpstan. This tool is the closest you have to "compile" PHP and verify everything would work as expected. +* Run `./indocker composer cs:fix` to fix coding styles (some may not be fixable from the CLI) +* Run `./indocker composer stan` to statically analyze the code with [phpstan](https://phpstan.org/). This tool is the closest to "compile" PHP and verify everything would work as expected. * Run `./indocker composer test:unit` to run the unit tests. -* Run `./indocker composer test:db` to run integration tests with the database. +* Run `./indocker composer test:db` to run the database integration tests. - This command runs the same test suite against all supported database engines. If you just want to run one of them, you can add one of `:sqlite`, `:mysql`, `:maria`, `:postgres`, `:mssql` to the command to run just one of them. + This command runs the same test suite against all supported database engines. If you just want to run one of them, you can add one of `:sqlite`, `:mysql`, `:maria`, `:postgres`, `:mssql` at the end of the command. For example, `test:db:postgres`. * Run `./indocker composer test:api` to run API E2E tests. For these, the MySQL database engine is used. +* Run `./indocker composer infect:test` ti run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/). -> Note: Due to some limitations in the tooling used by shlink, the testing databases need to exist first, both for db and api tests (except sqlite). +> Note: Due to some limitations in the tooling used by shlink, the testing databases need to exist beforehand, both for db and api tests (except sqlite). > -> However, they just need to be created empty, with no tables. Also, once created, they are automatically reset before each execution. +> However, they just need to be created empty, with no tables. Also, once created, they are automatically reset before every new execution. > -> The testing database is always called `shlink_testing`. You can create it using the database client of your choice. [DBeaver](https://dbeaver.io/) is a good multi-platform desktop database client which supports all the engines supported by shlink. +> The testing database is always called `shlink_test`. You can create it using the database client of your choice. [DBeaver](https://dbeaver.io/) is a good multi-platform desktop database client which supports all the engines supported by shlink. ## Pull request process + + From d234e114db3c17b16a7243de1bfaa5bef01df9ac Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 10:41:29 +0200 Subject: [PATCH 22/55] Added description on how to create pull requests to CONTRIBUTING file --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b335516..da4b26c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,7 @@ Once you finish this, you will have the project exposed in ports `8080` through * Run `./indocker composer test:api` to run API E2E tests. For these, the MySQL database engine is used. * Run `./indocker composer infect:test` ti run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/). +* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration. > Note: Due to some limitations in the tooling used by shlink, the testing databases need to exist beforehand, both for db and api tests (except sqlite). > @@ -58,4 +59,8 @@ Once you finish this, you will have the project exposed in ports `8080` through ## Pull request process +In order to provide pull requests to this project, you should always start by creating a new branch, where you will make all desired changes. +The base branch should always be `develop`, and the target branch for the pull request should also be `develop`. + +Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci`, or wait for the build to be run automatically after the pull request is created. From bf1c6e3d4333741828d4f84be843d2e7446e7613 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 10:43:43 +0200 Subject: [PATCH 23/55] Referenced CONTRIBUTING doc from README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f8a1990a..cd3ba8ea 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u > This document references Shlink 2.x. If you are using an older version and want to upgrade, follow the [UPGRADE](UPGRADE.md) doc. +> If you are trying to find out how to run the project in development mode or how to provide contributions, read the [CONTRIBUTING](CONTRIBUTING.md) doc. + ## Table of Contents - [Installation](#installation) From 08950f64338581e40997661b2d487a19ae8d44c6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 24 Jun 2020 20:21:05 +0200 Subject: [PATCH 24/55] Replaced UriInterface by string when creating a short URL --- .../ShortUrl/GenerateShortUrlCommand.php | 23 ++++++++----------- module/Core/src/Model/CreateShortUrlData.php | 19 ++++----------- module/Core/src/Service/UrlShortener.php | 5 +--- .../src/Service/UrlShortenerInterface.php | 3 +-- .../Action/ShortUrl/CreateShortUrlAction.php | 3 +-- .../SingleStepCreateShortUrlAction.php | 2 +- 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php index 7369f1f6..06cdd274 100644 --- a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; -use Laminas\Diactoros\Uri; use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; @@ -128,19 +127,15 @@ class GenerateShortUrlCommand extends Command $shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength; try { - $shortUrl = $this->urlShortener->urlToShortCode( - new Uri($longUrl), - $tags, - ShortUrlMeta::fromRawData([ - ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'), - ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'), - ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug, - ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null, - ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'), - ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'), - ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength, - ]), - ); + $shortUrl = $this->urlShortener->urlToShortCode($longUrl, $tags, ShortUrlMeta::fromRawData([ + ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'), + ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'), + ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug, + ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null, + ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'), + ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'), + ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength, + ])); $io->writeln([ sprintf('Processed long URL: %s', $longUrl), diff --git a/module/Core/src/Model/CreateShortUrlData.php b/module/Core/src/Model/CreateShortUrlData.php index 24ed90a6..9b64302d 100644 --- a/module/Core/src/Model/CreateShortUrlData.php +++ b/module/Core/src/Model/CreateShortUrlData.php @@ -4,41 +4,32 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Model; -use Psr\Http\Message\UriInterface; - final class CreateShortUrlData { - private UriInterface $longUrl; + private string $longUrl; private array $tags; private ShortUrlMeta $meta; - public function __construct( - UriInterface $longUrl, - array $tags = [], - ?ShortUrlMeta $meta = null - ) { + public function __construct(string $longUrl, array $tags = [], ?ShortUrlMeta $meta = null) + { $this->longUrl = $longUrl; $this->tags = $tags; $this->meta = $meta ?? ShortUrlMeta::createEmpty(); } - /** - */ - public function getLongUrl(): UriInterface + public function getLongUrl(): string { return $this->longUrl; } /** - * @return array + * @return string[] */ public function getTags(): array { return $this->tags; } - /** - */ public function getMeta(): ShortUrlMeta { return $this->meta; diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 4544bfc0..7892f959 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; use Doctrine\ORM\EntityManagerInterface; -use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; @@ -42,10 +41,8 @@ class UrlShortener implements UrlShortenerInterface * @throws InvalidUrlException * @throws Throwable */ - public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl + public function urlToShortCode(string $url, array $tags, ShortUrlMeta $meta): ShortUrl { - $url = (string) $url; - // First, check if a short URL exists for all provided params $existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta); if ($existingShortUrl !== null) { diff --git a/module/Core/src/Service/UrlShortenerInterface.php b/module/Core/src/Service/UrlShortenerInterface.php index 802eb048..e26530ca 100644 --- a/module/Core/src/Service/UrlShortenerInterface.php +++ b/module/Core/src/Service/UrlShortenerInterface.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; -use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; @@ -17,5 +16,5 @@ interface UrlShortenerInterface * @throws NonUniqueSlugException * @throws InvalidUrlException */ - public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl; + public function urlToShortCode(string $url, array $tags, ShortUrlMeta $meta): ShortUrl; } diff --git a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php index 489d1277..97097808 100644 --- a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; -use Laminas\Diactoros\Uri; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; @@ -28,6 +27,6 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction } $meta = ShortUrlMeta::fromRawData($postData); - return new CreateShortUrlData(new Uri($postData['longUrl']), (array) ($postData['tags'] ?? []), $meta); + return new CreateShortUrlData($postData['longUrl'], (array) ($postData['tags'] ?? []), $meta); } } diff --git a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php index daeb3d04..0a6a45d4 100644 --- a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php @@ -46,6 +46,6 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction ]); } - return new CreateShortUrlData(new Uri($query['longUrl'])); + return new CreateShortUrlData($query['longUrl']); } } From 78b838f6b61ee29d0fe0d45bd412ae088048143b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 11:09:56 +0200 Subject: [PATCH 25/55] Used league/uri to validate URLs including deeplinks, and fixed tests --- composer.json | 1 + .../ShortUrl/GenerateShortUrlCommandTest.php | 3 +-- module/Core/src/Action/AbstractTrackingAction.php | 4 ++-- module/Core/test/Service/UrlShortenerTest.php | 13 ++++++------- .../ShortUrl/SingleStepCreateShortUrlAction.php | 1 - .../Action/ShortUrl/CreateShortUrlActionTest.php | 3 +-- .../ShortUrl/SingleStepCreateShortUrlActionTest.php | 5 ++--- 7 files changed, 13 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 86e35f3f..d733ba89 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "laminas/laminas-servicemanager": "^3.4", "laminas/laminas-stdlib": "^3.2", "lcobucci/jwt": "^4.0@alpha", + "league/uri": "^6.2", "lstrojny/functional-php": "^1.9", "mezzio/mezzio": "^3.2", "mezzio/mezzio-fastroute": "^3.0", diff --git a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php index bcf00acb..689a5e7c 100644 --- a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand; use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -88,7 +87,7 @@ class GenerateShortUrlCommandTest extends TestCase { $shortUrl = new ShortUrl(''); $urlToShortCode = $this->urlShortener->urlToShortCode( - Argument::type(UriInterface::class), + Argument::type('string'), Argument::that(function (array $tags) { Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags); return $tags; diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 4d883794..4bf391c7 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Fig\Http\Message\RequestMethodInterface; -use Laminas\Diactoros\Uri; +use League\Uri\Uri; use Mezzio\Router\Middleware\ImplicitHeadMiddleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -67,7 +67,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string { - $uri = new Uri($shortUrl->getLongUrl()); + $uri = Uri::createFromString($shortUrl->getLongUrl()); $hardcodedQuery = parse_query($uri->getQuery()); if ($disableTrackParam !== null) { unset($currentQuery[$disableTrackParam]); diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index 2c67bf27..f1ef88af 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -9,7 +9,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMException; -use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -65,7 +64,7 @@ class UrlShortenerTest extends TestCase public function urlIsProperlyShortened(): void { $shortUrl = $this->urlShortener->urlToShortCode( - new Uri('http://foobar.com/12345/hello?foo=bar'), + 'http://foobar.com/12345/hello?foo=bar', [], ShortUrlMeta::createEmpty(), ); @@ -89,7 +88,7 @@ class UrlShortenerTest extends TestCase $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $shortUrl = $this->urlShortener->urlToShortCode( - new Uri('http://foobar.com/12345/hello?foo=bar'), + 'http://foobar.com/12345/hello?foo=bar', [], ShortUrlMeta::createEmpty(), ); @@ -112,7 +111,7 @@ class UrlShortenerTest extends TestCase $this->expectException(ORMException::class); $this->urlShortener->urlToShortCode( - new Uri('http://foobar.com/12345/hello?foo=bar'), + 'http://foobar.com/12345/hello?foo=bar', [], ShortUrlMeta::createEmpty(), ); @@ -131,7 +130,7 @@ class UrlShortenerTest extends TestCase $this->expectException(NonUniqueSlugException::class); $this->urlShortener->urlToShortCode( - new Uri('http://foobar.com/12345/hello?foo=bar'), + 'http://foobar.com/12345/hello?foo=bar', [], ShortUrlMeta::fromRawData(['customSlug' => 'custom-slug']), ); @@ -151,7 +150,7 @@ class UrlShortenerTest extends TestCase $findExisting = $repo->findBy(Argument::any())->willReturn([$expected]); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta); + $result = $this->urlShortener->urlToShortCode($url, $tags, $meta); $findExisting->shouldHaveBeenCalledOnce(); $getRepo->shouldHaveBeenCalledOnce(); @@ -235,7 +234,7 @@ class UrlShortenerTest extends TestCase ]); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta); + $result = $this->urlShortener->urlToShortCode($url, $tags, $meta); $this->assertSame($expected, $result); $findExisting->shouldHaveBeenCalledOnce(); diff --git a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php index 0a6a45d4..46385556 100644 --- a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; -use Laminas\Diactoros\Uri; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index 3a343d60..66f1eaaa 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use Cake\Chronos\Chronos; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; -use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -50,7 +49,7 @@ class CreateShortUrlActionTest extends TestCase { $shortUrl = new ShortUrl(''); $shorten = $this->urlShortener->urlToShortCode( - Argument::type(Uri::class), + Argument::type('string'), Argument::type('array'), $expectedMeta, )->willReturn($shortUrl); diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php index 1af4aeba..d63a83b9 100644 --- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php @@ -9,7 +9,6 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; @@ -71,8 +70,8 @@ class SingleStepCreateShortUrlActionTest extends TestCase ]); $findApiKey = $this->apiKeyService->check('abc123')->willReturn(true); $generateShortCode = $this->urlShortener->urlToShortCode( - Argument::that(function (UriInterface $argument) { - Assert::assertEquals('http://foobar.com', (string) $argument); + Argument::that(function (string $argument): string { + Assert::assertEquals('http://foobar.com', $argument); return $argument; }), [], From 2df6e694eaa5f69bb332be25495095dc00543e04 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 11:15:17 +0200 Subject: [PATCH 26/55] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f01eecc..64a22146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this When selecting 301 redirects, you can also configure the time redirects are cached, to mitigate deviations in stats. +* [#734](https://github.com/shlinkio/shlink/issues/734) Added support to redirect to deeplinks and other links with schemas different from `http` and `https`. * [#709](https://github.com/shlinkio/shlink/issues/709) Added multi-architecture builds for the docker image. #### Changed From 156eae56d0b13f087c15de128a0cebaed6b9ff5e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 11:16:59 +0200 Subject: [PATCH 27/55] Fixed typo in contributing doc --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da4b26c6..5f00dd0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ Then you will have to follow these steps: * Run `./indocker bin/cli db:migrate` to get database migrations up to date. * Run `./indocker bin/cli api-key:generate` to get your first API key generated. -Once you finish this, you will have the project exposed in ports `8080` through nginx+php-fpm and `8000` through swoole. +Once you finish this, you will have the project exposed in ports `8000` through nginx+php-fpm and `8080` through swoole. > Note: The `indocker` shell script is a helper used to run commands inside the main docker container. From 509672f4c7d1d36d54705aff4809a0c56de4cd90 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Jun 2020 16:42:17 +0200 Subject: [PATCH 28/55] Added intl to required PHP extensions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd3ba8ea..9d51e389 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u First, make sure the host where you are going to run shlink fulfills these requirements: -* PHP 7.4 or greater with JSON, curl, PDO and gd extensions enabled. +* PHP 7.4 or greater with JSON, curl, PDO, intl and gd extensions enabled. * MySQL, MariaDB, PostgreSQL, Microsoft SQL Server or SQLite. * The web server of your choice with PHP integration (Apache or Nginx recommended). From 73c6c52b2a70f1b2952cf27eda689b17a2c0da91 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Jun 2020 10:06:49 +0200 Subject: [PATCH 29/55] Updated to guzzle 7 --- composer.json | 11 +++++------ module/Core/src/Util/UrlValidator.php | 1 + module/Core/test/Util/UrlValidatorTest.php | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index d733ba89..f1a072fe 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "doctrine/orm": "^2.7", "endroid/qr-code": "^3.6", "geoip2/geoip2": "^2.9", - "guzzlehttp/guzzle": "^6.5.1", + "guzzlehttp/guzzle": "^7.0", "laminas/laminas-config": "^3.3", "laminas/laminas-config-aggregator": "^1.1", "laminas/laminas-dependency-plugin": "^1.0", @@ -50,18 +50,17 @@ "predis/predis": "^1.1", "pugx/shortid-php": "^0.5", "ramsey/uuid": "^3.9", - "shlinkio/shlink-common": "^3.1.0", + "shlinkio/shlink-common": "^3.2.0", "shlinkio/shlink-config": "^1.0", "shlinkio/shlink-event-dispatcher": "^1.4", "shlinkio/shlink-installer": "^5.1.0", - "shlinkio/shlink-ip-geolocation": "^1.4", + "shlinkio/shlink-ip-geolocation": "^1.5", "symfony/console": "^5.1", "symfony/filesystem": "^5.1", "symfony/lock": "^5.1", "symfony/mercure": "^0.3.0", "symfony/process": "^5.1", - "symfony/string": "^5.1", - "symfony/translation-contracts": "^2.1" + "symfony/string": "^5.1" }, "require-dev": { "devster/ubench": "^2.0", @@ -72,7 +71,7 @@ "phpunit/phpunit": "~9.0.1", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.1.0", - "shlinkio/shlink-test-utils": "^1.4", + "shlinkio/shlink-test-utils": "^1.5", "symfony/var-dumper": "^5.0" }, "autoload": { diff --git a/module/Core/src/Util/UrlValidator.php b/module/Core/src/Util/UrlValidator.php index 8d8cd072..01885446 100644 --- a/module/Core/src/Util/UrlValidator.php +++ b/module/Core/src/Util/UrlValidator.php @@ -37,6 +37,7 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface try { $this->httpClient->request(self::METHOD_GET, $url, [ RequestOptions::ALLOW_REDIRECTS => ['max' => self::MAX_REDIRECTS], + RequestOptions::IDN_CONVERSION => true, ]); } catch (GuzzleException $e) { throw InvalidUrlException::fromUrl($url, $e); diff --git a/module/Core/test/Util/UrlValidatorTest.php b/module/Core/test/Util/UrlValidatorTest.php index 50b70961..a20ed693 100644 --- a/module/Core/test/Util/UrlValidatorTest.php +++ b/module/Core/test/Util/UrlValidatorTest.php @@ -48,7 +48,10 @@ class UrlValidatorTest extends TestCase $request = $this->httpClient->request( RequestMethodInterface::METHOD_GET, $expectedUrl, - [RequestOptions::ALLOW_REDIRECTS => ['max' => 15]], + [ + RequestOptions::ALLOW_REDIRECTS => ['max' => 15], + RequestOptions::IDN_CONVERSION => true, + ], )->willReturn(new Response()); $this->urlValidator->validateUrl($expectedUrl); From 554a66503f154b0b47cd33764d8d300858f18ea7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Jun 2020 10:07:43 +0200 Subject: [PATCH 30/55] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a22146..b83747d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests. * [#790](https://github.com/shlinkio/shlink/issues/790) Updated to doctrine/migrations v3. +* [#798](https://github.com/shlinkio/shlink/issues/798) Updated to guzzlehttp/guzzle v7. #### Deprecated From a448972e3c7d8d9db82f1ec9304a205a0c50eeb3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 1 Jul 2020 16:35:25 +0200 Subject: [PATCH 31/55] Added project tests section to the CONTRIBUTING file --- CONTRIBUTING.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f00dd0d..f52c44ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,28 @@ Once you finish this, you will have the project exposed in ports `8000` through > Note: The `indocker` shell script is a helper used to run commands inside the main docker container. +## Project tests + +In order to ensure stability and no regressions are introduced while developing new features, this project has three types of tests. + +* **Unit tests**: These are the simplest to run, and usually test individual pieces of code, replacing any external dependency by mocks. + + The code coverage of unit tests is pretty high, and only entity repositories are excluded because of their nature. + +* **Database tests**: These are integration tests that run against a real database, and only cover entity repositories. + + Its purpose is to verify all the database queries behave as expected and return what's expected. + + The project provides some tooling to run them against any of the supported database engines. + +* **API tests**: These are E2E tests that spin up an instance of the app and test it from the outside, by interacting with the REST API. + + These are the best tests to catch regressions, and to verify everything interacts as expected. + +* **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line* + +Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better. + ## Running code checks * Run `./indocker composer cs` to check coding styles are fulfilled. From dd5dcf6ec1086df52dd4566f7c311c641b0f74d6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 1 Jul 2020 16:36:50 +0200 Subject: [PATCH 32/55] Fixed typo --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f52c44ec..1fcd19b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ Once you finish this, you will have the project exposed in ports `8000` through ## Project tests -In order to ensure stability and no regressions are introduced while developing new features, this project has three types of tests. +In order to ensure stability and no regressions are introduced while developing new features, this project has different types of tests. * **Unit tests**: These are the simplest to run, and usually test individual pieces of code, replacing any external dependency by mocks. @@ -53,6 +53,8 @@ In order to ensure stability and no regressions are introduced while developing These are the best tests to catch regressions, and to verify everything interacts as expected. + They use MySQL as the database engine, and include some fixtures that ensure the same data exists at the beginning of the execution. + * **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line* Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better. From 742e2d724e1b9ff7fcf319bf9675ca591ff70bbf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Jul 2020 09:28:31 +0200 Subject: [PATCH 33/55] Updated comment on issue templates --- .github/ISSUE_TEMPLATE.md | 5 +++-- .github/ISSUE_TEMPLATE/Bug.md | 5 +++-- .github/ISSUE_TEMPLATE/Feature_Request.md | 5 +++-- .github/ISSUE_TEMPLATE/Question_Support.md | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6cad4bc8..36d052d5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,7 @@ diff --git a/.github/ISSUE_TEMPLATE/Bug.md b/.github/ISSUE_TEMPLATE/Bug.md index 25a433c2..17351fe7 100644 --- a/.github/ISSUE_TEMPLATE/Bug.md +++ b/.github/ISSUE_TEMPLATE/Bug.md @@ -5,9 +5,10 @@ labels: bug ---