Compare commits

..

36 Commits

Author SHA1 Message Date
Alejandro Celaya
a30f796100 Merge pull request #743 from acelaya-forks/feature/geolite-license
Feature/geolite license
2020-04-30 19:34:44 +02:00
Alejandro Celaya
93a2c83652 Enabled GeoLite installer config option 2020-04-29 20:31:06 +02:00
Alejandro Celaya
4d4423413d Added GEOLITE_LICENSE_KEY env var to basic docker example, to encourage using it 2020-04-29 19:44:08 +02:00
Alejandro Celaya
a1c74c4038 Updated changelog 2020-04-29 19:31:10 +02:00
Alejandro Celaya
f71bb5e307 Added support for GEOLITE_LICENSE_KEY env var for docker image 2020-04-29 19:27:35 +02:00
Alejandro Celaya
9190996e54 Added support for geolite_license_key config option 2020-04-29 19:26:34 +02:00
Alejandro Celaya
af8b6b7f96 Documented how to pass a GEOLITE license key 2020-04-29 19:24:18 +02:00
Alejandro Celaya
e775b0f12f Merge pull request #722 from shlinkio/develop
Release 2.1.3
2020-04-09 12:50:46 +02:00
Alejandro Celaya
3ee5853b32 Merge pull request #721 from acelaya-forks/feature/qr-code-links
Feature/qr code links
2020-04-09 12:40:05 +02:00
Alejandro Celaya
832a24e4c7 Updated changelog 2020-04-09 12:33:00 +02:00
Alejandro Celaya
551368c30d Ensured QR code action respects configured domain 2020-04-09 12:31:03 +02:00
Alejandro Celaya
9f24b8eb76 Merge pull request #720 from acelaya-forks/feature/db-conn-recovery-task-workers
Feature/db conn recovery task workers
2020-04-09 12:01:47 +02:00
Alejandro Celaya
4c83ae2b22 Updated changelog 2020-04-09 11:55:47 +02:00
Alejandro Celaya
28e0fb049b Added check to ensure DB connection is gracefully recovered on swoole task workers 2020-04-09 11:54:54 +02:00
Alejandro Celaya
f79a369884 Merge pull request #719 from acelaya-forks/feature/handle-HEAD-requests
Feature/handle head requests
2020-04-09 00:06:28 +02:00
Alejandro Celaya
34c7b870a7 Removed unnecessary service registration, as it comes preregistered from third party config provider 2020-04-08 23:56:39 +02:00
Alejandro Celaya
ec9f874bb9 Updated changelog 2020-04-08 23:53:23 +02:00
Alejandro Celaya
1980d35691 Ensured redirect requests are not tracked when request is performed using method HEAD 2020-04-08 23:51:57 +02:00
Alejandro Celaya
ec8cbf82e5 Added HEAD request implicit handling 2020-04-08 17:27:26 +02:00
Alejandro Celaya
2b1011de52 Merge pull request #714 from acelaya-forks/feature/metadata-cache-clear
Feature/metadata cache clear
2020-04-06 21:08:46 +02:00
Alejandro Celaya
fa9ace83ad Fixed incorrect use of tilde 2020-04-06 20:59:10 +02:00
Alejandro Celaya
a9a53a9652 Ensured entities metadata cache is cleared during installation and docker start-up 2020-04-06 20:52:33 +02:00
Alejandro Celaya
afca8b2a62 Merge pull request #704 from shlinkio/develop
Release v2.1.2
2020-03-29 13:23:41 +02:00
Alejandro Celaya
daeb293fb9 Merge pull request #703 from acelaya-forks/feature/infection-0.16
Feature/infection 0.16
2020-03-29 13:14:07 +02:00
Alejandro Celaya
1ca50a4a8a Updated changelog 2020-03-29 13:08:21 +02:00
Alejandro Celaya
c6602a81ab Updated to infection 0.16 2020-03-29 13:07:27 +02:00
Alejandro Celaya
46da0e7824 Merge pull request #702 from acelaya-forks/feature/fix-tags-filtering
Feature/fix tags filtering
2020-03-29 13:00:06 +02:00
Alejandro Celaya
e790a38cea Updated changelog 2020-03-29 12:54:09 +02:00
Alejandro Celaya
11879ea377 Ensured tags are not sluggified when using them to filter short URL lists 2020-03-29 12:51:39 +02:00
Alejandro Celaya
7105add009 Merge pull request #701 from acelaya-forks/feature/fix-migration
Fixed query in migration for postgres
2020-03-29 12:25:19 +02:00
Alejandro Celaya
af61fdb52d Updated changelog 2020-03-29 12:15:52 +02:00
Alejandro Celaya
2b4fc354db Fixed query in migration for postgres 2020-03-29 12:13:52 +02:00
Alejandro Celaya
5b72001a8c Merge pull request #699 from shlinkio/develop
V2.1.1
2020-03-28 20:14:35 +01:00
Alejandro Celaya
7c79906ac4 Merge pull request #698 from acelaya-forks/feature/2.1.1
Feature/2.1.1
2020-03-28 20:03:09 +01:00
Alejandro Celaya
e30a724529 Make sure dist files include the htaccess file 2020-03-28 19:54:02 +01:00
Alejandro Celaya
73f97ea874 Ensured releases are published to github before the docker image is built 2020-03-28 19:48:06 +01:00
23 changed files with 224 additions and 171 deletions

