mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-01 04:33:12 +08:00
Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e775b0f12f | ||
|
|
3ee5853b32 | ||
|
|
832a24e4c7 | ||
|
|
551368c30d | ||
|
|
9f24b8eb76 | ||
|
|
4c83ae2b22 | ||
|
|
28e0fb049b | ||
|
|
f79a369884 | ||
|
|
34c7b870a7 | ||
|
|
ec9f874bb9 | ||
|
|
1980d35691 | ||
|
|
ec8cbf82e5 | ||
|
|
2b1011de52 | ||
|
|
fa9ace83ad | ||
|
|
a9a53a9652 | ||
|
|
afca8b2a62 | ||
|
|
daeb293fb9 | ||
|
|
1ca50a4a8a | ||
|
|
c6602a81ab | ||
|
|
46da0e7824 | ||
|
|
e790a38cea | ||
|
|
11879ea377 | ||
|
|
7105add009 | ||
|
|
af61fdb52d | ||
|
|
2b4fc354db | ||
|
|
5b72001a8c | ||
|
|
7c79906ac4 | ||
|
|
e30a724529 | ||
|
|
73f97ea874 | ||
|
|
09c155b7d3 | ||
|
|
1e2d115768 | ||
|
|
53ba58d7e9 | ||
|
|
2a30afbe7d | ||
|
|
4d39c7041b | ||
|
|
c012b4740d | ||
|
|
55778eb810 | ||
|
|
fb8ab0b5fe | ||
|
|
fcce18b059 | ||
|
|
43a3d469e7 | ||
|
|
f730c24ecb | ||
|
|
b8522b8c17 | ||
|
|
b0d96040be | ||
|
|
5554675d03 | ||
|
|
c88401ef29 | ||
|
|
75f77ed929 | ||
|
|
4539ab2dcf | ||
|
|
9ad0561cac | ||
|
|
774052a983 | ||
|
|
3beb27acc2 | ||
|
|
5432eb7b77 | ||
|
|
181ff16409 | ||
|
|
682a0768b7 | ||
|
|
d29ebb706e | ||
|
|
4e6836c605 | ||
|
|
59c0d36c0b | ||
|
|
e10b2884c0 | ||
|
|
8fb54e815e | ||
|
|
3a14483568 | ||
|
|
fdd8efc12d | ||
|
|
3fef4b4a28 | ||
|
|
cea50a860e | ||
|
|
f9318bb1b3 | ||
|
|
d22f020eb5 | ||
|
|
c556d8123b | ||
|
|
a592833bd0 | ||
|
|
881da3db3b | ||
|
|
32eb9924e5 | ||
|
|
d2c06dd0ab | ||
|
|
75b8ed813f | ||
|
|
f811002c2b | ||
|
|
ca1b17863c | ||
|
|
644f5be6fe | ||
|
|
65fbb1dfb3 | ||
|
|
8597966187 | ||
|
|
6ddd70d21d | ||
|
|
d32112fe7e | ||
|
|
da858f0353 | ||
|
|
ba8b041698 | ||
|
|
d9fee5582a | ||
|
|
c9f17d54ee | ||
|
|
f5c1e12db4 | ||
|
|
18ceafeb60 | ||
|
|
67e93a6874 | ||
|
|
f6a83a3062 | ||
|
|
8a0e902bdd | ||
|
|
590fc3fc92 | ||
|
|
0d54b7696f | ||
|
|
6b1dadc35c | ||
|
|
86009543ed | ||
|
|
b728a78673 | ||
|
|
fb89cb80ac | ||
|
|
d0a986dd5a | ||
|
|
bb231e668b | ||
|
|
f53fa5c90f | ||
|
|
1f3e0d1f73 | ||
|
|
33a404f051 | ||
|
|
51e130c7a0 | ||
|
|
343ee04acb | ||
|
|
9372d1739a | ||
|
|
13555366e3 | ||
|
|
8162dafe16 | ||
|
|
0b6602b275 | ||
|
|
2cf9f64e8e | ||
|
|
37c0a813db | ||
|
|
a9269811dc | ||
|
|
0b353737ea | ||
|
|
a3fc1513e1 | ||
|
|
5886d73093 | ||
|
|
12adce9ac2 | ||
|
|
d8cbf0512b | ||
|
|
2bb2c2cde3 | ||
|
|
27fd9c5988 | ||
|
|
542673fcb0 | ||
|
|
e60d80bb16 | ||
|
|
bb9e57fa8b | ||
|
|
1d4bea68af | ||
|
|
d2f9f5fd5e | ||
|
|
f13c3364eb | ||
|
|
ac04bedead | ||
|
|
67a66cefa6 | ||
|
|
43db066cb4 | ||
|
|
faec758fba | ||
|
|
ccec6e03aa | ||
|
|
3f08b38558 | ||
|
|
1ee5f64738 | ||
|
|
d22169803f | ||
|
|
57807c4360 | ||
|
|
6e1d07b0cc | ||
|
|
0c0349fa39 | ||
|
|
8d8a0f2484 | ||
|
|
8ff913aaf2 | ||
|
|
f7d54abb2b | ||
|
|
ce990c67e3 | ||
|
|
907b8453c6 | ||
|
|
8a0ba11f79 | ||
|
|
0c1ecd3caa | ||
|
|
c07c37f7bd | ||
|
|
fe652c67f4 | ||
|
|
297985cf01 | ||
|
|
10f79ec01d | ||
|
|
e87d4d61bc | ||
|
|
e58f2a384e | ||
|
|
881002634a | ||
|
|
aa80c2bb82 | ||
|
|
75cd9774b7 | ||
|
|
1a8e4cdfd7 | ||
|
|
6858dc4785 | ||
|
|
5d1d9dcac3 | ||
|
|
732bb06c62 | ||
|
|
5f00d8b732 | ||
|
|
a3ff545d43 | ||
|
|
279bd12a2d | ||
|
|
1b2a0d674f | ||
|
|
fd82de31c0 | ||
|
|
327d35fe57 | ||
|
|
e18187f04e | ||
|
|
bd2f488e2c | ||
|
|
96350c8b8f | ||
|
|
a737eed5c5 | ||
|
|
9b2ccaeb7b | ||
|
|
304979273f | ||
|
|
7add41d560 | ||
|
|
51ebe57ac8 | ||
|
|
6ff5a532ea | ||
|
|
fccd92497a | ||
|
|
452bfea088 | ||
|
|
240d2588f9 | ||
|
|
eca7800487 | ||
|
|
b9e58b9300 | ||
|
|
54918db9ef | ||
|
|
b07a603456 | ||
|
|
4fb2c64fa8 | ||
|
|
258c4102be | ||
|
|
b9c7f8e8d4 | ||
|
|
f32e7cc7c4 | ||
|
|
4ebd48b2b0 | ||
|
|
f71bd84a20 | ||
|
|
33b45eb620 | ||
|
|
1f9a912c04 | ||
|
|
45151cdde6 | ||
|
|
8ca45eb388 | ||
|
|
b7a34a6640 | ||
|
|
8ec686f4e2 | ||
|
|
43fc655218 | ||
|
|
f5a30c4c2d | ||
|
|
af1dd78b2c | ||
|
|
fc95986f0e | ||
|
|
c52794aed6 | ||
|
|
15a72e2a88 | ||
|
|
94af588a3c | ||
|
|
0a4f3bc0f5 | ||
|
|
09e3464426 | ||
|
|
7fcc4ebd57 | ||
|
|
b246815529 | ||
|
|
ad1334f289 | ||
|
|
49bccf9a06 | ||
|
|
1a8bf54e8b | ||
|
|
96bb0321eb | ||
|
|
37f0abf86f | ||
|
|
f9119a38b3 | ||
|
|
8465a9da31 | ||
|
|
b6b0d09647 | ||
|
|
3d2932782d | ||
|
|
fa2fede604 | ||
|
|
3aded3bc5f | ||
|
|
2d4cc912b7 | ||
|
|
c0881f9292 | ||
|
|
302a77b9dd | ||
|
|
2b544ad141 | ||
|
|
36d5e057d0 | ||
|
|
96eb6a80e1 | ||
|
|
9c5f5a46b5 | ||
|
|
886f63d3e4 | ||
|
|
7748dd7cef | ||
|
|
2e0f8067aa | ||
|
|
4fadd523f1 | ||
|
|
abe54c67d8 | ||
|
|
f3f35218c3 | ||
|
|
b9eb9cb6d9 | ||
|
|
40c014b663 | ||
|
|
c96d24cc0b | ||
|
|
824ee55d38 | ||
|
|
86e701dccc | ||
|
|
2151b97bec | ||
|
|
18312b0624 | ||
|
|
a67cf4fb85 | ||
|
|
50100c251e | ||
|
|
1070482629 | ||
|
|
d205405dcc | ||
|
|
16d9c3b93f | ||
|
|
52ecef0311 | ||
|
|
f172146f27 | ||
|
|
ee986912d4 | ||
|
|
0e04968bc2 | ||
|
|
0cf1d8d375 | ||
|
|
339121fbb1 | ||
|
|
f99ca464de | ||
|
|
5c90a7c7a7 | ||
|
|
434b56fa8c | ||
|
|
78b484e657 | ||
|
|
9d36534230 | ||
|
|
bd6243b2ac | ||
|
|
416857e129 | ||
|
|
8cd81d0441 | ||
|
|
bf0d9ab7d9 | ||
|
|
bfeb915cd2 | ||
|
|
a830420d75 | ||
|
|
b904c6d00d | ||
|
|
050050a9eb | ||
|
|
9e48dc4137 | ||
|
|
2bd9bb233c |
@@ -8,17 +8,16 @@ data/migrations_template.txt
|
||||
data/GeoLite2-City.*
|
||||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
**/.gitignore
|
||||
CHANGELOG.md
|
||||
UPGRADE.md
|
||||
composer.lock
|
||||
vendor
|
||||
docs
|
||||
indocker
|
||||
docker-*
|
||||
php*
|
||||
infection.json
|
||||
phpstan.neon
|
||||
php*xml*
|
||||
infection.json
|
||||
**/test*
|
||||
build*
|
||||
.github
|
||||
hooks
|
||||
**/.*
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -5,8 +5,6 @@
|
||||
/module/CLI/test-resources export-ignore
|
||||
/module/Core/test export-ignore
|
||||
/module/Core/test-db export-ignore
|
||||
/module/PreviewGenerator/test export-ignore
|
||||
/module/PreviewGenerator/test-db export-ignore
|
||||
/module/Rest/test export-ignore
|
||||
/module/Rest/test-api export-ignore
|
||||
.gitattributes export-ignore
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Bug.md
vendored
2
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -18,7 +18,7 @@ With that said, please fill in the information requested next. More information
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted swoole|Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|SQLite (x.y.z)
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
2
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
@@ -18,7 +18,7 @@ With that said, please fill in the information requested next. More information
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted swoole|Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|SQLite (x.y.z)
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.idea
|
||||
build
|
||||
!hooks/build
|
||||
!docker/build
|
||||
composer.lock
|
||||
composer.phar
|
||||
vendor/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
use Laminas\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
/**
|
||||
* PhpStorm Container Interop code completion
|
||||
|
||||
@@ -6,6 +6,9 @@ checks:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
build:
|
||||
dependencies:
|
||||
override:
|
||||
- composer install --no-interaction --no-scripts --ignore-platform-reqs
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
|
||||
21
.travis.yml
21
.travis.yml
@@ -5,8 +5,7 @@ branches:
|
||||
- /.*/
|
||||
|
||||
php:
|
||||
- '7.2'
|
||||
- '7.3'
|
||||
- '7.4'
|
||||
|
||||
services:
|
||||
- mysql
|
||||
@@ -19,7 +18,7 @@ cache:
|
||||
|
||||
before_install:
|
||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- yes | pecl install swoole
|
||||
- yes | pecl install swoole-4.4.15
|
||||
- phpenv config-rm xdebug.ini || return 0
|
||||
|
||||
install:
|
||||
@@ -34,19 +33,19 @@ before_script:
|
||||
|
||||
script:
|
||||
- composer ci
|
||||
- if [[ ! -z "$DOCKERFILE_CHANGED" && "${TRAVIS_PHP_VERSION}" == "7.2" ]]; then docker build -t shlink-docker-image:temp . ; fi
|
||||
- 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
|
||||
- wget https://phar.phpunit.de/phpcov-6.0.1.phar
|
||||
- phpdbg -qrr phpcov-6.0.1.phar merge build --clover build/clover.xml
|
||||
- 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
|
||||
|
||||
# Before deploying, build dist file for current travis tag
|
||||
before_deploy:
|
||||
- rm -f ocular.phar
|
||||
- ./build.sh ${TRAVIS_TAG#?}
|
||||
- if [[ ! -z $TRAVIS_TAG && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
@@ -56,4 +55,10 @@ deploy:
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: '7.2'
|
||||
php: '7.4'
|
||||
- provider: script
|
||||
script: bash ./docker/build
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_PULL_REQUEST == 'false'
|
||||
php: '7.4'
|
||||
|
||||
243
CHANGELOG.md
243
CHANGELOG.md
@@ -4,7 +4,190 @@ 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).
|
||||
|
||||
## 1.21.2 - 2020-01-12
|
||||
## 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
|
||||
|
||||
#### Added
|
||||
|
||||
* [#626](https://github.com/shlinkio/shlink/issues/626) Added support for Microsoft SQL Server.
|
||||
* [#556](https://github.com/shlinkio/shlink/issues/556) Short code lengths can now be customized, both globally and on a per-short URL basis.
|
||||
* [#541](https://github.com/shlinkio/shlink/issues/541) Added a request ID that is returned on `X-Request-Id` header, can be provided from outside and is set in log entries.
|
||||
* [#642](https://github.com/shlinkio/shlink/issues/642) IP geolocation is now performed over the non-anonymized IP address when using swoole.
|
||||
* [#521](https://github.com/shlinkio/shlink/issues/521) The long URL for any existing short URL can now be edited using the `PATCH /short-urls/{shortCode}` endpoint.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#656](https://github.com/shlinkio/shlink/issues/656) Updated to PHPUnit 9.
|
||||
* [#641](https://github.com/shlinkio/shlink/issues/641) Added two new flags to the `visit:locate` command, `--retry` and `--all`.
|
||||
|
||||
* When `--retry` is provided, it will try to re-locate visits which IP address was originally considered not found, in case it was a temporal issue.
|
||||
* When `--all` is provided together with `--retry`, it will try to re-locate all existing visits. A warning and confirmation are displayed, as this can have side effects.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#665](https://github.com/shlinkio/shlink/issues/665) Fixed `base_url_redirect_to` simplified config option not being properly parsed.
|
||||
* [#663](https://github.com/shlinkio/shlink/issues/663) Fixed Shlink allowing short URLs to be created with an empty custom slug.
|
||||
* [#678](https://github.com/shlinkio/shlink/issues/678) Fixed `db` commands not running in a non-interactive way.
|
||||
|
||||
|
||||
## 2.0.5 - 2020-02-09
|
||||
|
||||
#### Added
|
||||
|
||||
* [#651](https://github.com/shlinkio/shlink/issues/651) Documented how Shlink behaves when using multiple domains.
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#648](https://github.com/shlinkio/shlink/issues/648) Ensured any user can write in log files, in case shlink is run by several system users.
|
||||
* [#650](https://github.com/shlinkio/shlink/issues/650) Ensured default domain is ignored when trying to create a short URL.
|
||||
|
||||
|
||||
## 2.0.4 - 2020-02-02
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#577](https://github.com/shlinkio/shlink/issues/577) Wrapped params used to customize short URL lists into a DTO with implicit validation.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#620](https://github.com/shlinkio/shlink/issues/620) Ensured "controlled" errors (like validation errors and such) won't be logged with error level, preventing logs to be polluted.
|
||||
* [#637](https://github.com/shlinkio/shlink/issues/637) Fixed several work flows in which short URLs with domain are handled form the API.
|
||||
* [#644](https://github.com/shlinkio/shlink/issues/644) Fixed visits to short URL on non-default domain being linked to the URL on default domain with the same short code.
|
||||
* [#643](https://github.com/shlinkio/shlink/issues/643) Fixed searching on short URL lists not taking into consideration the domain name.
|
||||
|
||||
|
||||
## 2.0.3 - 2020-01-27
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#624](https://github.com/shlinkio/shlink/issues/624) Fixed order in which headers for remote IP detection are inspected.
|
||||
* [#623](https://github.com/shlinkio/shlink/issues/623) Fixed short URLs metadata being impossible to reset.
|
||||
* [#628](https://github.com/shlinkio/shlink/issues/628) Fixed `GET /short-urls/{shortCode}` REST endpoint returning a 404 for short URLs which are not enabled.
|
||||
* [#621](https://github.com/shlinkio/shlink/issues/621) Fixed permission denied error when updating same GeoLite file version more than once.
|
||||
|
||||
|
||||
## 2.0.2 - 2020-01-12
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -28,6 +211,64 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* [#615](https://github.com/shlinkio/shlink/issues/615) Fixed query args with no value being lost from the long URL when users are redirected.
|
||||
|
||||
|
||||
## 2.0.1 - 2020-01-10
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#607](https://github.com/shlinkio/shlink/issues/607) Added missing info on UPGRADE.md doc.
|
||||
* [#610](https://github.com/shlinkio/shlink/issues/610) Fixed use of hardcoded quotes on a database migration which makes it fail on postgres.
|
||||
* [#605](https://github.com/shlinkio/shlink/issues/605) Fixed crashes occurring when migrating from old Shlink versions with nullable DB columns that are assigned to non-nullable entity typed props.
|
||||
|
||||
|
||||
## 2.0.0 - 2020-01-08
|
||||
|
||||
#### Added
|
||||
|
||||
* [#429](https://github.com/shlinkio/shlink/issues/429) Added support for PHP 7.4
|
||||
* [#529](https://github.com/shlinkio/shlink/issues/529) Created an UPGRADING.md file explaining how to upgrade from v1.x to v2.x
|
||||
* [#594](https://github.com/shlinkio/shlink/issues/594) Updated external shlink packages, including installer v4.0, which adds the option to ask for the redis cluster config.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#592](https://github.com/shlinkio/shlink/issues/592) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.1.0.
|
||||
* [#530](https://github.com/shlinkio/shlink/issues/530) Migrated project from deprecated `zendframework` components to the new `laminas` and `mezzio` ones.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#429](https://github.com/shlinkio/shlink/issues/429) Dropped support for PHP 7.2 and 7.3
|
||||
|
||||
* [#229](https://github.com/shlinkio/shlink/issues/229) Remove everything which was deprecated, including:
|
||||
|
||||
* Preview generation feature completely removed.
|
||||
* Authentication against REST API using JWT is no longer supported.
|
||||
|
||||
See [UPGRADE](UPGRADE.md) doc in order to get details on how to migrate to this version.
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#600](https://github.com/shlinkio/shlink/issues/600) Fixed health action so that it works with and without version in the path.
|
||||
|
||||
|
||||
## 1.21.1 - 2020-01-02
|
||||
|
||||
#### Added
|
||||
|
||||
45
Dockerfile
45
Dockerfile
@@ -1,15 +1,14 @@
|
||||
FROM php:7.3.11-alpine3.10
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
FROM php:7.4.2-alpine3.11 as base
|
||||
|
||||
ARG SHLINK_VERSION=1.20.2
|
||||
ARG SHLINK_VERSION=2.0.5
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
ENV SWOOLE_VERSION 4.4.12
|
||||
ENV COMPOSER_VERSION 1.9.1
|
||||
ENV SWOOLE_VERSION 4.4.15
|
||||
ENV LC_ALL "C"
|
||||
|
||||
WORKDIR /etc/shlink
|
||||
|
||||
RUN \
|
||||
# Install mysl and calendar
|
||||
# Install mysql and calendar
|
||||
docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \
|
||||
# Install sqlite
|
||||
apk add --no-cache sqlite-libs sqlite-dev && \
|
||||
@@ -24,24 +23,36 @@ RUN \
|
||||
apk add --no-cache libzip-dev zlib-dev libpng-dev && \
|
||||
docker-php-ext-install -j"$(nproc)" zip gd
|
||||
|
||||
# Install swoole
|
||||
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} && \
|
||||
pecl install swoole-${SWOOLE_VERSION} && \
|
||||
docker-php-ext-enable swoole && \
|
||||
apk del .phpize-deps
|
||||
# Install swoole and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
|
||||
docker-php-ext-enable swoole pdo_sqlsrv && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
rm mssql-tools_17.5.1.1-1_amd64.apk
|
||||
|
||||
|
||||
# Install shlink
|
||||
FROM base as builder
|
||||
COPY . .
|
||||
RUN rm -rf ./docker && \
|
||||
wget https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar && \
|
||||
COPY --from=composer:1.10.1 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
||||
php composer.phar clear-cache && \
|
||||
rm composer.*
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Add shlink to the path to ease running it after container is created
|
||||
|
||||
# Prepare final image
|
||||
FROM base
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
|
||||
COPY --from=builder /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
||||
RUN sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Expose swoole port
|
||||
EXPOSE 8080
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2019 Alejandro Celaya
|
||||
Copyright (c) 2016-2020 Alejandro Celaya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
150
README.md
150
README.md
@@ -4,11 +4,14 @@
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||
[](https://github.com/shlinkio/shlink/blob/master/LICENSE)
|
||||
[](https://acel.me/donate)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
|
||||
|
||||
> This document references Shlink 2.x. If you are using an older version and want to upgrade, follow the [UPGRADE](UPGRADE.md) doc.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
@@ -20,6 +23,10 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
|
||||
- [Using a docker image](#using-a-docker-image)
|
||||
- [Using shlink](#using-shlink)
|
||||
- [Shlink CLI Help](#shlink-cli-help)
|
||||
- [Multiple domains](#multiple-domains)
|
||||
- [Management](#management)
|
||||
- [Visits](#visits)
|
||||
- [Special redirects](#special-redirects)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -29,8 +36,8 @@ 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.2 or greater with JSON, APCu, intl, curl, PDO and gd extensions enabled.
|
||||
* MySQL, MariaDB, PostgreSQL or SQLite.
|
||||
* PHP 7.4 or greater with JSON, curl, PDO 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).
|
||||
|
||||
### Download
|
||||
@@ -61,7 +68,7 @@ In order to run Shlink, you will need a built version of the project. There are
|
||||
|
||||
Despite how you built the project, you now need to configure it, by following these steps:
|
||||
|
||||
* If you are going to use MySQL, MariaDB or PostgreSQL, create an empty database with the name of your choice.
|
||||
* If you are going to use MySQL, MariaDB, PostgreSQL or Microsoft SQL Server, create an empty database with the name of your choice.
|
||||
* Recursively grant write permissions to the `data` directory. Shlink uses it to cache some information.
|
||||
* Setup the application by running the `bin/install` script. It is a command line tool that will guide you through the installation process. **Take into account that this tool has to be run directly on the server where you plan to host Shlink. Do not run it before uploading/moving it there.**
|
||||
* Generate your first API key by running `bin/cli api-key:generate`. You will need the key in order to interact with shlink's API.
|
||||
@@ -90,7 +97,7 @@ Once Shlink is configured, you need to expose it to the web, either by using a t
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
@@ -121,7 +128,7 @@ Once Shlink is configured, you need to expose it to the web, either by using a t
|
||||
|
||||
First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`.
|
||||
|
||||
Once installed, it's actually pretty easy to get shlink up and running with swoole. Run `./vendor/bin/zend-expressive-swoole start -d` and you will get shlink running on port 8080.
|
||||
Once installed, it's actually pretty easy to get shlink up and running with swoole. Run `./vendor/bin/mezzio-swoole start -d` and you will get shlink running on port 8080.
|
||||
|
||||
However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted.
|
||||
|
||||
@@ -138,7 +145,7 @@ Once Shlink is configured, you need to expose it to the web, either by using a t
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
SCRIPT=/path/to/shlink/vendor/bin/mezzio-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
@@ -197,31 +204,11 @@ Finally access to [https://app.shlink.io](https://app.shlink.io) and configure y
|
||||
|
||||
### Bonus
|
||||
|
||||
Depending on the shlink version you installed and how you serve it, there are a couple of time-consuming tasks that shlink expects you to do manually, or at least it is recommended, since it will improve runtime performance.
|
||||
Geo-locating visits to your short links is a time-consuming task. When serving Shlink with swoole, the geo-location task is automatically run asynchronously just after a visit to a short URL happens.
|
||||
|
||||
Those tasks can be performed using shlink's CLI tool, so it should be easy to schedule them to be run in the background (for example, using cron jobs):
|
||||
However, if you are not serving Shlink with swoole, you will have to schedule the geo-location task to be run regularly in the background (for example, using cron jobs):
|
||||
|
||||
* **For shlink older than 1.18.0 or not using swoole to serve it**: Resolve IP address locations: `/path/to/shlink/bin/cli visit:locate`
|
||||
|
||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||
|
||||
> If you serve Shlink with swoole and use v1.18.0 at least, visit location is automatically scheduled by Shlink just after the visit occurs, using swoole's task system.
|
||||
|
||||
* **For shlink older than v1.17.0**: Update IP geolocation database: `/path/to/shlink/bin/cli visit:update-db`
|
||||
|
||||
When shlink is installed it downloads a fresh [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) db file. Running this command will update this file.
|
||||
|
||||
The file is updated the first Tuesday of every month, so it should be enough running this command the first Wednesday.
|
||||
|
||||
> You don't need this if you use Shlink v1.17.0 or newer, since now it downloads/updates the geolocation database automatically just before trying to use it.
|
||||
|
||||
* Generate website previews: `/path/to/shlink/bin/cli short-url:process-previews`
|
||||
|
||||
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
|
||||
|
||||
> **Important!** Generating previews is considered deprecated and the feature will be removed in Shlink v2.
|
||||
|
||||
*Any of these commands accept the `-q` flag, which makes it not display any output. This is recommended when configuring the commands as cron jobs.*
|
||||
The command you need to run is `/path/to/shlink/bin/cli visit:locate`, and you can optionally provide the `-q` flag to remove any output and avoid your cron logs to be polluted.
|
||||
|
||||
## Update to new version
|
||||
|
||||
@@ -252,7 +239,7 @@ Once shlink is installed, there are two main ways to interact with it:
|
||||
|
||||
It is probably a good idea to symlink the CLI entry point (`bin/cli`) to somewhere in your path, so that you can run shlink from any directory.
|
||||
|
||||
* **The REST API**. The complete docs on how to use the API can be found [here](https://shlink.io/api-docs), and a sandbox which also documents every endpoint can be found in the [API Spec](https://api-spec.shlink.io/) portal.
|
||||
* **The REST API**. The complete docs on how to use the API can be found [here](https://shlink.io/documentation/api-docs), and a sandbox which also documents every endpoint can be found in the [API Spec](https://api-spec.shlink.io/) portal.
|
||||
|
||||
However, you probably don't want to consume the raw API yourself. That's why a nice [web client](https://github.com/shlinkio/shlink-web-client) is provided that can be directly used from [https://app.shlink.io](https://app.shlink.io), or you can host it yourself too.
|
||||
|
||||
@@ -274,33 +261,90 @@ Options:
|
||||
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
|
||||
|
||||
Available commands:
|
||||
help Displays help for a command
|
||||
list Lists commands
|
||||
help Displays help for a command
|
||||
list Lists commands
|
||||
api-key
|
||||
api-key:disable Disables an API key.
|
||||
api-key:generate Generates a new valid API key.
|
||||
api-key:list Lists all the available API keys.
|
||||
config
|
||||
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
|
||||
api-key:disable Disables an API key.
|
||||
api-key:generate Generates a new valid API key.
|
||||
api-key:list Lists all the available API keys.
|
||||
db
|
||||
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
|
||||
db:migrate Runs database migrations, which will ensure the shlink database is up to date.
|
||||
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
|
||||
db:migrate Runs database migrations, which will ensure the shlink database is up to date.
|
||||
short-url
|
||||
short-url:delete [short-code:delete] Deletes a short URL
|
||||
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
|
||||
short-url:list [shortcode:list|short-code:list] List all short URLs
|
||||
short-url:parse [shortcode:parse|short-code:parse] Returns the long URL behind a short code
|
||||
short-url:process-previews [shortcode:process-previews|short-code:process-previews] [DEPRECATED] Processes and generates the previews for every URL, improving performance for later web requests.
|
||||
short-url:visits [shortcode:visits|short-code:visits] Returns the detailed visits information for provided short code
|
||||
short-url:delete Deletes a short URL
|
||||
short-url:generate Generates a short URL for provided long URL and returns it
|
||||
short-url:list List all short URLs
|
||||
short-url:parse Returns the long URL behind a short code
|
||||
short-url:visits Returns the detailed visits information for provided short code
|
||||
tag
|
||||
tag:create Creates one or more tags.
|
||||
tag:delete Deletes one or more tags.
|
||||
tag:list Lists existing tags.
|
||||
tag:rename Renames one existing tag.
|
||||
tag:create Creates one or more tags.
|
||||
tag:delete Deletes one or more tags.
|
||||
tag:list Lists existing tags.
|
||||
tag:rename Renames one existing tag.
|
||||
visit
|
||||
visit:locate [visit:process] Resolves visits origin locations.
|
||||
visit:update-db [DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses
|
||||
visit:locate Resolves visits origin locations.
|
||||
```
|
||||
|
||||
## Multiple domains
|
||||
|
||||
While in many cases you will just have one short domain and you'll want all your short URLs to be served from it, there are some cases in which you might want to have multiple short domains served from the same Shlink instance.
|
||||
|
||||
If that's the case, you need to understand how Shlink will behave when managing your short URLs or any of them is visited.
|
||||
|
||||
### Management
|
||||
|
||||
When you create a short URL it is possible to optionally pass a `domain` param. If you don't pass it, the short URL will be created for the default domain (the one provided during Shlink's installation or in the `SHORT_DOMAIN_HOST` env var when using the docker image).
|
||||
|
||||
However, if you pass it, the short URL will be "linked" to that domain.
|
||||
|
||||
> Note that, if the default domain is passed, Shlink will ignore it and will behave as if no `domain` param was provided.
|
||||
|
||||
The main benefit of being able to pass the domain is that Shlink will allow the same custom slug to be used in multiple short URLs, as long as the domain is different (like `example.com/my-compaign`, `another.com/my-compaign` and `foo.com/my-compaign`).
|
||||
|
||||
Then, each short URL will be tracked separately and you will be able to define specific tags and metadata for each one of them.
|
||||
|
||||
However, this has a side effect. When you try to interact with an existing short URL (editing tags, editing meta, resolving it or deleting it), either from the REST API or the CLI tool, you will have to provide the domain appropriately.
|
||||
|
||||
Let's imagine this situation. Shlink's default domain is `example.com`, and you have the next short URLs:
|
||||
|
||||
* `https://example.com/abc123` -> a regular short URL where no domain was provided.
|
||||
* `https://example.com/my-campaign` -> a regular short URL where no domain was provided, but it has a custom slug.
|
||||
* `https://another.com/my-campaign` -> a short URL where the `another.com` domain was provided, and it has a custom slug.
|
||||
* `https://another.com/def456` -> a short URL where the `another.com` domain was provided.
|
||||
|
||||
These are some of the results you will get when trying to interact with them, depending on the params you provide:
|
||||
|
||||
* Providing just the `abc123` short code -> the first URL will be matched.
|
||||
* Providing just the `my-campaign` short code -> the second URL will be matched, since you did not specify a domain, therefor, Shlink looks for the one with the short code/slug `my-campaign` which is also linked to default domain (or not linked to any domain, to be more accurate).
|
||||
* Providing the `my-campaign` short code and the `another.com` domain -> The third one will be matched.
|
||||
* Providing just the `def456` short code -> Shlink will fail/not find any short URL, since there's none with the short code `def456` linked to default domain.
|
||||
* Providing the `def456` short code and the `another.com` domain -> The fourth short URL will be matched.
|
||||
* Providing any short code and the `foo.com` domain -> Again, no short URL will be found, as there's none linked to `foo.com` domain.
|
||||
|
||||
### Visits
|
||||
|
||||
Before adding support for multiple domains, you could point as many domains as you wanted to Shlink, and they would have always worked for existing short codes/slugs.
|
||||
|
||||
In order to keep backwards compatibility, Shlink's behavior when a short URL is visited is slightly different, getting to fallback in some cases.
|
||||
|
||||
Let's continue with previous example, and also consider we have three domains that will resolve to our Shlink instance, which are `example.com`, `another.com` and `foo.com`.
|
||||
|
||||
With that in mind, this is how Shlink will behave when the next short URLs are visited:
|
||||
|
||||
* `https://another.com/abc123` -> There was no short URL specifically defined for domain `another.com` and short code `abc123`, but it exists for default domain (`example.com`), so it will fall back to it and redirect to where `example.com/abc123` is configured to redirect.
|
||||
* `https://example.com/def456` -> The fall-back does not happen from default domain to specific ones, only the other way around (like in previous case). Because of that, this one will result in a not-found URL, even though the `def456` short code exists for `another.com` domain.
|
||||
* `https://foo.com/abc123` -> This will also fall-back to `example.com/abc123`, like in the first case.
|
||||
* `https://another.com/non-existing` -> The combination of `another.com` domain with the `non-existing` slug does not exist, so Shlink will try to fall-back to the same but for default domain (`example.com`). However, since that combination does not exist either, it will result in a not-found URL.
|
||||
* Any other short URL visited exactly as it was configured will, of course, resolve as expected.
|
||||
|
||||
### Special redirects
|
||||
|
||||
It is currently possible to configure some special redirects when the base domain is visited, a URL does not match, or an invalid/disabled short URL is visited.
|
||||
|
||||
Those are configured during Shlink's installation or via env vars when using the docker image.
|
||||
|
||||
Currently those are all shared for all domains serving the same Shlink instance, but the plan is to update that and allow specific ones for every existing domain.
|
||||
|
||||
---
|
||||
|
||||
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)
|
||||
|
||||
88
UPGRADE.md
Normal file
88
UPGRADE.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Upgrading
|
||||
|
||||
## From v1.x to v2.x
|
||||
|
||||
### PHP 7.4 required
|
||||
|
||||
This new version takes advantage of several new features introduced in PHP 7.4.
|
||||
|
||||
Thanks to that, the code is more reliable and robust, and easier to maintain and improve.
|
||||
|
||||
However, that means that any previous PHP version is no longer supported.
|
||||
|
||||
### Preview generation
|
||||
|
||||
The ability to generate website previews has been completely removed and has no replacement.
|
||||
|
||||
The feature never properly worked, and it wasn't really useful. Because of that, the feature is no longer available on Shlink 2.x
|
||||
|
||||
Removing this feature has these implications:
|
||||
|
||||
* The `short-url:process-previews` CLI command no longer exists, and an error will be thrown if executed.
|
||||
* The `/{shortCode}/preview` path is no longer valid, and will return a 404 status.
|
||||
|
||||
### Removed paths
|
||||
|
||||
These routes have been removed, but have a direct replacement:
|
||||
|
||||
* `/qr/{shortCode}[/{size}]` -> `/{shortCode}/qr-code[/{size}]`
|
||||
* `PUT /rest/v{version}/short-urls/{shortCode}` -> `PATCH /rest/v{version}/short-urls/{shortCode}`
|
||||
|
||||
When using the old ones, a 404 status will me returned now.
|
||||
|
||||
### Removed command and route aliases
|
||||
|
||||
All the aliases for the CLI commands in the `short-urls` namespace have been removed. If you were using any of those commands with the `shortcode` or `short-code` prefixes, make sure to update them to use the `short-urls` prefix instead.
|
||||
|
||||
The same happens for all REST endpoints starting with `/short-code`. They were previously aliased to `/short-urls` ones, but they will return a 404 now. Make sure to update them accordingly.
|
||||
|
||||
### JWT authentication removed
|
||||
|
||||
Shlink's REST API no longer accepts authentication using a JWT token. The API key has to be passed now in the `x-api-key` header.
|
||||
|
||||
Removing this feature has these implications:
|
||||
|
||||
* Shlink will no longer introspect the `Authorization` header for Bearer tokens.
|
||||
* The `POST /rest/v{version}/authenticate` endpoint no longer exists and will return a 404.
|
||||
|
||||
### API version is now required
|
||||
|
||||
Endpoints need to provide a version in the path now. Previously, not providing a version used to fall back to v1. Now, it will return a 404 status, as no route will match.
|
||||
|
||||
The only exception is the `/rest/health` endpoint, which will continue working without the version.
|
||||
|
||||
### API errors
|
||||
|
||||
Shlink v1.21.0 introduced support for API errors using the Problem Details format, as well as the v2 of the API.
|
||||
|
||||
For backwards compatibility reasons, requests performed to v1 continued to return the old `error` and `message` properties.
|
||||
|
||||
Starting with Shlink v2.0.0, both versions of the API will no longer return those two properties.
|
||||
|
||||
As a replacement, use `type` instead of `error`, and `detail` instead of `message`.
|
||||
|
||||
### Changes in models
|
||||
|
||||
The next REST API models have changed:
|
||||
|
||||
* **ShortUrl**: The `originalUrl` property was deprecated and has been removed. Use `longUrl` instead.
|
||||
* **Visit**: The `remoteAddr` property was deprecated and has been removed. It has no replacement.
|
||||
* **VisitLocation**: The `latitude` and `longitude` properties are no longer strings, but float.
|
||||
|
||||
### URL validation
|
||||
|
||||
Shlink can verify provided long URLs are valid before trying to shorten them. Starting with v2, it no longer does it by default and needs to be explicitly enabled instead of explicitly disabled.
|
||||
|
||||
### Removed config options
|
||||
|
||||
The `not_found_redirect_to` config option and the `NOT_FOUND_REDIRECT_TO` env var are no longer taken into consideration for the docker image.
|
||||
|
||||
Instead, use `invalid_short_url_redirect_to` and `INVALID_SHORT_URL_REDIRECT_TO` respectively.
|
||||
|
||||
### Migrated to Laminas
|
||||
|
||||
The project has been using Zend Framework components since the beginning. Since it has been re-branded as [Laminas](https://getlaminas.org/), this version updates to the new set of components.
|
||||
|
||||
Updating to Laminas components has these implications:
|
||||
|
||||
* If you were manually serving Shlink with swoole, the entry script has to be changed from `/path/to/shlink/vendor/bin/zend-expressive-swoole` to `/path/to/shlink/vendor/bin/mezzio-swoole`
|
||||
@@ -8,5 +8,5 @@ use function chdir;
|
||||
use function dirname;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$run(false);
|
||||
[$install] = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$install();
|
||||
|
||||
@@ -3,16 +3,16 @@ export APP_ENV=test
|
||||
export DB_DRIVER=mysql
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
vendor/bin/zend-expressive-swoole stop
|
||||
vendor/bin/mezzio-swoole stop
|
||||
|
||||
echo 'Starting server...'
|
||||
vendor/bin/zend-expressive-swoole start -d
|
||||
vendor/bin/mezzio-swoole start -d
|
||||
sleep 2
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
|
||||
phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
|
||||
testsExitCode=$?
|
||||
|
||||
vendor/bin/zend-expressive-swoole stop
|
||||
vendor/bin/mezzio-swoole stop
|
||||
|
||||
# Exit this script with the same code as the tests. If tests failed, this script has to fail
|
||||
exit $testsExitCode
|
||||
|
||||
@@ -8,5 +8,5 @@ use function chdir;
|
||||
use function dirname;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$run(true);
|
||||
[, $update] = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$update();
|
||||
|
||||
Binary file not shown.
3
build.sh
3
build.sh
@@ -19,13 +19,14 @@ mkdir -p "${builtcontent}"
|
||||
rsync -av * "${builtcontent}" \
|
||||
--exclude=*docker* \
|
||||
--exclude=Dockerfile \
|
||||
--include=.htaccess \
|
||||
--exclude-from=./.dockerignore
|
||||
cd "${builtcontent}"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||
${composerBin} install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2",
|
||||
"php": "^7.4",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^1.0",
|
||||
@@ -26,53 +26,56 @@
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"geoip2/geoip2": "^2.9",
|
||||
"guzzlehttp/guzzle": "^6.5.1",
|
||||
"laminas/laminas-config": "^3.3",
|
||||
"laminas/laminas-config-aggregator": "^1.1",
|
||||
"laminas/laminas-dependency-plugin": "^1.0",
|
||||
"laminas/laminas-diactoros": "^2.1.3",
|
||||
"laminas/laminas-inputfilter": "^2.10",
|
||||
"laminas/laminas-paginator": "^2.8",
|
||||
"laminas/laminas-servicemanager": "^3.4",
|
||||
"laminas/laminas-stdlib": "^3.2",
|
||||
"lstrojny/functional-php": "^1.9",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"mezzio/mezzio": "^3.2",
|
||||
"mezzio/mezzio-fastroute": "^3.0",
|
||||
"mezzio/mezzio-helpers": "^5.3",
|
||||
"mezzio/mezzio-platesrenderer": "^2.1",
|
||||
"mezzio/mezzio-problem-details": "^1.1",
|
||||
"mezzio/mezzio-swoole": "^2.6",
|
||||
"monolog/monolog": "^2.0",
|
||||
"nikolaposa/monolog-factory": "^3.0",
|
||||
"ocramius/proxy-manager": "~2.2.2",
|
||||
"ocramius/proxy-manager": "^2.7.0",
|
||||
"phly/phly-event-dispatcher": "^1.0",
|
||||
"php-middleware/request-id": "^4.0",
|
||||
"predis/predis": "^1.1",
|
||||
"pugx/shortid-php": "^0.5",
|
||||
"shlinkio/shlink-common": "^2.4",
|
||||
"shlinkio/shlink-event-dispatcher": "^1.1",
|
||||
"shlinkio/shlink-installer": "^3.3",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.2",
|
||||
"ramsey/uuid": "^3.9",
|
||||
"shlinkio/shlink-common": "^3.0",
|
||||
"shlinkio/shlink-config": "^1.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^1.4",
|
||||
"shlinkio/shlink-installer": "^4.3.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.4",
|
||||
"symfony/console": "^5.0",
|
||||
"symfony/filesystem": "^5.0",
|
||||
"symfony/lock": "^5.0",
|
||||
"symfony/process": "^5.0",
|
||||
"zendframework/zend-config": "^3.3",
|
||||
"zendframework/zend-config-aggregator": "^1.1",
|
||||
"zendframework/zend-diactoros": "^2.1.3",
|
||||
"zendframework/zend-expressive": "^3.2",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.3",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.1",
|
||||
"zendframework/zend-expressive-swoole": "^2.4",
|
||||
"zendframework/zend-inputfilter": "^2.10",
|
||||
"zendframework/zend-paginator": "^2.8",
|
||||
"zendframework/zend-problem-details": "^1.0",
|
||||
"zendframework/zend-servicemanager": "^3.4",
|
||||
"zendframework/zend-stdlib": "^3.2"
|
||||
"symfony/process": "^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.2.0",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"infection/infection": "^0.15.0",
|
||||
"phpstan/phpstan-shim": "^0.11.16",
|
||||
"phpunit/phpunit": "^8.3",
|
||||
"infection/infection": "^0.16.1",
|
||||
"phpstan/phpstan": "^0.12.18",
|
||||
"phpunit/phpunit": "~9.0.1",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.0.0",
|
||||
"shlinkio/shlink-test-utils": "^1.2",
|
||||
"shlinkio/php-coding-standard": "~2.1.0",
|
||||
"shlinkio/shlink-test-utils": "^1.4",
|
||||
"symfony/var-dumper": "^5.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Core/functions/functions.php"
|
||||
@@ -86,8 +89,7 @@
|
||||
"ShlinkioTest\\Shlink\\Core\\": [
|
||||
"module/Core/test",
|
||||
"module/Core/test-db"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -99,7 +101,7 @@
|
||||
],
|
||||
"cs": "phpcs",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config --level=5 -c phpstan.neon",
|
||||
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config --level=6",
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:db",
|
||||
@@ -108,7 +110,7 @@
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db:ci",
|
||||
"@test:api"
|
||||
"@test:api:ci"
|
||||
],
|
||||
"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",
|
||||
@@ -116,7 +118,8 @@
|
||||
"@test:db:sqlite",
|
||||
"@test:db:mysql",
|
||||
"@test:db:maria",
|
||||
"@test:db:postgres"
|
||||
"@test:db:postgres",
|
||||
"@test:db:ms"
|
||||
],
|
||||
"test:db:ci": [
|
||||
"@test:db:sqlite",
|
||||
@@ -127,10 +130,12 @@
|
||||
"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",
|
||||
"infect:ci": "@infect --coverage=build --skip-initial-tests",
|
||||
"infect:show": "@infect --show-mutations",
|
||||
"infect:test": [
|
||||
"@test:unit:ci",
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY', ''),
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ return [
|
||||
'ip_address_resolution' => [
|
||||
'headers_to_inspect' => [
|
||||
'CF-Connecting-IP',
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'Forwarded',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded',
|
||||
'Forwarded',
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'X-Cluster-Client-Ip',
|
||||
'Client-Ip',
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Mezzio\Container;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'delegators' => [
|
||||
Expressive\Application::class => [
|
||||
Mezzio\Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
@@ -12,7 +13,7 @@ return [
|
||||
],
|
||||
|
||||
'initializers' => [
|
||||
function (ContainerInterface $container, $instance) {
|
||||
function (ContainerInterface $container, $instance): void {
|
||||
if ($instance instanceof Log\LoggerAwareInterface) {
|
||||
$instance->setLogger($container->get(Log\LoggerInterface::class));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
'load_mappings_using_functional_style' => true,
|
||||
],
|
||||
'connection' => [
|
||||
'user' => '',
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\Stratigility\Middleware\ErrorHandler;
|
||||
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
|
||||
use Shlinkio\Shlink\Common\Logger;
|
||||
use Zend\ProblemDetails\ProblemDetailsMiddleware;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
|
||||
'backwards_compatible_problem_details' => [
|
||||
'default_type_fallbacks' => [
|
||||
'problem-details' => [
|
||||
'default_types_map' => [
|
||||
404 => 'NOT_FOUND',
|
||||
500 => 'INTERNAL_SERVER_ERROR',
|
||||
],
|
||||
'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
|
||||
],
|
||||
|
||||
'error_handler' => [
|
||||
|
||||
@@ -7,9 +7,7 @@ return [
|
||||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'download_from' =>
|
||||
'https://download.maxmind.com/app/geoip_download'
|
||||
. '?edition_id=GeoLite2-City&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz',
|
||||
'license_key' => 'G4Lm0C60yJsnkdPi',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,51 +2,44 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
use Shlinkio\Shlink\Installer\Config\Option;
|
||||
|
||||
return [
|
||||
|
||||
'installer_plugins_expected_config' => [
|
||||
Plugin\UrlShortenerConfigCustomizer::class => [
|
||||
Plugin\UrlShortenerConfigCustomizer::SCHEMA,
|
||||
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
|
||||
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
|
||||
Plugin\UrlShortenerConfigCustomizer::NOTIFY_VISITS_WEBHOOKS,
|
||||
Plugin\UrlShortenerConfigCustomizer::VISITS_WEBHOOKS,
|
||||
'installer' => [
|
||||
'enabled_options' => [
|
||||
Option\DatabaseDriverConfigOption::class,
|
||||
Option\DatabaseNameConfigOption::class,
|
||||
Option\DatabaseHostConfigOption::class,
|
||||
Option\DatabasePortConfigOption::class,
|
||||
Option\DatabaseUserConfigOption::class,
|
||||
Option\DatabasePasswordConfigOption::class,
|
||||
Option\DatabaseSqlitePathConfigOption::class,
|
||||
Option\DatabaseMySqlOptionsConfigOption::class,
|
||||
Option\ShortDomainHostConfigOption::class,
|
||||
Option\ShortDomainSchemaConfigOption::class,
|
||||
Option\ValidateUrlConfigOption::class,
|
||||
Option\VisitsWebhooksConfigOption::class,
|
||||
Option\BaseUrlRedirectConfigOption::class,
|
||||
Option\InvalidShortUrlRedirectConfigOption::class,
|
||||
Option\Regular404RedirectConfigOption::class,
|
||||
Option\DisableTrackParamConfigOption::class,
|
||||
Option\CheckVisitsThresholdConfigOption::class,
|
||||
Option\VisitsThresholdConfigOption::class,
|
||||
Option\BasePathConfigOption::class,
|
||||
Option\TaskWorkerNumConfigOption::class,
|
||||
Option\WebWorkerNumConfigOption::class,
|
||||
Option\RedisServersConfigOption::class,
|
||||
Option\ShortCodeLengthOption::class,
|
||||
],
|
||||
|
||||
Plugin\ApplicationConfigCustomizer::class => [
|
||||
Plugin\ApplicationConfigCustomizer::SECRET,
|
||||
Plugin\ApplicationConfigCustomizer::DISABLE_TRACK_PARAM,
|
||||
Plugin\ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD,
|
||||
Plugin\ApplicationConfigCustomizer::VISITS_THRESHOLD,
|
||||
Plugin\ApplicationConfigCustomizer::BASE_PATH,
|
||||
Plugin\ApplicationConfigCustomizer::WEB_WORKER_NUM,
|
||||
Plugin\ApplicationConfigCustomizer::TASK_WORKER_NUM,
|
||||
],
|
||||
|
||||
Plugin\DatabaseConfigCustomizer::class => [
|
||||
Plugin\DatabaseConfigCustomizer::DRIVER,
|
||||
Plugin\DatabaseConfigCustomizer::NAME,
|
||||
Plugin\DatabaseConfigCustomizer::USER,
|
||||
Plugin\DatabaseConfigCustomizer::PASSWORD,
|
||||
Plugin\DatabaseConfigCustomizer::HOST,
|
||||
Plugin\DatabaseConfigCustomizer::PORT,
|
||||
],
|
||||
|
||||
Plugin\RedirectsConfigCustomizer::class => [
|
||||
Plugin\RedirectsConfigCustomizer::INVALID_SHORT_URL_REDIRECT_TO,
|
||||
Plugin\RedirectsConfigCustomizer::REGULAR_404_REDIRECT_TO,
|
||||
Plugin\RedirectsConfigCustomizer::BASE_URL_REDIRECT_TO,
|
||||
],
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
'db_create_schema' => [
|
||||
'command' => 'bin/cli db:create',
|
||||
],
|
||||
'db_migrate' => [
|
||||
'command' => 'bin/cli db:migrate',
|
||||
'installation_commands' => [
|
||||
'db_create_schema' => [
|
||||
'command' => 'bin/cli db:create',
|
||||
],
|
||||
'db_migrate' => [
|
||||
'command' => 'bin/cli db:migrate',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Shlinkio\Shlink\Common\Cache\RedisFactory;
|
||||
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
$localLockFactory = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY;
|
||||
|
||||
return [
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
||||
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
|
||||
Lock\LockFactory::class => ConfigAbstractFactory::class,
|
||||
$localLockFactory => ConfigAbstractFactory::class,
|
||||
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
||||
@@ -44,7 +44,7 @@ return [
|
||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
|
||||
Lock\LockFactory::class => ['lock_store'],
|
||||
$localLockFactory => ['local_lock_store'],
|
||||
LOCAL_LOCK_FACTORY => ['local_lock_store'],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ use Monolog\Handler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor;
|
||||
use MonologFactory\DiContainerLoggerFactory;
|
||||
use PhpMiddleware\RequestId;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
use const PHP_EOL;
|
||||
@@ -20,11 +21,12 @@ $processors = [
|
||||
'psr3' => [
|
||||
'name' => Processor\PsrLogMessageProcessor::class,
|
||||
],
|
||||
'request_id' => RequestId\MonologProcessor::class,
|
||||
];
|
||||
$formatter = [
|
||||
'name' => Formatter\LineFormatter::class,
|
||||
'params' => [
|
||||
'format' => '[%datetime%] %channel%.%level_name% - %message%' . PHP_EOL,
|
||||
'format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%' . PHP_EOL,
|
||||
'allow_inline_line_breaks' => true,
|
||||
],
|
||||
];
|
||||
@@ -41,6 +43,7 @@ return [
|
||||
'level' => Logger::INFO,
|
||||
'filename' => 'data/log/shlink_log.log',
|
||||
'max_files' => 30,
|
||||
'file_permission' => 0666,
|
||||
],
|
||||
'formatter' => $formatter,
|
||||
],
|
||||
@@ -75,10 +78,11 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger-name' => 'Logger_Access',
|
||||
'format' => '%h %l %u "%r" %>s %b',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -4,16 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Zend\Expressive;
|
||||
use Zend\ProblemDetails;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
use Laminas\Stratigility\Middleware\ErrorHandler;
|
||||
use Mezzio\Helper;
|
||||
use Mezzio\ProblemDetails;
|
||||
use Mezzio\Router;
|
||||
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
||||
|
||||
return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'error-handler' => [
|
||||
'middleware' => [
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Helper\ContentLengthMiddleware::class,
|
||||
ErrorHandler::class,
|
||||
],
|
||||
],
|
||||
@@ -21,7 +23,7 @@ return [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\CrossDomainMiddleware::class,
|
||||
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
|
||||
RequestIdMiddleware::class,
|
||||
ProblemDetails\ProblemDetailsMiddleware::class,
|
||||
],
|
||||
],
|
||||
@@ -31,24 +33,18 @@ return [
|
||||
Common\Middleware\CloseDbConnectionMiddleware::class,
|
||||
],
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\PathVersionMiddleware::class,
|
||||
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
|
||||
],
|
||||
],
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\RouteMiddleware::class,
|
||||
Router\Middleware\RouteMiddleware::class,
|
||||
Router\Middleware\ImplicitHeadMiddleware::class,
|
||||
],
|
||||
],
|
||||
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
],
|
||||
@@ -56,7 +52,7 @@ return [
|
||||
|
||||
'dispatch' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
Router\Middleware\DispatchMiddleware::class,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -73,4 +69,5 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/* @deprecated */
|
||||
return [
|
||||
|
||||
'preview_generation' => [
|
||||
'files_location' => 'data/cache',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,13 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'redis' => [
|
||||
'servers' => 'tcp://shlink_redis:6379',
|
||||
// 'servers' => [
|
||||
// 'tcp://shlink_redis:6379',
|
||||
// ],
|
||||
'cache' => [
|
||||
'redis' => [
|
||||
'servers' => 'tcp://shlink_redis:6379',
|
||||
// 'servers' => [
|
||||
// 'tcp://shlink_redis:6379',
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
|
||||
38
config/autoload/request_id.global.php
Normal file
38
config/autoload/request_id.global.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use PhpMiddleware\RequestId;
|
||||
|
||||
return [
|
||||
|
||||
'request_id' => [
|
||||
'allow_override' => true,
|
||||
'header_name' => 'X-Request-Id',
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
RequestId\Generator\RamseyUuid4StaticGenerator::class => InvokableFactory::class,
|
||||
RequestId\RequestIdProviderFactory::class => ConfigAbstractFactory::class,
|
||||
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
|
||||
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
RequestId\RequestIdProviderFactory::class => [
|
||||
RequestId\Generator\RamseyUuid4StaticGenerator::class,
|
||||
'config.request_id.allow_override',
|
||||
'config.request_id.header_name',
|
||||
],
|
||||
RequestId\RequestIdMiddleware::class => [
|
||||
RequestId\RequestIdProviderFactory::class,
|
||||
'config.request_id.header_name',
|
||||
],
|
||||
RequestId\MonologProcessor::class => [RequestId\RequestIdMiddleware::class],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Router\FastRouteRouter;
|
||||
use Mezzio\Router\FastRouteRouter;
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<?php
|
||||
use Zend\Expressive\Router\FastRouteRouter;
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mezzio\Router\FastRouteRouter;
|
||||
|
||||
return [
|
||||
|
||||
'router' => [
|
||||
// 'base_path' => '',
|
||||
'fastroute' => [
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => true,
|
||||
|
||||
'swoole-http-server' => [
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Mezzio\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher;
|
||||
|
||||
return [
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'mezzio-swoole' => [
|
||||
'hot-code-reload' => [
|
||||
'enable' => true,
|
||||
],
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
@@ -9,8 +11,9 @@ return [
|
||||
'schema' => 'https',
|
||||
'hostname' => '',
|
||||
],
|
||||
'validate_url' => true,
|
||||
'validate_url' => false,
|
||||
'visits_webhooks' => [],
|
||||
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'wkhtmltopdf' => [
|
||||
'images' => [
|
||||
'binary' => __DIR__ . '/../../bin/wkhtmltoimage',
|
||||
'type' => 'jpg',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Laminas\ServiceManager\ServiceManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
return (function () {
|
||||
/** @var ContainerInterface|ServiceManager $container */
|
||||
|
||||
@@ -4,31 +4,33 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
use Zend\ProblemDetails;
|
||||
use Laminas\ConfigAggregator;
|
||||
use Laminas\ZendFrameworkBridge;
|
||||
use Mezzio;
|
||||
use Mezzio\ProblemDetails;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
Expressive\ConfigProvider::class,
|
||||
Expressive\Router\ConfigProvider::class,
|
||||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Expressive\Plates\ConfigProvider::class,
|
||||
Expressive\Swoole\ConfigProvider::class,
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Mezzio\Plates\ConfigProvider::class,
|
||||
Mezzio\Swoole\ConfigProvider::class,
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Config\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
PreviewGenerator\ConfigProvider::class,
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
env('APP_ENV') === 'test'
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
: new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
: new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
], 'data/cache/app_config.php', [
|
||||
ZendFrameworkBridge\ConfigPostProcessor::class,
|
||||
Core\Config\SimplifiedConfigParser::class,
|
||||
Core\Config\BasePathPrefixer::class,
|
||||
Core\Config\DeprecatedConfigParser::class,
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\ServiceManager;
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
|
||||
if (! class_exists('Shlinkio\Shlink\LocalLockFactory')) {
|
||||
class_alias(Lock\LockFactory::class, 'Shlinkio\Shlink\LocalLockFactory');
|
||||
// It needs to be placed here as individual config files will not be loaded once config is cached
|
||||
if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
||||
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
|
||||
}
|
||||
|
||||
// Build container
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mezzio\Application;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Zend\Expressive\Application;
|
||||
|
||||
return function (bool $isCli = false): void {
|
||||
/** @var ContainerInterface $container */
|
||||
|
||||
@@ -15,6 +15,4 @@ $em = $container->get(EntityManager::class);
|
||||
|
||||
$testHelper->createTestDb();
|
||||
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) {
|
||||
$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []);
|
||||
});
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
||||
|
||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use PDO;
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sprintf;
|
||||
@@ -19,9 +19,7 @@ $swooleTestingPort = 9999;
|
||||
$buildDbConnection = function (): array {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
$isCi = env('TRAVIS', false);
|
||||
$getMysqlHost = function (string $driver) {
|
||||
return sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
|
||||
};
|
||||
$getMysqlHost = fn (string $driver) => sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
|
||||
|
||||
$driverConfigMap = [
|
||||
'sqlite' => [
|
||||
@@ -47,6 +45,13 @@ $buildDbConnection = function (): array {
|
||||
'dbname' => 'shlink_test',
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
'mssql' => [
|
||||
'driver' => 'pdo_sqlsrv',
|
||||
'host' => $isCi ? '127.0.0.1' : 'shlink_db_ms',
|
||||
'user' => 'sa',
|
||||
'password' => $isCi ? '' : 'Passw0rd!',
|
||||
'dbname' => 'shlink_test',
|
||||
],
|
||||
];
|
||||
$driverConfigMap['maria'] = $driverConfigMap['mysql'];
|
||||
|
||||
@@ -63,9 +68,10 @@ return [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
'validate_url' => true,
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => false,
|
||||
'swoole-http-server' => [
|
||||
'host' => $swooleTestingHost,
|
||||
|
||||
@@ -11,7 +11,7 @@ server {
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
SCRIPT=/path/to/shlink/vendor/bin/mezzio-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
FROM php:7.3.11-fpm-alpine3.10
|
||||
FROM php:7.4.2-fpm-alpine3.11
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.18
|
||||
ENV APCU_BC_VERSION 1.0.5
|
||||
ENV XDEBUG_VERSION 2.8.0
|
||||
ENV XDEBUG_VERSION 2.9.0
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install iconv
|
||||
RUN docker-php-ext-install mbstring
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache oniguruma-dev
|
||||
RUN docker-php-ext-install mbstring
|
||||
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
RUN apk add --no-cache sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
@@ -63,6 +65,18 @@ RUN docker-php-ext-configure xdebug\
|
||||
# cleanup
|
||||
RUN rm /tmp/xdebug.tar.gz
|
||||
|
||||
# Install sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv && \
|
||||
docker-php-ext-enable pdo_sqlsrv && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
rm mssql-tools_17.5.1.1-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
RUN chmod +x composer.phar
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
FROM php:7.3.11-alpine3.10
|
||||
FROM php:7.4.2-alpine3.11
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.18
|
||||
ENV APCU_BC_VERSION 1.0.5
|
||||
ENV INOTIFY_VERSION 2.0.0
|
||||
ENV SWOOLE_VERSION 4.4.12
|
||||
ENV SWOOLE_VERSION 4.4.15
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install iconv
|
||||
RUN docker-php-ext-install mbstring
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache oniguruma-dev
|
||||
RUN docker-php-ext-install mbstring
|
||||
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
RUN apk add --no-cache sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
@@ -64,12 +66,17 @@ RUN docker-php-ext-configure inotify\
|
||||
# cleanup
|
||||
RUN rm /tmp/inotify.tar.gz
|
||||
|
||||
# Install swoole
|
||||
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
||||
pecl install swoole-${SWOOLE_VERSION} && \
|
||||
docker-php-ext-enable swoole && \
|
||||
apk del .phpize-deps
|
||||
# Install swoole and mssql driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
|
||||
docker-php-ext-enable swoole pdo_sqlsrv && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
rm mssql-tools_17.5.1.1-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
@@ -90,4 +97,4 @@ CMD \
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# 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
|
||||
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done
|
||||
until php ./vendor/bin/mezzio-swoole start; do sleep 1 ; done
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
@@ -30,12 +30,12 @@ class Version20160820191203 extends AbstractMigration
|
||||
private function createTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('tags');
|
||||
$table->addColumn('id', Type::BIGINT, [
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('name', Type::STRING, [
|
||||
$table->addColumn('name', Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => true,
|
||||
]);
|
||||
@@ -47,11 +47,11 @@ class Version20160820191203 extends AbstractMigration
|
||||
private function createShortUrlsInTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('short_urls_in_tags');
|
||||
$table->addColumn('short_url_id', Type::BIGINT, [
|
||||
$table->addColumn('short_url_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('tag_id', Type::BIGINT, [
|
||||
$table->addColumn('tag_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
@@ -24,10 +24,10 @@ class Version20171021093246 extends AbstractMigration
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('valid_since', Type::DATETIME, [
|
||||
$shortUrls->addColumn('valid_since', Types::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('valid_until', Type::DATETIME, [
|
||||
$shortUrls->addColumn('valid_until', Types::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ class Version20171022064541 extends AbstractMigration
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('max_visits', Type::INTEGER, [
|
||||
$shortUrls->addColumn('max_visits', Types::INTEGER, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
@@ -17,7 +17,6 @@ final class Version20180801183328 extends AbstractMigration
|
||||
private const OLD_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
@@ -26,7 +25,6 @@ final class Version20180801183328 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
@@ -35,8 +33,6 @@ final class Version20180801183328 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param int $size
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function setSize(Schema $schema, int $size): void
|
||||
|
||||
@@ -17,7 +17,6 @@ use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
final class Version20180913205455 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
@@ -25,7 +24,6 @@ final class Version20180913205455 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
@@ -67,7 +65,6 @@ final class Version20180913205455 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@ final class Version20180915110857 extends AbstractMigration
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
@@ -39,7 +38,7 @@ final class Version20180915110857 extends AbstractMigration
|
||||
[
|
||||
'onDelete' => self::ON_DELETE_MAP[$foreignTable],
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,6 @@ final class Version20181020060559 extends AbstractMigration
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
@@ -36,7 +35,7 @@ final class Version20181020060559 extends AbstractMigration
|
||||
{
|
||||
foreach ($columnNames as $name) {
|
||||
if (! $visitLocations->hasColumn($name)) {
|
||||
$visitLocations->addColumn($name, Type::STRING, ['notnull' => false]);
|
||||
$visitLocations->addColumn($name, Types::STRING, ['notnull' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20190930165521 extends AbstractMigration
|
||||
@@ -22,19 +22,19 @@ final class Version20190930165521 extends AbstractMigration
|
||||
}
|
||||
|
||||
$domains = $schema->createTable('domains');
|
||||
$domains->addColumn('id', Type::BIGINT, [
|
||||
$domains->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$domains->addColumn('authority', Type::STRING, [
|
||||
$domains->addColumn('authority', Types::STRING, [
|
||||
'length' => 512,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$domains->addUniqueIndex(['authority']);
|
||||
$domains->setPrimaryKey(['id']);
|
||||
|
||||
$shortUrls->addColumn('domain_id', Type::BIGINT, [
|
||||
$shortUrls->addColumn('domain_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
96
data/migrations/Version20200105165647.php
Normal file
96
data/migrations/Version20200105165647.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\some;
|
||||
|
||||
final class Version20200105165647 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['lat' => 'latitude', 'lon' => 'longitude'];
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function preUp(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(some(
|
||||
self::COLUMNS,
|
||||
fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName),
|
||||
), 'New columns already exist');
|
||||
|
||||
foreach (self::COLUMNS as $columnName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($columnName, ':zeroValue')
|
||||
->where($qb->expr()->orX(
|
||||
$qb->expr()->eq($columnName, ':emptyString'),
|
||||
$qb->expr()->isNull($columnName),
|
||||
))
|
||||
->setParameters([
|
||||
'zeroValue' => '0',
|
||||
'emptyString' => '',
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$visitLocations->addColumn($newName, Types::FLOAT, [
|
||||
'default' => '0.0',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$platformName = $this->connection->getDatabasePlatform()->getName();
|
||||
$castType = $platformName === 'postgres' ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)';
|
||||
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')')
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function preDown(Schema $schema): void
|
||||
{
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($oldName, $newName)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $colName => $oldName) {
|
||||
$visitLocations->dropColumn($colName);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
data/migrations/Version20200106215144.php
Normal file
47
data/migrations/Version20200106215144.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\none;
|
||||
|
||||
final class Version20200106215144 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['latitude', 'longitude'];
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(none(
|
||||
self::COLUMNS,
|
||||
fn (string $oldColName) => $visitLocations->hasColumn($oldColName),
|
||||
), 'Old columns do not exist');
|
||||
|
||||
foreach (self::COLUMNS as $colName) {
|
||||
$visitLocations->dropColumn($colName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $colName) {
|
||||
$visitLocations->addColumn($colName, Types::STRING, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
data/migrations/Version20200110182849.php
Normal file
53
data/migrations/Version20200110182849.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\each;
|
||||
use function Functional\partial_left;
|
||||
|
||||
final class Version20200110182849 extends AbstractMigration
|
||||
{
|
||||
private const DEFAULT_EMPTY_VALUE = '';
|
||||
private const COLUMN_DEFAULTS_MAP = [
|
||||
'visits' => [
|
||||
'referer',
|
||||
'user_agent',
|
||||
],
|
||||
'visit_locations' => [
|
||||
'timezone',
|
||||
'country_code',
|
||||
'country_name',
|
||||
'region_name',
|
||||
'city_name',
|
||||
],
|
||||
];
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
each(
|
||||
self::COLUMN_DEFAULTS_MAP,
|
||||
fn (array $columns, string $tableName) =>
|
||||
each($columns, partial_left([$this, 'setDefaultValueForColumnInTable'], $tableName)),
|
||||
);
|
||||
}
|
||||
|
||||
public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update($tableName)
|
||||
->set($columnName, ':emptyValue')
|
||||
->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE)
|
||||
->where($qb->expr()->isNull($columnName))
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No need (and no way) to undo this migration
|
||||
}
|
||||
}
|
||||
45
data/migrations/Version20200323190014.php
Normal file
45
data/migrations/Version20200323190014.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20200323190014 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf($visitLocations->hasColumn('is_empty'));
|
||||
|
||||
$visitLocations->addColumn('is_empty', Types::BOOLEAN, ['default' => false]);
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set('is_empty', ':isEmpty')
|
||||
->where($qb->expr()->eq('country_code', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('country_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('region_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('city_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('timezone', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('lat', 0))
|
||||
->andWhere($qb->expr()->eq('lon', 0))
|
||||
->setParameter('isEmpty', true)
|
||||
->setParameter('emptyString', '')
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(!$visitLocations->hasColumn('is_empty'));
|
||||
|
||||
$visitLocations->dropColumn('is_empty');
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,10 @@ services:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
environment:
|
||||
LC_ALL: C
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
@@ -42,7 +45,10 @@ services:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
environment:
|
||||
LC_ALL: C
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
@@ -82,6 +88,15 @@ services:
|
||||
MYSQL_DATABASE: shlink
|
||||
MYSQL_INITDB_SKIP_TZINFO: 1
|
||||
|
||||
shlink_db_ms:
|
||||
container_name: shlink_db_ms
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
ports:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
ACCEPT_EULA: Y
|
||||
SA_PASSWORD: "Passw0rd!"
|
||||
|
||||
shlink_redis:
|
||||
container_name: shlink_redis
|
||||
image: redis:5.0-alpine
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Shlink Docker image
|
||||
|
||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||
|
||||
This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime.
|
||||
@@ -38,10 +37,10 @@ Or you can list all tags with:
|
||||
docker exec -it shlink_container shlink tag:list
|
||||
```
|
||||
|
||||
Or process remaining visits with:
|
||||
Or locate remaining visits with:
|
||||
|
||||
```bash
|
||||
docker exec -it shlink_container shlink visit:process
|
||||
docker exec -it shlink_container shlink visit:locate
|
||||
```
|
||||
|
||||
All shlink commands will work the same way.
|
||||
@@ -56,9 +55,9 @@ docker exec -it shlink_container shlink
|
||||
|
||||
The image comes with a working sqlite database, but in production you will probably want to usa a distributed database.
|
||||
|
||||
It is possible to use a set of env vars to make this shlink instance interact with an external MySQL, MariaDB or PostgreSQL database.
|
||||
It is possible to use a set of env vars to make this shlink instance interact with an external MySQL, MariaDB, PostgreSQL or Microsoft SQL Server database.
|
||||
|
||||
* `DB_DRIVER`: **[Mandatory]**. Use the value **mysql**, **maria** or **postgres** to prevent the sqlite database to be used.
|
||||
* `DB_DRIVER`: **[Mandatory]**. Use the value **mysql**, **maria**, **postgres** or **mssql** to prevent the sqlite database to be used.
|
||||
* `DB_NAME`: [Optional]. The database name to be used. Defaults to **shlink**.
|
||||
* `DB_USER`: **[Mandatory]**. The username credential for the database server.
|
||||
* `DB_PASSWORD`: **[Mandatory]**. The password credential for the database server.
|
||||
@@ -67,8 +66,9 @@ It is possible to use a set of env vars to make this shlink instance interact wi
|
||||
* Default value is based on the value provided for `DB_DRIVER`:
|
||||
* **mysql** or **maria** -> `3306`
|
||||
* **postgres** -> `5432`
|
||||
* **mssql** -> `1433`
|
||||
|
||||
> PostgreSQL is supported since v1.16.1 of this image. Do not try to use it with previous versions.
|
||||
> PostgreSQL is supported since v1.16.1 and Microsoft SQL server since v2.1.0. Do not try to use them with previous versions.
|
||||
|
||||
Taking this into account, you could run shlink on a local docker service like this:
|
||||
|
||||
@@ -92,7 +92,7 @@ This is the complete list of supported env vars:
|
||||
|
||||
* `SHORT_DOMAIN_HOST`: The custom short domain used for this shlink instance. For example **doma.in**.
|
||||
* `SHORT_DOMAIN_SCHEMA`: Either **http** or **https**.
|
||||
* `DB_DRIVER`: **sqlite** (which is the default value), **mysql**, **maria** or **postgres**.
|
||||
* `DB_DRIVER`: **sqlite** (which is the default value), **mysql**, **maria**, **postgres** or **mssql**.
|
||||
* `DB_NAME`: The database name to be used when using an external database driver. Defaults to **shlink**.
|
||||
* `DB_USER`: The username credential to be used when using an external database driver.
|
||||
* `DB_PASSWORD`: The password credential to be used when using an external database driver.
|
||||
@@ -101,9 +101,10 @@ This is the complete list of supported env vars:
|
||||
* Default value is based on the value provided for `DB_DRIVER`:
|
||||
* **mysql** or **maria** -> `3306`
|
||||
* **postgres** -> `5432`
|
||||
* **mssql** -> `1433`
|
||||
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
|
||||
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
|
||||
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`.
|
||||
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x is returned (after following redirects) when trying to shorten a URL. Defaults to `false`.
|
||||
* `INVALID_SHORT_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
|
||||
* `REGULAR_404_REDIRECT_TO`: If a URL is provided here, when a user tries to access a URL not matching any one supported by the router, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
|
||||
* `BASE_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access Shlink's base URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
|
||||
@@ -111,6 +112,7 @@ This is the complete list of supported env vars:
|
||||
* `WEB_WORKER_NUM`: The amount of concurrent http requests this shlink instance will be able to server. Defaults to 16.
|
||||
* `TASK_WORKER_NUM`: The amount of concurrent background tasks this shlink instance will be able to execute. Defaults to 16.
|
||||
* `VISITS_WEBHOOKS`: A comma-separated list of URLs that will receive a `POST` request when a short URL receives a visit.
|
||||
* `DEFAULT_SHORT_CODES_LENGTH`: The length you want generated short codes to have. It defaults to 5 and has to be at least 4, so any value smaller than that will fall back to 4.
|
||||
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
|
||||
|
||||
This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately.
|
||||
@@ -119,9 +121,6 @@ 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.
|
||||
|
||||
* `NOT_FOUND_REDIRECT_TO`: **Deprecated since v1.20 in favor of `INVALID_SHORT_URL_REDIRECT_TO`** If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
|
||||
* `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
|
||||
|
||||
An example using all env vars could look like this:
|
||||
|
||||
```bash
|
||||
@@ -138,7 +137,7 @@ docker run \
|
||||
-e DB_PORT=3306 \
|
||||
-e DISABLE_TRACK_PARAM="no-track" \
|
||||
-e DELETE_SHORT_URL_THRESHOLD=30 \
|
||||
-e VALIDATE_URLS=false \
|
||||
-e VALIDATE_URLS=true \
|
||||
-e "INVALID_SHORT_URL_REDIRECT_TO=https://my-landing-page.com" \
|
||||
-e "REGULAR_404_REDIRECT_TO=https://my-landing-page.com" \
|
||||
-e "BASE_URL_REDIRECT_TO=https://my-landing-page.com" \
|
||||
@@ -147,6 +146,7 @@ docker run \
|
||||
-e WEB_WORKER_NUM=64 \
|
||||
-e TASK_WORKER_NUM=32 \
|
||||
-e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \
|
||||
-e DEFAULT_SHORT_CODES_LENGTH=6 \
|
||||
shlinkio/shlink:stable
|
||||
```
|
||||
|
||||
@@ -164,13 +164,14 @@ The whole configuration should have this format, but it can be split into multip
|
||||
"delete_short_url_threshold": 30,
|
||||
"short_domain_schema": "https",
|
||||
"short_domain_host": "doma.in",
|
||||
"validate_url": false,
|
||||
"validate_url": true,
|
||||
"invalid_short_url_redirect_to": "https://my-landing-page.com",
|
||||
"regular_404_redirect_to": "https://my-landing-page.com",
|
||||
"base_url_redirect_to": "https://my-landing-page.com",
|
||||
"base_path": "/my-campaign",
|
||||
"web_worker_num": 64,
|
||||
"task_worker_num": 32,
|
||||
"default_short_codes_length": 6,
|
||||
"redis_servers": [
|
||||
"tcp://172.20.0.1:6379",
|
||||
"tcp://172.20.0.2:6379"
|
||||
@@ -186,15 +187,12 @@ The whole configuration should have this format, but it can be split into multip
|
||||
"password": "123abc",
|
||||
"host": "something.rds.amazonaws.com",
|
||||
"port": "3306"
|
||||
},
|
||||
"not_found_redirect_to": "https://my-landing-page.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes).
|
||||
|
||||
> The `not_found_redirect_to` option has been deprecated in v1.20. Use `invalid_short_url_redirect_to` instead (however, it will still work for backwards compatibility).
|
||||
|
||||
Once created just run shlink with the volume:
|
||||
|
||||
```bash
|
||||
@@ -211,20 +209,12 @@ These are some considerations to take into account when running multiple instanc
|
||||
|
||||
You can (and should) make the locks to be shared by all Shlink instances by using a redis server/cluster. Just define the `REDIS_SERVERS` env var with the list of servers.
|
||||
|
||||
* **Ignore this if using Shlink 1.20 or newer**. The first time shlink is run, it generates a charset used to generate short codes, which is a shuffled base62 charset.
|
||||
|
||||
If you are using several shlink instances, you will probably want all of them to use the same charset.
|
||||
|
||||
You can get a shuffled base62 charset by going to [https://shlink.io/short-code-chars](https://shlink.io/short-code-chars), and then you just need to pass it to all shlink instances using the `SHORTCODE_CHARS` env var.
|
||||
|
||||
If you don't do this, each shlink instance will use a different charset. However this shouldn't be a problem in practice, since the chances to get a collision will be very low.
|
||||
|
||||
## Versions
|
||||
|
||||
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 1.20.0, installing `shlinkio/shlink:stable`, you will get an image containing shlink v1.20.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.
|
||||
|
||||
> **Important**: The docker image was introduced with shlink v1.15.0, so there are no official images previous to that versions.
|
||||
|
||||
15
docker/build
Executable file
15
docker/build
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
# If there is a tag, regardless the branch, build that docker tag and also "stable"
|
||||
if [[ ! -z $TRAVIS_TAG ]]; then
|
||||
docker build --build-arg SHLINK_VERSION=${TRAVIS_TAG#?} -t shlinkio/shlink:${TRAVIS_TAG#?} -t shlinkio/shlink:stable .
|
||||
docker push shlinkio/shlink:${TRAVIS_TAG#?}
|
||||
docker push shlinkio/shlink:stable
|
||||
# If build branch is develop, build latest (on master, when there's no tag, do not build anything)
|
||||
elif [[ "$TRAVIS_BRANCH" == 'develop' ]]; then
|
||||
docker build -t shlinkio/shlink:latest .
|
||||
docker push shlinkio/shlink:latest
|
||||
fi
|
||||
@@ -8,64 +8,26 @@ use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
use function explode;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function Functional\contains;
|
||||
use function implode;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
use function substr;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
$helper = new class {
|
||||
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private const DB_DRIVERS_MAP = [
|
||||
'mysql' => 'pdo_mysql',
|
||||
'maria' => 'pdo_mysql',
|
||||
'postgres' => 'pdo_pgsql',
|
||||
'mssql' => 'pdo_sqlsrv',
|
||||
];
|
||||
private const DB_PORTS_MAP = [
|
||||
'mysql' => '3306',
|
||||
'maria' => '3306',
|
||||
'postgres' => '5432',
|
||||
'mssql' => '1433',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $secretKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
[, $this->secretKey] = $this->initShlinkSecretKey();
|
||||
}
|
||||
|
||||
private function initShlinkSecretKey(): array
|
||||
{
|
||||
$keysFile = sprintf('%s/shlink.keys', sys_get_temp_dir());
|
||||
if (file_exists($keysFile)) {
|
||||
return explode(',', file_get_contents($keysFile));
|
||||
}
|
||||
|
||||
$keys = [
|
||||
'', // This was the SHORTCODE_CHARS. Kept as empty string for BC
|
||||
env('SECRET_KEY', $this->generateSecretKey()), // Deprecated
|
||||
];
|
||||
|
||||
file_put_contents($keysFile, implode(',', $keys));
|
||||
return $keys;
|
||||
}
|
||||
|
||||
private function generateSecretKey(): string
|
||||
{
|
||||
return substr(str_shuffle(self::BASE62), 0, 32);
|
||||
}
|
||||
|
||||
public function getSecretKey(): string
|
||||
{
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
public function getDbConfig(): array
|
||||
{
|
||||
$driver = env('DB_DRIVER');
|
||||
@@ -94,7 +56,7 @@ $helper = new class {
|
||||
public function getNotFoundRedirectsConfig(): array
|
||||
{
|
||||
return [
|
||||
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO', env('NOT_FOUND_REDIRECT_TO')),
|
||||
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'),
|
||||
'regular_404' => env('REGULAR_404_REDIRECT_TO'),
|
||||
'base_url' => env('BASE_URL_REDIRECT_TO'),
|
||||
];
|
||||
@@ -105,6 +67,18 @@ $helper = new class {
|
||||
$webhooks = env('VISITS_WEBHOOKS');
|
||||
return $webhooks === null ? [] : explode(',', $webhooks);
|
||||
}
|
||||
|
||||
public function getRedisConfig(): ?array
|
||||
{
|
||||
$redisServers = env('REDIS_SERVERS');
|
||||
return $redisServers === null ? null : ['servers' => $redisServers];
|
||||
}
|
||||
|
||||
public function getDefaultShortCodesLength(): int
|
||||
{
|
||||
$value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH);
|
||||
return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value;
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -112,7 +86,6 @@ return [
|
||||
'config_cache_enabled' => false,
|
||||
|
||||
'app_options' => [
|
||||
'secret_key' => $helper->getSecretKey(),
|
||||
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
|
||||
],
|
||||
|
||||
@@ -130,8 +103,9 @@ return [
|
||||
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
|
||||
'hostname' => env('SHORT_DOMAIN_HOST', ''),
|
||||
],
|
||||
'validate_url' => (bool) env('VALIDATE_URLS', true),
|
||||
'validate_url' => (bool) env('VALIDATE_URLS', false),
|
||||
'visits_webhooks' => $helper->getVisitsWebhooks(),
|
||||
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
|
||||
],
|
||||
|
||||
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
|
||||
@@ -156,15 +130,15 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'servers' => env('REDIS_SERVERS'),
|
||||
'cache' => [
|
||||
'redis' => $helper->getRedisConfig(),
|
||||
],
|
||||
|
||||
'router' => [
|
||||
'base_path' => env('BASE_PATH', ''),
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'options' => [
|
||||
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
|
||||
|
||||
@@ -12,6 +12,9 @@ php bin/cli db:migrate -n -q
|
||||
echo "Generating proxies..."
|
||||
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
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/zendframework/zend-expressive-swoole/bin/zend-expressive-swoole start; do sleep 1 ; done
|
||||
until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done
|
||||
|
||||
@@ -17,16 +17,6 @@
|
||||
"status": {
|
||||
"type": "number",
|
||||
"description": "HTTP response status code"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "**[Deprecated] Use type instead. Not returned for v2 of the REST API** A machine unique code",
|
||||
"deprecated": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "**[Deprecated] Use detail instead. Not returned for v2 of the REST API** A human-friendly error message",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,9 @@
|
||||
"meta": {
|
||||
"$ref": "./ShortUrlMeta.json"
|
||||
},
|
||||
"originalUrl": {
|
||||
"deprecated": true,
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The original long URL. [DEPRECATED. Use longUrl instead]"
|
||||
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
},
|
||||
"visitLocation": {
|
||||
"$ref": "./VisitLocation.json"
|
||||
},
|
||||
"remoteAddr": {
|
||||
"type": "string",
|
||||
"description": "This value is deprecated and will always be null",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "string"
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "string"
|
||||
"type": "number"
|
||||
},
|
||||
"regionName": {
|
||||
"type": "string"
|
||||
|
||||
9
docs/swagger/parameters/domain.json
Normal file
9
docs/swagger/parameters/domain.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "domain",
|
||||
"description": "The domain in which the short code should be searched for.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "authenticate",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "[Deprecated] Perform authentication",
|
||||
"description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiKey"
|
||||
],
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"description": "The API key to authenticate with",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The authentication worked.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The authentication token that needs to be sent in the Authorization header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "An API key was not provided.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The API key is incorrect, is disabled or has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "List short URLs",
|
||||
"description": "Returns the list of short URLs.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Returns the list of short URLs.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -77,9 +77,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -126,7 +123,8 @@
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
},
|
||||
"domain": null
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
@@ -141,11 +139,12 @@
|
||||
"validSince": null,
|
||||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
}
|
||||
},
|
||||
"domain": null
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
"shortUrl": "https://doma.in/123bA",
|
||||
"shortUrl": "https://example.com/123bA",
|
||||
"longUrl": "https://www.google.com",
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
@@ -154,7 +153,8 @@
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
}
|
||||
},
|
||||
"domain": "example.com"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
@@ -187,13 +187,10 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create short URL",
|
||||
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
|
||||
"description": "Creates a new short URL.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
@@ -246,6 +243,10 @@
|
||||
"domain": {
|
||||
"description": "The domain to which the short URL will be attached",
|
||||
"type": "string"
|
||||
},
|
||||
"shortCodeLength": {
|
||||
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,7 +278,8 @@
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 500
|
||||
}
|
||||
},
|
||||
"domain": null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create a short URL",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -72,7 +72,8 @@
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
},
|
||||
"domain": null
|
||||
},
|
||||
"text/plain": "https://doma.in/abc123"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Parse short code",
|
||||
"description": "Get the long URL behind a short URL's short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Get the long URL behind a short URL's short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -20,21 +20,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"description": "The domain in which the short code should be searched for. Will fall back to default domain if not found.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -61,7 +52,8 @@
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
},
|
||||
"domain": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -94,7 +86,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit short URL",
|
||||
"description": "Update certain meta arguments from an existing short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Update certain meta arguments from an existing short URL.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -107,6 +99,9 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -117,6 +112,10 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"description": "The long URL this short URL will redirect to",
|
||||
"type": "string"
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
@@ -137,9 +136,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -165,6 +161,7 @@
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"longUrl",
|
||||
"validSince",
|
||||
"validUntil",
|
||||
"maxVisits"
|
||||
@@ -201,105 +198,13 @@
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "editShortUrlPut",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "[DEPRECATED] Edit short URL",
|
||||
"description": "**[DEPRECATED]** Use [editShortUrl](#/Short_URLs/getShortUrl) instead",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short code has been properly updated."
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided meta arguments are invalid.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteShortUrl",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Delete short URL",
|
||||
"description": "Deletes the short URL for provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Deletes the short URL for provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -312,14 +217,14 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit tags on short URL",
|
||||
"description": "Edit the tags on URL identified by provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Edit the tags on URL identified by provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -18,6 +18,9 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -46,9 +49,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Visits"
|
||||
],
|
||||
"summary": "List visits for short URL",
|
||||
"description": "Get the list of visits on the short URL behind provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Get the list of visits on the short URL behind provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -19,6 +19,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
},
|
||||
{
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
@@ -59,9 +62,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -108,8 +108,8 @@
|
||||
"cityName": "Cupertino",
|
||||
"countryCode": "US",
|
||||
"countryName": "United States",
|
||||
"latitude": "37.3042",
|
||||
"longitude": "-122.0946",
|
||||
"latitude": 37.3042,
|
||||
"longitude": -122.0946,
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
@@ -78,9 +75,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
@@ -170,9 +164,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
@@ -279,9 +270,6 @@
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "shortUrlPreview",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL preview image",
|
||||
"description": "Returns the preview of the page behind a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
|
||||
"externalDocs": {
|
||||
"url": "https://shlink.io/api-docs",
|
||||
"url": "https://shlink.io/documentation/api-docs",
|
||||
"description": "Find more info on how to start using this API here"
|
||||
},
|
||||
|
||||
@@ -33,12 +33,6 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Api-Key"
|
||||
},
|
||||
"Bearer": {
|
||||
"description": "**[DEPRECATED]** The JWT identifying a previously authenticated API key",
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -63,10 +57,6 @@
|
||||
{
|
||||
"name": "URL Shortener",
|
||||
"description": "Non-rest endpoints, used to be publicly exposed"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "**[DEPRECATED]** Authentication-related endpoints"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -104,13 +94,6 @@
|
||||
},
|
||||
"/{shortCode}/qr-code": {
|
||||
"$ref": "paths/{shortCode}_qr-code.json"
|
||||
},
|
||||
"/{shortCode}/preview": {
|
||||
"$ref": "paths/{shortCode}_preview.json"
|
||||
},
|
||||
|
||||
"/rest/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
hooks/build
10
hooks/build
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
if [[ ${SOURCE_BRANCH} == 'develop' ]]; then
|
||||
SHLINK_RELEASE='latest'
|
||||
else
|
||||
SHLINK_RELEASE=${SOURCE_BRANCH#?}
|
||||
fi
|
||||
|
||||
docker build --build-arg SHLINK_VERSION=${SHLINK_RELEASE} -t ${IMAGE_NAME} .
|
||||
@@ -12,14 +12,9 @@ return [
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
|
||||
@@ -6,19 +6,21 @@ namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use GeoIp2\Database\Reader;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Core\Visit;
|
||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console as SymfonyCli;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY;
|
||||
|
||||
return [
|
||||
|
||||
@@ -34,14 +36,9 @@ return [
|
||||
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -58,22 +55,24 @@ return [
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, 'Shlinkio\Shlink\LocalLockFactory'],
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, LOCAL_LOCK_FACTORY],
|
||||
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||
Service\UrlShortener::class,
|
||||
'config.url_shortener.domain',
|
||||
'config.url_shortener.default_short_codes_length',
|
||||
],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
Visit\VisitLocator::class,
|
||||
IpLocationResolverInterface::class,
|
||||
LockFactory::class,
|
||||
GeolocationDbUpdater::class,
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
|
||||
@@ -19,8 +19,7 @@ class DisableKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
|
||||
@@ -19,8 +19,7 @@ class GenerateKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
@@ -37,7 +36,7 @@ class GenerateKeyCommand extends Command
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date in which the API key should expire. Use any valid PHP format.'
|
||||
'The date in which the API key should expire. Use any valid PHP format.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ class ListKeysCommand extends Command
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
@@ -43,7 +42,7 @@ class ListKeysCommand extends Command
|
||||
'enabledOnly',
|
||||
'e',
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if only enabled API keys should be returned.'
|
||||
'Tells if only enabled API keys should be returned.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,8 +81,6 @@ class ListKeysCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ApiKey $apiKey
|
||||
* @return string
|
||||
*/
|
||||
private function getEnabledSymbol(ApiKey $apiKey): string
|
||||
{
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
/** @deprecated */
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
public const NAME = 'config:generate-charset';
|
||||
private const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(sprintf(
|
||||
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
|
||||
self::DEFAULT_CHARS
|
||||
))
|
||||
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$charSet = str_shuffle(self::DEFAULT_CHARS);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @deprecated */
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
public const NAME = 'config:generate-secret';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption')
|
||||
->setHelp(
|
||||
'<fg=red;options=bold>This command is deprecated. Better leave shlink generate the secret key.</>'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,10 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function array_unshift;
|
||||
|
||||
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
{
|
||||
/** @var ProcessHelper */
|
||||
private $processHelper;
|
||||
/** @var string */
|
||||
private $phpBinary;
|
||||
private ProcessHelper $processHelper;
|
||||
private string $phpBinary;
|
||||
|
||||
public function __construct(LockFactory $locker, ProcessHelper $processHelper, PhpExecutableFinder $phpFinder)
|
||||
{
|
||||
@@ -29,7 +25,7 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
|
||||
protected function runPhpCommand(OutputInterface $output, array $command): void
|
||||
{
|
||||
array_unshift($command, $this->phpBinary);
|
||||
$command = [$this->phpBinary, ...$command, '--no-interaction'];
|
||||
$this->processHelper->mustRun($output, $command);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||
|
||||
/** @var Connection */
|
||||
private $regularConn;
|
||||
/** @var Connection */
|
||||
private $noDbNameConn;
|
||||
private Connection $regularConn;
|
||||
private Connection $noDbNameConn;
|
||||
|
||||
public function __construct(
|
||||
LockFactory $locker,
|
||||
@@ -43,7 +41,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
'Creates the database needed for shlink to work. It will do nothing if the database already exists'
|
||||
'Creates the database needed for shlink to work. It will do nothing if the database already exists',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -19,10 +20,8 @@ use function sprintf;
|
||||
class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete';
|
||||
private const ALIASES = ['short-code:delete'];
|
||||
|
||||
/** @var DeleteShortUrlServiceInterface */
|
||||
private $deleteShortUrlService;
|
||||
private DeleteShortUrlServiceInterface $deleteShortUrlService;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
{
|
||||
@@ -34,7 +33,6 @@ class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('Deletes a short URL')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
|
||||
->addOption(
|
||||
@@ -42,34 +40,40 @@ class DeleteShortUrlCommand extends Command
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||
. 'accidentally deleted'
|
||||
. 'accidentally deleted',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The domain if the short code does not belong to the default one',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
||||
$ignoreThreshold = $input->getOption('ignore-threshold');
|
||||
|
||||
try {
|
||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||
$this->runDelete($io, $identifier, $ignoreThreshold);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Exception\ShortUrlNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
return $this->retry($io, $shortCode, $e->getMessage());
|
||||
return $this->retry($io, $identifier, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function retry(SymfonyStyle $io, string $shortCode, string $warningMsg): int
|
||||
private function retry(SymfonyStyle $io, ShortUrlIdentifier $identifier, string $warningMsg): int
|
||||
{
|
||||
$io->writeln(sprintf('<bg=yellow>%s</>', $warningMsg));
|
||||
$forceDelete = $io->confirm('Do you want to delete it anyway?', false);
|
||||
|
||||
if ($forceDelete) {
|
||||
$this->runDelete($io, $shortCode, true);
|
||||
$this->runDelete($io, $identifier, true);
|
||||
} else {
|
||||
$io->warning('Short URL was not deleted.');
|
||||
}
|
||||
@@ -77,9 +81,9 @@ class DeleteShortUrlCommand extends Command
|
||||
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
|
||||
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
||||
{
|
||||
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
|
||||
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode));
|
||||
$this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold);
|
||||
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @deprecated */
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||
|
||||
/** @var PreviewGeneratorInterface */
|
||||
private $previewGenerator;
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, PreviewGeneratorInterface $previewGenerator)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->previewGenerator = $previewGenerator;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
'[DEPRECATED] Processes and generates the previews for every URL, improving performance for later web '
|
||||
. 'requests.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$page = 1;
|
||||
do {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($page);
|
||||
$page += 1;
|
||||
|
||||
foreach ($shortUrls as $shortUrl) {
|
||||
$this->processUrl($shortUrl->getLongUrl(), $output);
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function processUrl($url, OutputInterface $output): void
|
||||
{
|
||||
try {
|
||||
$output->write(sprintf('Processing URL %s...', $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln(' <info>Success!</info>');
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$output->writeln(' <error>Error</error>');
|
||||
if ($output->isVerbose()) {
|
||||
$this->getApplication()->renderThrowable($e, $output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,19 @@ 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;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\curry;
|
||||
@@ -26,70 +27,74 @@ use function sprintf;
|
||||
class GenerateShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:generate';
|
||||
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
private UrlShortenerInterface $urlShortener;
|
||||
private array $domainConfig;
|
||||
private int $defaultShortCodeLength;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig, int $defaultShortCodeLength)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->domainConfig = $domainConfig;
|
||||
$this->defaultShortCodeLength = $defaultShortCodeLength;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('Generates a short URL for provided long URL and returns it')
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
'Tags to apply to the new short URL'
|
||||
'Tags to apply to the new short URL',
|
||||
)
|
||||
->addOption(
|
||||
'validSince',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.'
|
||||
. 'If someone tries to access it before this date, it will not be found.',
|
||||
)
|
||||
->addOption(
|
||||
'validUntil',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.'
|
||||
. 'If someone tries to access it after this date, it will not be found.',
|
||||
)
|
||||
->addOption(
|
||||
'customSlug',
|
||||
'c',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
'If provided, this slug will be used instead of generating a short code',
|
||||
)
|
||||
->addOption(
|
||||
'maxVisits',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.'
|
||||
'This will limit the number of visits for this short URL.',
|
||||
)
|
||||
->addOption(
|
||||
'findIfExists',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'This will force existing matching URL to be returned if found, instead of creating a new one.'
|
||||
'This will force existing matching URL to be returned if found, instead of creating a new one.',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The domain to which this short URL will be attached.'
|
||||
'The domain to which this short URL will be attached.',
|
||||
)
|
||||
->addOption(
|
||||
'shortCodeLength',
|
||||
'l',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The length for generated short code (it will be ignored if --customSlug was provided).',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,19 +125,21 @@ class GenerateShortUrlCommand extends Command
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlShortener->urlToShortCode(
|
||||
new Uri($longUrl),
|
||||
$tags,
|
||||
ShortUrlMeta::createFromParams(
|
||||
$input->getOption('validSince'),
|
||||
$input->getOption('validUntil'),
|
||||
$customSlug,
|
||||
$maxVisits !== null ? (int) $maxVisits : null,
|
||||
$input->getOption('findIfExists'),
|
||||
$input->getOption('domain')
|
||||
)
|
||||
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([
|
||||
|
||||
@@ -9,10 +9,13 @@ use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
@@ -22,10 +25,8 @@ use function Functional\select_keys;
|
||||
class GetVisitsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
|
||||
/** @var VisitsTrackerInterface */
|
||||
private $visitsTracker;
|
||||
private VisitsTrackerInterface $visitsTracker;
|
||||
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
||||
{
|
||||
@@ -37,9 +38,9 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('Returns the detailed visits information for provided short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get');
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
|
||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code');
|
||||
}
|
||||
|
||||
protected function getStartDateDesc(): string
|
||||
@@ -68,15 +69,15 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
||||
$startDate = $this->getDateOption($input, $output, 'startDate');
|
||||
$endDate = $this->getDateOption($input, $output, 'endDate');
|
||||
|
||||
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
|
||||
$paginator = $this->visitsTracker->info($identifier, new VisitsParams(new DateRange($startDate, $endDate)));
|
||||
|
||||
$rows = map($paginator->getCurrentItems(), function (Visit $visit) {
|
||||
$rowData = $visit->jsonSerialize();
|
||||
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||
$rowData['country'] = ($visit->getVisitLocation() ?? new UnknownVisitLocation())->getCountryName();
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||
});
|
||||
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
|
||||
@@ -4,20 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Laminas\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
@@ -32,7 +33,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
private const COLUMNS_WHITELIST = [
|
||||
'shortCode',
|
||||
'shortUrl',
|
||||
@@ -42,10 +42,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
'tags',
|
||||
];
|
||||
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
/** @var ShortUrlDataTransformer */
|
||||
private $transformer;
|
||||
private ShortUrlServiceInterface $shortUrlService;
|
||||
private ShortUrlDataTransformer $transformer;
|
||||
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
|
||||
{
|
||||
@@ -58,32 +56,31 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('List all short URLs')
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE),
|
||||
'1'
|
||||
'1',
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
'st',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields',
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A comma-separated list of tags to filter results'
|
||||
'A comma-separated list of tags to filter results',
|
||||
)
|
||||
->addOption(
|
||||
'orderBy',
|
||||
'o',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma',
|
||||
)
|
||||
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
|
||||
}
|
||||
@@ -112,7 +109,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
$orderBy = $this->processOrderBy($input);
|
||||
|
||||
do {
|
||||
$result = $this->renderPage($output, $page, $searchTerm, $tags, $showTags, $startDate, $endDate, $orderBy);
|
||||
$result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData([
|
||||
ShortUrlsParamsInputFilter::PAGE => $page,
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
|
||||
ShortUrlsParamsInputFilter::TAGS => $tags,
|
||||
ShortUrlsOrdering::ORDER_BY => $orderBy,
|
||||
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
|
||||
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
|
||||
]));
|
||||
$page++;
|
||||
|
||||
$continue = $this->isLastPage($result)
|
||||
@@ -126,23 +130,9 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function renderPage(
|
||||
OutputInterface $output,
|
||||
int $page,
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
bool $showTags,
|
||||
?Chronos $startDate,
|
||||
?Chronos $endDate,
|
||||
$orderBy
|
||||
): Paginator {
|
||||
$result = $this->shortUrlService->listShortUrls(
|
||||
$page,
|
||||
$searchTerm,
|
||||
$tags,
|
||||
$orderBy,
|
||||
new DateRange($startDate, $endDate)
|
||||
);
|
||||
private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params): Paginator
|
||||
{
|
||||
$result = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
@@ -163,7 +153,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
|
||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
||||
$result,
|
||||
'Page %s of %s'
|
||||
'Page %s of %s',
|
||||
));
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -19,22 +20,19 @@ use function sprintf;
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:parse';
|
||||
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener)
|
||||
public function __construct(ShortUrlResolverInterface $urlResolver)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->urlResolver = $urlResolver;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('Returns the long URL behind a short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse')
|
||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain to which the short URL is attached.');
|
||||
@@ -57,11 +55,9 @@ class ResolveUrlCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$domain = $input->getOption('domain');
|
||||
|
||||
try {
|
||||
$url = $this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
||||
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
|
||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
|
||||
@@ -16,8 +16,7 @@ class CreateTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
@@ -34,7 +33,7 @@ class CreateTagCommand extends Command
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'The name of the tags to create'
|
||||
'The name of the tags to create',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ class DeleteTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:delete';
|
||||
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
@@ -34,7 +33,7 @@ class DeleteTagsCommand extends Command
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'The name of the tags to delete'
|
||||
'The name of the tags to delete',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user