View File

@@ -48,12 +48,6 @@ before_deploy:
- if [[ ! -z $TRAVIS_TAG && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi - if [[ ! -z $TRAVIS_TAG && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi
deploy: deploy:
- provider: script
script: bash ./docker/build
on:
all_branches: true
condition: $TRAVIS_PULL_REQUEST == 'false'
php: '7.4'
- provider: releases - provider: releases
api_key: api_key:
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I= secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
@@ -62,3 +56,9 @@ deploy:
on: on:
tags: true tags: true
php: '7.4' php: '7.4'
- provider: script
script: bash ./docker/build
on:
all_branches: true
condition: $TRAVIS_PULL_REQUEST == 'false'
php: '7.4'

View File

@@ -4,6 +4,103 @@ 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). The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## 2.1.4 - 2020-04-30
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#742](https://github.com/shlinkio/shlink/issues/742) Allowed a custom GeoLite2 license key to be provided, in order to avoid download limits.
## 2.1.3 - 2020-04-09
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#712](https://github.com/shlinkio/shlink/issues/712) Fixed app set-up not clearing entities metadata cache.
* [#711](https://github.com/shlinkio/shlink/issues/711) Fixed `HEAD` requests returning a duplicated `Content-Length` header.
* [#716](https://github.com/shlinkio/shlink/issues/716) Fixed Twitter not properly displaying preview for final long URL.
* [#717](https://github.com/shlinkio/shlink/issues/717) Fixed DB connection expiring on task workers when using swoole.
* [#705](https://github.com/shlinkio/shlink/issues/705) Fixed how the short URL domain is inferred when generating QR codes, making sure the configured domain is respected even if the request is performed using a different one, and only when a custom domain is used, then that one is used instead.
## 2.1.2 - 2020-03-29
#### Added
* *Nothing*
#### Changed
* [#696](https://github.com/shlinkio/shlink/issues/696) Updated to infection v0.16.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#700](https://github.com/shlinkio/shlink/issues/700) Fixed migration not working with postgres.
* [#690](https://github.com/shlinkio/shlink/issues/690) Fixed tags being incorrectly sluggified when filtering short URL lists, making results not be the expected.
## 2.1.1 - 2020-03-28
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#697](https://github.com/shlinkio/shlink/issues/697) Recovered `.htaccess` file that was unintentionally removed in v2.1.0, making Shlink unusable with Apache.
## 2.1.0 - 2020-03-28 ## 2.1.0 - 2020-03-28
#### Added #### Added

View File

@@ -19,6 +19,7 @@ mkdir -p "${builtcontent}"
rsync -av * "${builtcontent}" \ rsync -av * "${builtcontent}" \
--exclude=*docker* \ --exclude=*docker* \
--exclude=Dockerfile \ --exclude=Dockerfile \
--include=.htaccess \
--exclude-from=./.dockerignore --exclude-from=./.dockerignore
cd "${builtcontent}" cd "${builtcontent}"

View File

@@ -52,7 +52,7 @@
"shlinkio/shlink-common": "^3.0", "shlinkio/shlink-common": "^3.0",
"shlinkio/shlink-config": "^1.0", "shlinkio/shlink-config": "^1.0",
"shlinkio/shlink-event-dispatcher": "^1.4", "shlinkio/shlink-event-dispatcher": "^1.4",
"shlinkio/shlink-installer": "^4.3.1", "shlinkio/shlink-installer": "^4.4.0",
"shlinkio/shlink-ip-geolocation": "^1.4", "shlinkio/shlink-ip-geolocation": "^1.4",
"symfony/console": "^5.0", "symfony/console": "^5.0",
"symfony/filesystem": "^5.0", "symfony/filesystem": "^5.0",
@@ -63,9 +63,9 @@
"devster/ubench": "^2.0", "devster/ubench": "^2.0",
"dms/phpunit-arraysubset-asserts": "^0.2.0", "dms/phpunit-arraysubset-asserts": "^0.2.0",
"eaglewu/swoole-ide-helper": "dev-master", "eaglewu/swoole-ide-helper": "dev-master",
"infection/infection": "^0.15.0", "infection/infection": "^0.16.1",
"phpstan/phpstan": "^0.12.3", "phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^9.0.1", "phpunit/phpunit": "~9.0.1",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.1.0", "shlinkio/php-coding-standard": "~2.1.0",
"shlinkio/shlink-test-utils": "^1.4", "shlinkio/shlink-test-utils": "^1.4",
@@ -135,7 +135,7 @@
"test:api:ci": "@test:api --coverage-php build/coverage-api.cov", "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", "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": "infection --threads=4 --min-msi=80 --log-verbosity=default --only-covered",
"infect:ci": "@infect --coverage=build", "infect:ci": "@infect --coverage=build --skip-initial-tests",
"infect:show": "@infect --show-mutations", "infect:show": "@infect --show-mutations",
"infect:test": [ "infect:test": [
"@test:unit:ci", "@test:unit:ci",

View File

@@ -31,6 +31,7 @@ return [
Option\WebWorkerNumConfigOption::class, Option\WebWorkerNumConfigOption::class,
Option\RedisServersConfigOption::class, Option\RedisServersConfigOption::class,
Option\ShortCodeLengthOption::class, Option\ShortCodeLengthOption::class,
Option\GeoLiteLicenseKeyConfigOption::class,
], ],
'installation_commands' => [ 'installation_commands' => [

View File

@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use Laminas\Stratigility\Middleware\ErrorHandler; use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio; use Mezzio\Helper;
use Mezzio\ProblemDetails; use Mezzio\ProblemDetails;
use Mezzio\Router;
use PhpMiddleware\RequestId\RequestIdMiddleware; use PhpMiddleware\RequestId\RequestIdMiddleware;
return [ return [
@@ -14,7 +15,7 @@ return [
'middleware_pipeline' => [ 'middleware_pipeline' => [
'error-handler' => [ 'error-handler' => [
'middleware' => [ 'middleware' => [
Mezzio\Helper\ContentLengthMiddleware::class, Helper\ContentLengthMiddleware::class,
ErrorHandler::class, ErrorHandler::class,
], ],
], ],
@@ -35,14 +36,15 @@ return [
'routing' => [ 'routing' => [
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\RouteMiddleware::class, Router\Middleware\RouteMiddleware::class,
Router\Middleware\ImplicitHeadMiddleware::class,
], ],
], ],
'rest' => [ 'rest' => [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\ImplicitOptionsMiddleware::class, Router\Middleware\ImplicitOptionsMiddleware::class,
Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\BodyParserMiddleware::class,
Rest\Middleware\AuthenticationMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class,
], ],
@@ -50,7 +52,7 @@ return [
'dispatch' => [ 'dispatch' => [
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\DispatchMiddleware::class, Router\Middleware\DispatchMiddleware::class,
], ],
], ],
@@ -67,4 +69,5 @@ return [
], ],
], ],
], ],
]; ];

View File

@@ -22,15 +22,16 @@ final class Version20200323190014 extends AbstractMigration
{ {
$qb = $this->connection->createQueryBuilder(); $qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations') $qb->update('visit_locations')
->set('is_empty', true) ->set('is_empty', ':isEmpty')
->where($qb->expr()->eq('country_code', ':empty')) ->where($qb->expr()->eq('country_code', ':emptyString'))
->andWhere($qb->expr()->eq('country_name', ':empty')) ->andWhere($qb->expr()->eq('country_name', ':emptyString'))
->andWhere($qb->expr()->eq('region_name', ':empty')) ->andWhere($qb->expr()->eq('region_name', ':emptyString'))
->andWhere($qb->expr()->eq('city_name', ':empty')) ->andWhere($qb->expr()->eq('city_name', ':emptyString'))
->andWhere($qb->expr()->eq('timezone', ':empty')) ->andWhere($qb->expr()->eq('timezone', ':emptyString'))
->andWhere($qb->expr()->eq('lat', 0)) ->andWhere($qb->expr()->eq('lat', 0))
->andWhere($qb->expr()->eq('lon', 0)) ->andWhere($qb->expr()->eq('lon', 0))
->setParameter('empty', '') ->setParameter('isEmpty', true)
->setParameter('emptyString', '')
->execute(); ->execute();
} }

View File

@@ -18,7 +18,7 @@ It also expects these two env vars to be provided, in order to properly generate
So based on this, to run shlink on a local docker service, you should run a command like this: So based on this, to run shlink on a local docker service, you should run a command like this:
```bash ```bash
docker run --name shlink -p 8080:8080 -e SHORT_DOMAIN_HOST=doma.in -e SHORT_DOMAIN_SCHEMA=https shlinkio/shlink:stable docker run --name shlink -p 8080:8080 -e SHORT_DOMAIN_HOST=doma.in -e SHORT_DOMAIN_SCHEMA=https -e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 shlinkio/shlink:stable
``` ```
### Interact with shlink's CLI on a running container. ### Interact with shlink's CLI on a running container.
@@ -121,6 +121,8 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink. In the future, these redis servers could be used for other caching operations performed by shlink.
* `GEOLITE_LICENSE_KEY`: The license key used to download new GeoLite2 database files. This is not mandatory, as a default license key is provided, but it is **strongly recommended** that you provide your own. Go to [https://shlink.io/documentation/geolite-license-key](https://shlink.io/documentation/geolite-license-key) to know how to generate it.
An example using all env vars could look like this: An example using all env vars could look like this:
```bash ```bash
@@ -147,6 +149,7 @@ docker run \
-e TASK_WORKER_NUM=32 \ -e TASK_WORKER_NUM=32 \
-e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \ -e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \
-e DEFAULT_SHORT_CODES_LENGTH=6 \ -e DEFAULT_SHORT_CODES_LENGTH=6 \
-e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \
shlinkio/shlink:stable shlinkio/shlink:stable
``` ```
@@ -187,7 +190,8 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc", "password": "123abc",
"host": "something.rds.amazonaws.com", "host": "something.rds.amazonaws.com",
"port": "3306" "port": "3306"
} },
"geolite_license_key": "kjh23ljkbndskj345"
} }
``` ```

View File

@@ -147,4 +147,8 @@ return [
], ],
], ],
'geolite2' => [
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'),
],
]; ];

View File

@@ -12,6 +12,9 @@ php bin/cli db:migrate -n -q
echo "Generating proxies..." echo "Generating proxies..."
php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q
echo "Clearing entities cache..."
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n -q
# When restarting the container, swoole might think it is already in execution # When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0 # This forces the app to be started every second until the exit code is 0
until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done

View File

@@ -4,9 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core; namespace Shlinkio\Shlink\Core;
use Doctrine\Common\Cache\Cache;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Router\RouterInterface;
use Mezzio\Template\TemplateRendererInterface; use Mezzio\Template\TemplateRendererInterface;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Shlinkio\Shlink\Core\Domain\Resolver; use Shlinkio\Shlink\Core\Domain\Resolver;
@@ -39,8 +37,6 @@ return [
Action\PixelAction::class => ConfigAbstractFactory::class, Action\PixelAction::class => ConfigAbstractFactory::class,
Action\QrCodeAction::class => ConfigAbstractFactory::class, Action\QrCodeAction::class => ConfigAbstractFactory::class,
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class, Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class,
], ],
], ],
@@ -81,13 +77,11 @@ return [
'Logger_Shlink', 'Logger_Shlink',
], ],
Action\QrCodeAction::class => [ Action\QrCodeAction::class => [
RouterInterface::class,
Service\ShortUrl\ShortUrlResolver::class, Service\ShortUrl\ShortUrlResolver::class,
'config.url_shortener.domain',
'Logger_Shlink', 'Logger_Shlink',
], ],
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
Resolver\PersistenceDomainResolver::class => ['em'], Resolver\PersistenceDomainResolver::class => ['em'],
], ],

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
use Fig\Http\Message\RequestMethodInterface as RequestMethod; use Fig\Http\Message\RequestMethodInterface as RequestMethod;
use RKA\Middleware\IpAddress; use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Action; use Shlinkio\Shlink\Core\Action;
use Shlinkio\Shlink\Core\Middleware;
return [ return [
@@ -32,7 +31,6 @@ return [
'name' => Action\QrCodeAction::class, 'name' => Action\QrCodeAction::class,
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]', 'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
'middleware' => [ 'middleware' => [
Middleware\QrCodeCacheMiddleware::class,
Action\QrCodeAction::class, Action\QrCodeAction::class,
], ],
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Fig\Http\Message\RequestMethodInterface;
use Laminas\Diactoros\Uri; use Laminas\Diactoros\Uri;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@@ -24,7 +26,7 @@ use function array_merge;
use function GuzzleHttp\Psr7\build_query; use function GuzzleHttp\Psr7\build_query;
use function GuzzleHttp\Psr7\parse_query; use function GuzzleHttp\Psr7\parse_query;
abstract class AbstractTrackingAction implements MiddlewareInterface abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
{ {
private ShortUrlResolverInterface $urlResolver; private ShortUrlResolverInterface $urlResolver;
private VisitsTrackerInterface $visitTracker; private VisitsTrackerInterface $visitTracker;
@@ -50,14 +52,13 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
$disableTrackParam = $this->appOptions->getDisableTrackParam(); $disableTrackParam = $this->appOptions->getDisableTrackParam();
try { try {
$url = $this->urlResolver->resolveEnabledShortUrl($identifier); $shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
// Track visit to this short code if ($this->shouldTrackRequest($request, $query, $disableTrackParam)) {
if ($disableTrackParam === null || ! array_key_exists($disableTrackParam, $query)) { $this->visitTracker->track($shortUrl, Visitor::fromRequest($request));
$this->visitTracker->track($url, Visitor::fromRequest($request));
} }
return $this->createSuccessResp($this->buildUrlToRedirectTo($url, $query, $disableTrackParam)); return $this->createSuccessResp($this->buildUrlToRedirectTo($shortUrl, $query, $disableTrackParam));
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]); $this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]);
return $this->createErrorResp($request, $handler); return $this->createErrorResp($request, $handler);
@@ -76,6 +77,16 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
return (string) $uri->withQuery(build_query($mergedQuery)); return (string) $uri->withQuery(build_query($mergedQuery));
} }
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
{
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
if ($forwardedMethod === self::METHOD_HEAD) {
return false;
}
return $disableTrackParam === null || ! array_key_exists($disableTrackParam, $query);
}
abstract protected function createSuccessResp(string $longUrl): ResponseInterface; abstract protected function createSuccessResp(string $longUrl): ResponseInterface;
abstract protected function createErrorResp( abstract protected function createErrorResp(

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Endroid\QrCode\QrCode; use Endroid\QrCode\QrCode;
use Mezzio\Router\RouterInterface;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@@ -23,17 +22,17 @@ class QrCodeAction implements MiddlewareInterface
private const MIN_SIZE = 50; private const MIN_SIZE = 50;
private const MAX_SIZE = 1000; private const MAX_SIZE = 1000;
private RouterInterface $router;
private ShortUrlResolverInterface $urlResolver; private ShortUrlResolverInterface $urlResolver;
private array $domainConfig;
private LoggerInterface $logger; private LoggerInterface $logger;
public function __construct( public function __construct(
RouterInterface $router,
ShortUrlResolverInterface $urlResolver, ShortUrlResolverInterface $urlResolver,
array $domainConfig,
?LoggerInterface $logger = null ?LoggerInterface $logger = null
) { ) {
$this->router = $router;
$this->urlResolver = $urlResolver; $this->urlResolver = $urlResolver;
$this->domainConfig = $domainConfig;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
@@ -42,23 +41,19 @@ class QrCodeAction implements MiddlewareInterface
$identifier = ShortUrlIdentifier::fromRedirectRequest($request); $identifier = ShortUrlIdentifier::fromRedirectRequest($request);
try { try {
$this->urlResolver->resolveEnabledShortUrl($identifier); $shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
return $handler->handle($request); return $handler->handle($request);
} }
$path = $this->router->generateUri(RedirectAction::class, ['shortCode' => $identifier->shortCode()]); $qrCode = new QrCode($shortUrl->toString($this->domainConfig));
$size = $this->getSizeParam($request); $qrCode->setSize($this->getSizeParam($request));
$qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery(''));
$qrCode->setSize($size);
$qrCode->setMargin(0); $qrCode->setMargin(0);
return new QrCodeResponse($qrCode); return new QrCodeResponse($qrCode);
} }
/**
*/
private function getSizeParam(Request $request): int private function getSizeParam(Request $request): int
{ {
$size = (int) $request->getAttribute('size', self::DEFAULT_SIZE); $size = (int) $request->getAttribute('size', self::DEFAULT_SIZE);

View File

@@ -33,6 +33,7 @@ class SimplifiedConfigParser
'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'], 'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'],
'visits_webhooks' => ['url_shortener', 'visits_webhooks'], 'visits_webhooks' => ['url_shortener', 'visits_webhooks'],
'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'], 'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'],
'geolite_license_key' => ['geolite2', 'license_key'],
]; ];
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
'delete_short_url_threshold' => [ 'delete_short_url_threshold' => [

View File

@@ -9,6 +9,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManager;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
@@ -41,22 +42,35 @@ class LocateShortUrlVisit
public function __invoke(ShortUrlVisited $shortUrlVisited): void public function __invoke(ShortUrlVisited $shortUrlVisited): void
{ {
// FIXME Temporarily handling DB connection reset here to fix https://github.com/shlinkio/shlink/issues/717
// Remove when https://github.com/shlinkio/shlink-event-dispatcher/issues/23 is implemented
if ($this->em instanceof ReopeningEntityManager) {
$this->em->open();
}
$visitId = $shortUrlVisited->visitId(); $visitId = $shortUrlVisited->visitId();
/** @var Visit|null $visit */ try {
$visit = $this->em->find(Visit::class, $visitId); /** @var Visit|null $visit */
if ($visit === null) { $visit = $this->em->find(Visit::class, $visitId);
$this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [ if ($visit === null) {
'visitId' => $visitId, $this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [
]); 'visitId' => $visitId,
return; ]);
} return;
}
if ($this->downloadOrUpdateGeoLiteDb($visitId)) { if ($this->downloadOrUpdateGeoLiteDb($visitId)) {
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit); $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
} }
$this->eventDispatcher->dispatch(new VisitLocated($visitId)); $this->eventDispatcher->dispatch(new VisitLocated($visitId));
} finally {
// FIXME Temporarily handling DB connection reset here to fix https://github.com/shlinkio/shlink/issues/717
// Remove when https://github.com/shlinkio/shlink-event-dispatcher/issues/23 is implemented
$this->em->getConnection()->close();
$this->em->clear();
}
} }
private function downloadOrUpdateGeoLiteDb(string $visitId): bool private function downloadOrUpdateGeoLiteDb(string $visitId): bool

View File

@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Middleware;
use Doctrine\Common\Cache\Cache;
use Laminas\Diactoros\Response as DiactResp;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class QrCodeCacheMiddleware implements MiddlewareInterface
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* 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
{
$cacheKey = $request->getUri()->getPath();
// If this QR code is already cached, just return it
if ($this->cache->contains($cacheKey)) {
$qrData = $this->cache->fetch($cacheKey);
$response = new DiactResp();
$response->getBody()->write($qrData['body']);
return $response->withHeader('Content-Type', $qrData['content-type']);
}
// If not, call the next middleware and cache it
/** @var Response $resp */
$resp = $handler->handle($request);
$this->cache->save($cacheKey, [
'body' => $resp->getBody()->__toString(),
'content-type' => $resp->getHeaderLine('Content-Type'),
]);
return $resp;
}
}

View File

@@ -39,7 +39,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
$tags = $this->createArrayInput(self::TAGS, false); $tags = $this->createArrayInput(self::TAGS, false);
$tags->getFilterChain()->attach(new Filter\StringToLower()) $tags->getFilterChain()->attach(new Filter\StringToLower())
->attach(new Validation\SluggerFilter()); ->attach(new Filter\PregReplace(['pattern' => '/ /', 'replacement' => '-']));
$this->add($tags); $this->add($tags);
} }
} }

View File

@@ -30,7 +30,7 @@ class QrCodeActionTest extends TestCase
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->action = new QrCodeAction($router->reveal(), $this->urlResolver->reveal()); $this->action = new QrCodeAction($this->urlResolver->reveal(), ['domain' => 'doma.in']);
} }
/** @test */ /** @test */

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use Fig\Http\Message\RequestMethodInterface;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequest;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -89,4 +91,23 @@ class RedirectActionTest extends TestCase
$handle->shouldHaveBeenCalledOnce(); $handle->shouldHaveBeenCalledOnce();
} }
/** @test */
public function trackingIsDisabledWhenRequestIsForwardedFromHead(): void
{
$shortCode = 'abc123';
$shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing');
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
});
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
->withAttribute(
ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE,
RequestMethodInterface::METHOD_HEAD,
);
$this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
$track->shouldNotHaveBeenCalled();
}
} }

View File

@@ -60,6 +60,7 @@ class SimplifiedConfigParserTest extends TestCase
'https://third-party.io/foo', 'https://third-party.io/foo',
], ],
'default_short_codes_length' => 8, 'default_short_codes_length' => 8,
'geolite_license_key' => 'kjh23ljkbndskj345',
]; ];
$expected = [ $expected = [
'app_options' => [ 'app_options' => [
@@ -127,6 +128,10 @@ class SimplifiedConfigParserTest extends TestCase
], ],
], ],
], ],
'geolite2' => [
'license_key' => 'kjh23ljkbndskj345',
],
]; ];
$result = ($this->postProcessor)(array_merge($config, $simplified)); $result = ($this->postProcessor)(array_merge($config, $simplified));

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@@ -37,6 +38,10 @@ class LocateShortUrlVisitTest extends TestCase
{ {
$this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class); $this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class);
$conn = $this->prophesize(Connection::class);
$this->em->getConnection()->willReturn($conn->reveal());
$this->em->clear()->will(function (): void {
});
$this->logger = $this->prophesize(LoggerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class);
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class); $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);

View File

@@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Middleware;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Uri;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware;
class QrCodeCacheMiddlewareTest extends TestCase
{
private QrCodeCacheMiddleware $middleware;
private Cache $cache;
public function setUp(): void
{
$this->cache = new ArrayCache();
$this->middleware = new QrCodeCacheMiddleware($this->cache);
}
/** @test */
public function noCachedPathFallsBackToNextMiddleware(): void
{
$delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledOnce();
$this->middleware->process((new ServerRequest())->withUri(new Uri('/foo/bar')), $delegate->reveal());
$this->assertTrue($this->cache->contains('/foo/bar'));
}
/** @test */
public function cachedPathReturnsCacheContent(): void
{
$isCalled = false;
$uri = (new Uri())->withPath('/foo');
$this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->middleware->process((new ServerRequest())->withUri($uri), $delegate->reveal());
$this->assertFalse($isCalled);
$resp->getBody()->rewind();
$this->assertEquals('the body', $resp->getBody()->getContents());
$this->assertEquals('image/png', $resp->getHeaderLine('Content-Type'));
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
}
}